{-# LANGUAGE
DataKinds
, DeriveGeneric
, FlexibleContexts
, FlexibleInstances
, GADTs
, LambdaCase
, OverloadedLabels
, OverloadedStrings
, PolyKinds
, QuantifiedConstraints
, RankNTypes
, TypeApplications
, TypeOperators
#-}
module Squeal.PostgreSQL.Migration
(
Migration (..)
, Migratory (..)
, Terminally (..)
, terminally
, pureMigration
, MigrationsTable
, defaultMain
) where
import Control.Category
import Control.Monad
import Data.ByteString (ByteString)
import Data.Foldable (traverse_)
import Data.Function ((&))
import Data.List ((\\))
import Data.Text (Text)
import Data.Time (UTCTime)
import Prelude hiding ((.), id)
import System.Environment
import UnliftIO (MonadIO (..))
import qualified Data.Text.IO as Text (putStrLn)
import qualified Generics.SOP as SOP
import qualified GHC.Generics as GHC
import Squeal.PostgreSQL.Alias
import Squeal.PostgreSQL.Binary
import Squeal.PostgreSQL.Definition
import Squeal.PostgreSQL.Expression.Comparison
import Squeal.PostgreSQL.Expression.Parameter
import Squeal.PostgreSQL.Expression.Time
import Squeal.PostgreSQL.Expression.Type
import Squeal.PostgreSQL.List
import Squeal.PostgreSQL.Manipulation
import Squeal.PostgreSQL.PQ
import Squeal.PostgreSQL.Query
import Squeal.PostgreSQL.Schema
import Squeal.PostgreSQL.Transaction
data Migration p schemas0 schemas1 = Migration
{ name :: Text
, up :: p schemas0 schemas1
, down :: p schemas1 schemas0
} deriving (GHC.Generic)
class Category p => Migratory p where
migrateUp
:: AlignedList (Migration p) schemas0 schemas1
-> PQ schemas0 schemas1 IO ()
migrateDown
:: AlignedList (Migration p) schemas0 schemas1
-> PQ schemas1 schemas0 IO ()
instance Migratory Definition where
migrateUp = migrateUp . mapAligned pureMigration
migrateDown = migrateDown . mapAligned pureMigration
newtype Terminally trans monad x0 x1 = Terminally
{ runTerminally :: trans x0 x1 monad () }
deriving GHC.Generic
instance
( IndexedMonadTransPQ trans
, Monad monad
, forall x0 x1. x0 ~ x1 => Monad (trans x0 x1 monad) )
=> Category (Terminally trans monad) where
id = Terminally (return ())
Terminally g . Terminally f = Terminally $ pqThen g f
terminally
:: Functor (trans x0 x1 monad)
=> trans x0 x1 monad ignore
-> Terminally trans monad x0 x1
terminally = Terminally . void
pureMigration
:: Migration Definition schemas0 schemas1
-> Migration (Terminally PQ IO) schemas0 schemas1
pureMigration migration = Migration
{ name = name migration
, up = terminally . define $ up migration
, down = terminally . define $ down migration
}
instance Migratory (Terminally PQ IO) where
migrateUp migration = unsafePQ . transactionally_ $ do
define createMigrations
upMigrations migration
where
upMigrations
:: AlignedList (Migration (Terminally PQ IO)) schemas0 schemas1
-> PQ MigrationsSchemas MigrationsSchemas IO ()
upMigrations = \case
Done -> return ()
step :>> steps -> upMigration step >> upMigrations steps
upMigration
:: Migration (Terminally PQ IO) schemas0 schemas1
-> PQ MigrationsSchemas MigrationsSchemas IO ()
upMigration step = do
executed <- queryExecuted step
unless (executed == 1) $ do
unsafePQ . runTerminally $ up step
manipulateParams_ insertMigration (Only (name step))
queryExecuted
:: Migration (Terminally PQ IO) schemas0 schemas1
-> PQ MigrationsSchemas MigrationsSchemas IO Row
queryExecuted step = do
result <- runQueryParams selectMigration (Only (name step))
ntuples result
migrateDown migrations = unsafePQ . transactionally_ $ do
define createMigrations
downMigrations migrations
where
downMigrations
:: AlignedList (Migration (Terminally PQ IO)) schemas0 schemas1
-> PQ MigrationsSchemas MigrationsSchemas IO ()
downMigrations = \case
Done -> return ()
step :>> steps -> downMigrations steps >> downMigration step
downMigration
:: Migration (Terminally PQ IO) schemas0 schemas1
-> PQ MigrationsSchemas MigrationsSchemas IO ()
downMigration step = do
executed <- queryExecuted step
unless (executed == 0) $ do
unsafePQ . runTerminally $ down step
manipulateParams_ deleteMigration (Only (name step))
queryExecuted
:: Migration (Terminally PQ IO) schemas0 schemas1
-> PQ MigrationsSchemas MigrationsSchemas IO Row
queryExecuted step = do
result <- runQueryParams selectMigration (Only (name step))
ntuples result
unsafePQ :: (Functor m) => PQ db0 db1 m x -> PQ db0' db1' m x
unsafePQ (PQ pq) = PQ $ fmap (SOP.K . SOP.unK) . pq . SOP.K . SOP.unK
type MigrationsTable =
'[ "migrations_unique_name" ::: 'Unique '["name"]] :=>
'[ "name" ::: 'NoDef :=> 'NotNull 'PGtext
, "executed_at" ::: 'Def :=> 'NotNull 'PGtimestamptz
]
data MigrationRow =
MigrationRow { migrationName :: Text
, migrationTime :: UTCTime }
deriving (GHC.Generic, Show)
instance SOP.Generic MigrationRow
instance SOP.HasDatatypeInfo MigrationRow
type MigrationsSchema = '["schema_migrations" ::: 'Table MigrationsTable]
type MigrationsSchemas = Public MigrationsSchema
createMigrations :: Definition MigrationsSchemas MigrationsSchemas
createMigrations =
createTableIfNotExists #schema_migrations
( (text & notNullable) `as` #name :*
(timestampWithTimeZone & notNullable & default_ currentTimestamp)
`as` #executed_at )
( unique #name `as` #migrations_unique_name )
insertMigration :: Manipulation_ MigrationsSchemas (Only Text) ()
insertMigration = insertInto_ #schema_migrations
(Values_ (Set (param @1) `as` #name :* Default `as` #executed_at))
deleteMigration :: Manipulation_ MigrationsSchemas (Only Text) ()
deleteMigration = deleteFrom_ #schema_migrations (#name .== param @1)
selectMigration
:: Query_ MigrationsSchemas (Only Text) (Only UTCTime)
selectMigration = select_ (#executed_at `as` #fromOnly)
$ from (table (#schema_migrations))
& where_ (#name .== param @1)
selectMigrations :: Query_ MigrationsSchemas () MigrationRow
selectMigrations = select_
(#name `as` #migrationName :* #executed_at `as` #migrationTime)
(from (table #schema_migrations))
data MigrateCommand
= MigrateStatus
| MigrateUp
| MigrateDown deriving (GHC.Generic, Show)
defaultMain
:: Migratory p
=> ByteString
-> AlignedList (Migration p) db0 db1
-> IO ()
defaultMain connectTo migrations = do
command <- readCommandFromArgs
maybe (pure ()) performCommand command
where
performCommand :: MigrateCommand -> IO ()
performCommand = \case
MigrateStatus -> withConnection connectTo $
suppressNotices >> migrateStatus
MigrateUp -> withConnection connectTo $
suppressNotices & pqThen (migrateUp migrations) & pqThen migrateStatus
MigrateDown -> withConnection connectTo $
suppressNotices & pqThen (migrateDown migrations) & pqThen migrateStatus
migrateStatus :: PQ schema schema IO ()
migrateStatus = unsafePQ $ do
runNames <- getRunMigrationNames
let names = extractList name migrations
unrunNames = names \\ runNames
liftIO $ displayRunned runNames >> displayUnrunned unrunNames
suppressNotices :: PQ schema schema IO ()
suppressNotices = manipulate_ $
UnsafeManipulation "SET client_min_messages TO WARNING;"
readCommandFromArgs :: IO (Maybe MigrateCommand)
readCommandFromArgs = getArgs >>= \case
["migrate"] -> pure . Just $ MigrateUp
["rollback"] -> pure . Just $ MigrateDown
["status"] -> pure . Just $ MigrateStatus
args -> displayUsage args >> pure Nothing
displayUsage :: [String] -> IO ()
displayUsage args = do
putStrLn $ "Invalid command: \"" <> unwords args <> "\". Use:"
putStrLn "migrate to run all available migrations"
putStrLn "rollback to rollback all available migrations"
putStrLn "status to display migrations run and migrations left to run"
getRunMigrationNames :: (MonadIO m) => PQ db0 db0 m [Text]
getRunMigrationNames =
fmap migrationName <$> (unsafePQ (define createMigrations & pqThen (runQuery selectMigrations)) >>= getRows)
displayListOfNames :: [Text] -> IO ()
displayListOfNames [] = Text.putStrLn " None"
displayListOfNames xs =
let singleName n = Text.putStrLn $ " - " <> n
in traverse_ singleName xs
displayUnrunned :: [Text] -> IO ()
displayUnrunned unrunned =
Text.putStrLn "Migrations left to run:"
>> displayListOfNames unrunned
displayRunned :: [Text] -> IO ()
displayRunned runned =
Text.putStrLn "Migrations already run:"
>> displayListOfNames runned