{-# OPTIONS_GHC -fno-warn-unused-top-binds #-} {-# OPTIONS_HADDOCK not-home #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeFamilies #-} {-| This module provides functions for creating and running migrations written as plain SQL files. By using 'runMigrations', the migrations will be compiled directly into your Haskell application using Template Haskell, so the files will not need to be present in the eventual runtime environment. For more information, see the documentation for 'runMigrations'. -} module Genesis.Persist.Migrate (runMigrations, runMigrations') where import qualified Data.Text.IO as T import Control.Monad (filterM, forM, forM_, unless, when) import Control.Monad.Logger (MonadLogger, logDebugNS, logInfoNS) import Control.Monad.Persist (MonadSqlPersist, insert_, rawExecute, runMigrationSilent, selectList, transactionSave) import System.Directory (doesDirectoryExist, doesFileExist, doesPathExist, listDirectory) import System.FilePath (()) import Data.FileEmbed (makeRelativeToProject) import Data.List (isSuffixOf, sort) import Data.Monoid ((<>)) import Data.Text (Text) import Data.Text.Conversions (toText) import Database.Persist.Sql (Entity(..)) import Database.Persist.TH (share, mkPersist, sqlSettings, mkMigrate, persistLowerCase) import Language.Haskell.TH import Language.Haskell.TH.Syntax share [mkPersist sqlSettings, mkMigrate "migrateSchema"] [persistLowerCase| SchemaMigration name FilePath deriving Eq Show |] {-| Compiles a set of .sql files in a particular directory into your application, then runs them against a database at runtime. This function is implemented with Template Haskell so that it can read the migration files at compile-time, but it semantically has the type @('MonadLogger' m, 'MonadSqlPersist' m) => 'FilePath' -> m ()@. Migrations will be executed in order based on their filename, according to 'sort'. The 'FilePath' provided should be relative to your /project root/, and it will detect all files within the immediate directory (that is, /not/ in subdirectories) with the suffix @.sql@. Example: @ main :: IO () main = 'Control.Monad.Logger.runStderrLoggingT' $ withSqliteConn ":memory:" ('Control.Monad.Persist.runSqlPersistT' $('runMigrations' "db/migrations")) @ -} runMigrations :: FilePath -> Q Exp runMigrations dir = do migrationsDir <- makeRelativeToProject dir migrationsDirExists <- runIO $ doesPathExist migrationsDir unless migrationsDirExists $ fail $ "No such directory ‘" ++ dir ++ "’ exists at root of project.\n (Looking at ‘" ++ migrationsDir ++ "’)." migrationsDirIsDir <- runIO $ doesDirectoryExist migrationsDir unless migrationsDirIsDir $ fail $ "The file at ‘" ++ dir ++ "’ in root of project is not a directory.\n (Looking at ‘" ++ migrationsDir ++ "’)." allFiles <- runIO $ listDirectory migrationsDir nonDirectoryFiles <- runIO $ filterM (doesFileExist . (migrationsDir )) allFiles let migrationFiles = sort $ filter (isSuffixOf ".sql") nonDirectoryFiles forM_ migrationFiles (addDependentFile . (migrationsDir )) when (null migrationFiles) $ reportWarning $ "No migrations (files ending in .sql) in ‘" ++ dir ++ "’ at root of project.\n (Looking in ‘" ++ migrationsDir ++ "’.)\n" migrations <- runIO $ forM migrationFiles $ \path -> (path,) <$> T.readFile (migrationsDir path) [| runMigrations' migrations |] {-| The low-level API that underlies 'runMigrations'. It is unlikely that you will need to use this. Unlike 'runMigrations', this function does not use Template Haskell. Migrations are passed as a list of tuples, where the first element is the name of the migration (which must be unique) and the second element is the SQL to be run. The migrations will be run in the order they are provided, from left to right. -} runMigrations' :: (MonadLogger m, MonadSqlPersist m) => [(FilePath, Text)] -> m () runMigrations' allMigrations = do messages <- runMigrationSilent migrateSchema forM_ messages (logDebugNS "SQL") transactionSave existingMigrations <- map (schemaMigrationName . entityVal) <$> selectList [] [] let newMigrations = filter (\(path, _) -> path `notElem` existingMigrations) allMigrations forM_ newMigrations $ \(path, sql) -> do logInfoNS "migrate" $ "migrating ‘" <> toText path <> "’" rawExecute sql [] insert_ (SchemaMigration path) transactionSave