{-# LANGUAGE AllowAmbiguousTypes #-}
module Database.Beam.Migrate.Simple
( autoMigrate
, simpleSchema
, simpleMigration
, runSimpleMigration
, backendMigrationScript
, VerificationResult(..)
, verifySchema
, createSchema
, BringUpToDateHooks(..)
, defaultUpToDateHooks
, bringUpToDate, bringUpToDateWithHooks
, haskellSchema
, module Database.Beam.Migrate.Actions
, module Database.Beam.Migrate.Types ) where
import Prelude hiding (log)
import Database.Beam
import Database.Beam.Backend
import Database.Beam.Haskell.Syntax
import Database.Beam.Migrate.Actions
import Database.Beam.Migrate.Backend
import Database.Beam.Migrate.Checks (HasDataTypeCreatedCheck)
import Database.Beam.Migrate.Log
import Database.Beam.Migrate.SQL (BeamMigrateSqlBackendDataTypeSyntax)
import Database.Beam.Migrate.Types
import Control.Monad.Cont
import Control.Monad.Writer
import Control.Monad.State
import qualified Data.HashSet as HS
import Data.Semigroup (Max(..))
import qualified Data.Text as T
data BringUpToDateHooks m
= BringUpToDateHooks
{ runIrreversibleHook :: m Bool
, startStepHook :: Int -> T.Text -> m ()
, endStepHook :: Int -> T.Text -> m ()
, runCommandHook :: Int -> String -> m ()
, queryFailedHook :: m ()
, discontinuousMigrationsHook
:: Int -> m ()
, logMismatchHook :: Int -> T.Text -> T.Text -> m ()
, databaseAheadHook :: Int -> m ()
}
defaultUpToDateHooks :: Monad m => BringUpToDateHooks m
defaultUpToDateHooks =
BringUpToDateHooks
{ runIrreversibleHook = pure False
, startStepHook = \_ _ -> pure ()
, endStepHook = \_ _ -> pure ()
, runCommandHook = \_ _ -> pure ()
, queryFailedHook = fail "Log entry query fails"
, discontinuousMigrationsHook =
\ix -> fail ("Discontinuous migration log: missing migration at " ++ show ix)
, logMismatchHook =
\ix actual expected ->
fail ("Log mismatch at index " ++ show ix ++ ":\n" ++
" expected: " ++ T.unpack expected ++ "\n" ++
" actual : " ++ T.unpack actual)
, databaseAheadHook =
\aheadBy ->
fail ("The database is ahead of the known schema by " ++ show aheadBy ++ " migration(s)")
}
bringUpToDate :: ( Database be db
, HasDataTypeCreatedCheck (BeamMigrateSqlBackendDataTypeSyntax be) )
=> BeamMigrationBackend be m
-> MigrationSteps be () (CheckedDatabaseSettings be db)
-> m (Maybe (CheckedDatabaseSettings be db))
bringUpToDate be@BeamMigrationBackend {} =
bringUpToDateWithHooks defaultUpToDateHooks be
bringUpToDateWithHooks :: forall db be m
. ( Database be db
, HasDataTypeCreatedCheck (BeamMigrateSqlBackendDataTypeSyntax be) )
=> BringUpToDateHooks m
-> BeamMigrationBackend be m
-> MigrationSteps be () (CheckedDatabaseSettings be db)
-> m (Maybe (CheckedDatabaseSettings be db))
bringUpToDateWithHooks hooks be@(BeamMigrationBackend { backendRenderSyntax = renderSyntax' }) steps = do
ensureBackendTables be
entries <- runSelectReturningList $ select $
all_ (_beamMigrateLogEntries (beamMigrateDb @be @m))
let verifyMigration :: Int -> T.Text -> Migration be a -> StateT [LogEntry] (WriterT (Max Int) m) a
verifyMigration stepIx stepNm step =
do log <- get
case log of
[] -> pure ()
LogEntry actId actStepNm _:log'
| actId == stepIx && actStepNm == stepNm ->
tell (Max stepIx) >> put log'
| actId /= stepIx ->
lift . lift $ discontinuousMigrationsHook hooks stepIx
| otherwise ->
lift . lift $ logMismatchHook hooks stepIx actStepNm stepNm
executeMigration (\_ -> pure ()) step
(futureEntries, Max lastCommit) <-
runWriterT (execStateT (runMigrationSteps 0 Nothing steps verifyMigration) entries <*
tell (Max (-1)))
case futureEntries of
_:_ -> databaseAheadHook hooks (length futureEntries)
[] -> pure ()
shouldRunMigration <-
flip runContT (\_ -> pure True) $
runMigrationSteps (lastCommit + 1) Nothing steps
(\_ _ step -> do
case migrationDataLoss step of
MigrationLosesData ->
ContT $ \_ -> runIrreversibleHook hooks
MigrationKeepsData ->
executeMigration (\_ -> pure ()) step)
if shouldRunMigration
then Just <$>
runMigrationSteps (lastCommit + 1) Nothing steps
(\stepIx stepName step ->
do startStepHook hooks stepIx stepName
ret <-
executeMigration
(\cmd -> do
runCommandHook hooks stepIx (renderSyntax' cmd)
runNoReturn cmd)
step
runInsert $ insert (_beamMigrateLogEntries (beamMigrateDb @be @m)) $
insertExpressions [ LogEntry (val_ stepIx) (val_ stepName) currentTimestamp_ ]
endStepHook hooks stepIx stepName
return ret)
else pure Nothing
simpleSchema :: Database be db
=> ActionProvider be
-> CheckedDatabaseSettings be db
-> Maybe [BeamSqlBackendSyntax be]
simpleSchema provider settings =
let allChecks = collectChecks settings
solver = heuristicSolver provider [] allChecks
in case finalSolution solver of
Solved cmds -> Just (fmap migrationCommand cmds)
Candidates {} -> Nothing
createSchema :: Database be db
=> BeamMigrationBackend be m
-> CheckedDatabaseSettings be db
-> m ()
createSchema BeamMigrationBackend { backendActionProvider = actions } db =
case simpleSchema actions db of
Nothing -> fail "createSchema: Could not determine schema"
Just cmds ->
mapM_ runNoReturn cmds
autoMigrate :: Database be db
=> BeamMigrationBackend be m
-> CheckedDatabaseSettings be db
-> m ()
autoMigrate BeamMigrationBackend { backendActionProvider = actions
, backendGetDbConstraints = getCs }
db =
do actual <- getCs
let expected = collectChecks db
case finalSolution (heuristicSolver actions actual expected) of
Candidates {} -> fail "autoMigrate: Could not determine migration"
Solved cmds ->
case foldMap migrationCommandDataLossPossible cmds of
MigrationKeepsData -> mapM_ (runNoReturn . migrationCommand) cmds
_ -> fail "autoMigrate: Not performing automatic migration due to data loss"
simpleMigration :: ( MonadBeam be m
, Database be db )
=> (forall a. handle -> m a -> IO a)
-> BeamMigrationBackend be m
-> handle
-> CheckedDatabaseSettings be db
-> IO (Maybe [BeamSqlBackendSyntax be])
simpleMigration runner BeamMigrationBackend { backendGetDbConstraints = getCs
, backendActionProvider = action } hdl db = do
pre <- runner hdl getCs
let post = collectChecks db
solver = heuristicSolver action pre post
case finalSolution solver of
Solved cmds -> pure (Just (fmap migrationCommand cmds))
Candidates {} -> pure Nothing
data VerificationResult
= VerificationSucceeded
| VerificationFailed [SomeDatabasePredicate]
deriving Show
verifySchema :: ( Database be db, MonadBeam be m )
=> BeamMigrationBackend be m
-> CheckedDatabaseSettings be db
-> m VerificationResult
verifySchema BeamMigrationBackend { backendGetDbConstraints = getConstraints } db =
do actualSchema <- HS.fromList <$> getConstraints
let expectedSchema = HS.fromList (collectChecks db)
missingPredicates = expectedSchema `HS.difference` actualSchema
if HS.null missingPredicates
then pure VerificationSucceeded
else pure (VerificationFailed (HS.toList missingPredicates))
runSimpleMigration :: MonadBeam be m
=> (forall a. hdl -> m a -> IO a)
-> hdl -> [BeamSqlBackendSyntax be] -> IO ()
runSimpleMigration runner hdl =
runner hdl . mapM_ runNoReturn
backendMigrationScript :: BeamSqlBackend be
=> (BeamSqlBackendSyntax be -> String)
-> Migration be a
-> String
backendMigrationScript render mig =
migrateScript ((++"\n") . T.unpack) ((++"\n") . render) (migrationStep "Migration Script" (\() -> mig))
haskellSchema :: MonadBeam be m
=> BeamMigrationBackend be m
-> m String
haskellSchema BeamMigrationBackend { backendGetDbConstraints = getCs
, backendConvertToHaskell = HaskellPredicateConverter conv2Hs } = do
constraints <- getCs
let hsConstraints = [ hsConstraint | c <- constraints, Just hsConstraint <- [ conv2Hs c ] ]
solver = heuristicSolver (defaultActionProvider @HsMigrateBackend) [] hsConstraints
case finalSolution solver of
Solved cmds ->
let hsModule = hsActionsToModule "NewBeamSchema" (map migrationCommand cmds)
in case renderHsSchema hsModule of
Left err -> fail ("Error writing Haskell schema: " ++ err)
Right modStr -> pure modStr
Candidates {} -> fail "Could not form Haskell schema"