module Database.PostgreSQL.Simple.Migration
( runMigration
, MigrationContext(..)
, MigrationCommand(..)
, MigrationResult(..)
, ScriptName
) where
import Control.Monad (liftM, void, when)
import qualified Crypto.Hash.MD5 as MD5 (hash)
import qualified Data.ByteString as BS (ByteString, readFile)
import qualified Data.ByteString.Base64 as B64 (encode)
import Data.List (isPrefixOf, sort)
import Data.Monoid (mconcat)
import Database.PostgreSQL.Simple (Connection, Only (..),
execute, execute_, query)
import Database.PostgreSQL.Simple.Types (Query (..))
import System.Directory (getDirectoryContents)
runMigration :: MigrationContext -> IO (MigrationResult String)
runMigration (MigrationContext cmd verbose con) = case cmd of
MigrationInitialization ->
initializeSchema con verbose >> return MigrationSuccess
MigrationDirectory path ->
executeDirectoryMigration con verbose path
MigrationScript name contents ->
executeMigration con verbose name contents
MigrationFile name path ->
executeMigration con verbose name =<< BS.readFile path
executeDirectoryMigration :: Connection -> Bool -> FilePath -> IO (MigrationResult String)
executeDirectoryMigration con verbose dir =
liftM (filter (\x -> not $ "." `isPrefixOf` x))
(getDirectoryContents dir) >>= go . sort
where
go [] = return MigrationSuccess
go (f:fs) = do
r <- executeMigration con verbose f =<< BS.readFile (dir ++ "/" ++ f)
case r of
MigrationError _ ->
return r
MigrationSuccess ->
go fs
executeMigration :: Connection -> Bool -> ScriptName -> BS.ByteString -> IO (MigrationResult String)
executeMigration con verbose name contents = do
let checksum = md5Hash contents
checkScript con name checksum >>= \r -> case r of
ScriptOk -> do
when verbose $ putStrLn $ "Ok:\t" ++ name
return MigrationSuccess
ScriptNotExecuted -> do
void $ execute_ con (Query contents)
void $ execute con q (name, checksum)
when verbose $ putStrLn $ "Execute:\t" ++ name
return MigrationSuccess
ScriptModified _ -> do
when verbose $ putStrLn $ "Fail:\t" ++ name
return (MigrationError name)
where
q = "insert into schema_migrations(filename, checksum) values(?, ?) "
initializeSchema :: Connection -> Bool -> IO ()
initializeSchema con verbose = do
when verbose $ putStrLn "Initializing schema"
void $ execute_ con $ mconcat
[ "create table if not exists schema_migrations "
, "( filename varchar(512) not null"
, ", checksum varchar(32) not null"
, ", executed_at timestamp without time zone not null default now() "
, ");"
]
checkScript :: Connection -> ScriptName -> Checksum -> IO CheckScriptResult
checkScript con name checksum =
query con q (Only name) >>= \r -> case r of
[] ->
return ScriptNotExecuted
Only actualChecksum:_ | checksum == actualChecksum ->
return ScriptOk
Only actualChecksum:_ ->
return (ScriptModified actualChecksum)
where
q = mconcat
[ "select checksum from schema_migrations "
, "where filename = ? limit 1"
]
md5Hash :: BS.ByteString -> Checksum
md5Hash = B64.encode . MD5.hash
type Checksum = BS.ByteString
type ScriptName = String
data MigrationCommand
= MigrationInitialization
| MigrationDirectory FilePath
| MigrationFile ScriptName FilePath
| MigrationScript ScriptName BS.ByteString
deriving (Show, Eq, Read, Ord)
data CheckScriptResult
= ScriptOk
| ScriptModified Checksum
| ScriptNotExecuted
deriving (Show, Eq, Read, Ord)
data MigrationResult a
= MigrationError a
| MigrationSuccess
deriving (Show, Eq, Read, Ord)
data MigrationContext = MigrationContext
{ migrationContextCommand :: MigrationCommand
, migrationContextVerbose :: Bool
, migrationContextConnection :: Connection
}