{-# LANGUAGE GADTs #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleInstances #-}
module Morph.Migrator
( migrate
) where
import Control.Monad
import Data.Function
import Data.List
import Data.Monoid
import Data.String
import System.Directory
import System.FilePath
import System.IO
import Database.PostgreSQL.Simple
data MigrationType = Full | Rollback
type family MigrationSQL (a :: MigrationType) :: * where
MigrationSQL 'Full = (Query, String)
MigrationSQL 'Rollback = Query
data Migration :: MigrationType -> * where
Migration ::
{ migrationIdentifier :: String
, migrationSQL :: MigrationSQL a
} -> Migration a
createMigrationTable :: Connection -> IO ()
createMigrationTable conn = void $ execute_ conn
"CREATE TABLE IF NOT EXISTS migrations (\
\ id varchar PRIMARY KEY CHECK (id <> ''),\
\ rollback_sql text CHECK (rollback_sql <> '')\
\);"
listDone :: Connection -> IO [Migration 'Rollback]
listDone conn = do
pairs <- query_ conn "SELECT id, rollback_sql FROM migrations ORDER BY id ASC"
return $ flip map pairs $ \(identifier, mSQL) -> Migration
{ migrationIdentifier = identifier
, migrationSQL = maybe "" fromString mSQL
}
listGoals :: FilePath -> IO [Migration 'Full]
listGoals dir = do
allNames <- sort <$> getDirectoryContents dir
let upNames = filter (".up.sql" `isSuffixOf`) allNames
downNames = filter (".down.sql" `isSuffixOf`) allNames
forM upNames $ \upName -> do
let identifier = extractIdentifier upName
up <- readMigrationFile upName
down <- readDownMigrationFile downNames identifier
return Migration
{ migrationIdentifier = identifier
, migrationSQL = (up, down)
}
where
extractIdentifier :: FilePath -> String
extractIdentifier = takeWhile (`elem` ("0123456789" :: String))
readMigrationFile :: FilePath -> IO Query
readMigrationFile path = do
contents <- readFile $ dir </> path
return $ fromString contents
readDownMigrationFile :: [FilePath] -> String -> IO String
readDownMigrationFile paths identifier =
case find ((==identifier) . extractIdentifier) paths of
Nothing -> return $
"RAISE EXCEPTION 'No rollback migration found for "
<> fromString identifier <> "';"
Just path -> readFile $ dir </> path
rollbackMigration :: Connection -> Migration 'Rollback -> IO ()
rollbackMigration conn migration = do
hPutStrLn stderr $
"Rollbacking migration " ++ migrationIdentifier migration ++ " ..."
void $ execute_ conn $ migrationSQL migration
void $ execute conn "DELETE FROM migrations WHERE id = ?" $
Only $ migrationIdentifier migration
doMigration :: Connection -> Migration 'Full -> IO ()
doMigration conn migration = do
hPutStrLn stderr $
"Running migration " ++ migrationIdentifier migration ++ " ..."
let (up, down) = migrationSQL migration
void $ execute_ conn up
void $ execute conn "INSERT INTO migrations (id, rollback_sql) VALUES (?, ?)"
(migrationIdentifier migration, down)
migrate :: Bool -> Connection -> FilePath -> IO ()
migrate inTransaction conn dir = do
createMigrationTable conn
doneMigrations <- listDone conn
goalMigrations <- listGoals dir
let doneIdentifiers = map migrationIdentifier doneMigrations
goalIdentifiers = map migrationIdentifier goalMigrations
toRollbackIdentifiers = doneIdentifiers \\ goalIdentifiers
toDoIdentifiers = goalIdentifiers \\ doneIdentifiers
toRollback = sortBy (flip (compare `on` migrationIdentifier)) $
filter ((`elem` toRollbackIdentifiers) . migrationIdentifier)
doneMigrations
toDo = filter ((`elem` toDoIdentifiers) . migrationIdentifier)
goalMigrations
(if inTransaction then withTransaction conn else id) $ do
forM_ toRollback $ rollbackMigration conn
forM_ toDo $ doMigration conn