{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE UndecidableInstances #-}
module Database.Beam.Migrate.Actions
(
DatabaseStateSource(..)
, DatabaseState(..)
, PotentialAction(..)
, ActionProvider(..)
, ActionProviderFn
, ensuringNot_
, justOne_
, createTableActionProvider
, dropTableActionProvider
, addColumnProvider
, addColumnNullProvider
, dropColumnNullProvider
, defaultActionProvider
, Solver(..), FinalSolution(..)
, finalSolution
, heuristicSolver
) where
import Database.Beam.Backend.SQL
import Database.Beam.Migrate.Checks
import Database.Beam.Migrate.SQL
import Database.Beam.Migrate.Types
import Database.Beam.Migrate.Types.Predicates (qnameAsText, qnameAsTableName)
import Control.Applicative
import Control.DeepSeq
import Control.Monad
import Control.Parallel.Strategies
import Data.Foldable
import qualified Data.HashMap.Strict as HM
import qualified Data.HashSet as HS
import qualified Data.PQueue.Min as PQ
import qualified Data.Sequence as Seq
import Data.Text (Text)
import qualified Data.Text as T
import Data.Typeable
#if !MIN_VERSION_base(4, 11, 0)
import Data.Semigroup
#endif
import GHC.Generics
data DatabaseStateSource
= DatabaseStateSourceOriginal
| DatabaseStateSourceDerived
deriving (Show, Eq, Ord, Enum, Bounded, Generic)
instance NFData DatabaseStateSource
data DatabaseState be
= DatabaseState
{ dbStateCurrentState :: !(HM.HashMap SomeDatabasePredicate DatabaseStateSource)
, dbStateKey :: !(HS.HashSet SomeDatabasePredicate)
, dbStateCmdSequence :: !(Seq.Seq (MigrationCommand be))
}
deriving instance Show (BeamSqlBackendSyntax be) => Show (DatabaseState be)
instance NFData (DatabaseState cmd) where
rnf d@DatabaseState {..} = d `seq` ()
data MeasuredDatabaseState be
= MeasuredDatabaseState {-# UNPACK #-} !Int {-# UNPACK #-} !Int (DatabaseState be)
deriving Generic
deriving instance Show (BeamSqlBackendSyntax be) => Show (MeasuredDatabaseState be)
instance NFData (MeasuredDatabaseState cmd)
instance Eq (MeasuredDatabaseState cmd) where
a == b = measure a == measure b
instance Ord (MeasuredDatabaseState cmd) where
compare a b = compare (measure a) (measure b)
measure :: MeasuredDatabaseState cmd -> Int
measure (MeasuredDatabaseState cmdLength estGoalDistance _) = cmdLength + 100 * estGoalDistance
measuredDbState :: MeasuredDatabaseState cmd -> DatabaseState cmd
measuredDbState (MeasuredDatabaseState _ _ s) = s
measureDb' :: HS.HashSet SomeDatabasePredicate
-> HS.HashSet SomeDatabasePredicate
-> Int
-> DatabaseState cmd
-> MeasuredDatabaseState cmd
measureDb' _ post cmdLength st@(DatabaseState _ repr _) =
MeasuredDatabaseState cmdLength distToGoal st
where
distToGoal = HS.size ((repr `HS.difference` post) `HS.union`
(post `HS.difference` repr))
data PotentialAction be
= PotentialAction
{ actionPreConditions :: !(HS.HashSet SomeDatabasePredicate)
, actionPostConditions :: !(HS.HashSet SomeDatabasePredicate)
, actionCommands :: !(Seq.Seq (MigrationCommand be))
, actionEnglish :: !Text
, actionScore :: {-# UNPACK #-} !Int
}
instance Semigroup (PotentialAction be) where
(<>) = mappend
instance Monoid (PotentialAction be) where
mempty = PotentialAction mempty mempty mempty "" 0
mappend a b =
PotentialAction (actionPreConditions a <> actionPreConditions b)
(actionPostConditions a <> actionPostConditions b)
(actionCommands a <> actionCommands b)
(if T.null (actionEnglish a) then actionEnglish b
else if T.null (actionEnglish b) then actionEnglish a
else actionEnglish a <> "; " <> actionEnglish b)
(actionScore a + actionScore b)
type ActionProviderFn be =
(forall preCondition. Typeable preCondition => [ preCondition ])
-> (forall postCondition. Typeable postCondition => [ postCondition ])
-> [ PotentialAction be ]
newtype ActionProvider be
= ActionProvider { getPotentialActions :: ActionProviderFn be }
instance Semigroup (ActionProvider be) where
(<>) = mappend
instance Monoid (ActionProvider be) where
mempty = ActionProvider (\_ _ -> [])
mappend (ActionProvider a) (ActionProvider b) =
ActionProvider $ \pre post ->
let aRes = a pre post
bRes = b pre post
in withStrategy (rparWith (parList rseq)) aRes `seq`
withStrategy (rparWith (parList rseq)) bRes `seq`
aRes ++ bRes
createTableWeight, dropTableWeight, addColumnWeight, dropColumnWeight :: Int
createTableWeight = 500
dropTableWeight = 100
addColumnWeight = 1
dropColumnWeight = 1
ensuringNot_ :: Alternative m => [ a ] -> m ()
ensuringNot_ [] = pure ()
ensuringNot_ _ = empty
justOne_ :: [ a ] -> [ a ]
justOne_ [x] = [x]
justOne_ _ = []
createTableActionProvider :: forall be
. ( Typeable be, BeamMigrateOnlySqlBackend be )
=> ActionProvider be
createTableActionProvider =
ActionProvider provider
where
provider :: ActionProviderFn be
provider findPreConditions findPostConditions =
do tblP@(TableExistsPredicate postTblNm) <- findPostConditions
ensuringNot_ $
do TableExistsPredicate preTblNm <- findPreConditions
guard (preTblNm == postTblNm)
(columnsP, columns) <- pure . unzip $
do columnP@
(TableHasColumn tblNm colNm schema :: TableHasColumn be) <-
findPostConditions
guard (tblNm == postTblNm && dataTypeHasBeenCreated schema findPreConditions)
(constraintsP, constraints) <-
pure . unzip $ do
constraintP@
(TableColumnHasConstraint tblNm' colNm' c
:: TableColumnHasConstraint be) <-
findPostConditions
guard (postTblNm == tblNm')
guard (colNm == colNm')
pure (p constraintP, c)
pure (p columnP:constraintsP, (colNm, schema, constraints))
(primaryKeyP, primaryKey) <- justOne_ $ do
primaryKeyP@(TableHasPrimaryKey tblNm primaryKey) <-
findPostConditions
guard (tblNm == postTblNm)
pure (primaryKeyP, primaryKey)
let postConditions = [ p tblP, p primaryKeyP ] ++ concat columnsP
cmd = createTableCmd (createTableSyntax Nothing (qnameAsTableName postTblNm) colsSyntax tblConstraints)
tblConstraints = if null primaryKey then [] else [ primaryKeyConstraintSyntax primaryKey ]
colsSyntax = map (\(colNm, type_, cs) -> (colNm, columnSchemaSyntax type_ Nothing cs Nothing)) columns
pure (PotentialAction mempty (HS.fromList postConditions)
(Seq.singleton (MigrationCommand cmd MigrationKeepsData))
("Create the table " <> qnameAsText postTblNm) createTableWeight)
dropTableActionProvider :: forall be
. BeamMigrateOnlySqlBackend be
=> ActionProvider be
dropTableActionProvider =
ActionProvider provider
where
provider :: ActionProviderFn be
provider findPreConditions findPostConditions =
do tblP@(TableExistsPredicate preTblNm) <- findPreConditions
ensuringNot_ $
do TableExistsPredicate postTblNm <- findPostConditions
guard (preTblNm == postTblNm)
relatedPreds <-
pure $ do p'@(SomeDatabasePredicate pred') <- findPreConditions
guard (pred' `predicateCascadesDropOn` tblP)
pure p'
let cmd = dropTableCmd (dropTableSyntax (qnameAsTableName preTblNm))
pure (
PotentialAction (HS.fromList (SomeDatabasePredicate tblP:relatedPreds)) mempty
(Seq.singleton (MigrationCommand cmd MigrationLosesData))
("Drop table " <> qnameAsText preTblNm) dropTableWeight)
addColumnProvider :: forall be
. ( Typeable be, BeamMigrateOnlySqlBackend be )
=> ActionProvider be
addColumnProvider =
ActionProvider provider
where
provider :: ActionProviderFn be
provider findPreConditions findPostConditions =
do colP@(TableHasColumn tblNm colNm colType :: TableHasColumn be)
<- findPostConditions
TableExistsPredicate tblNm' <- findPreConditions
guard (tblNm' == tblNm && dataTypeHasBeenCreated colType findPreConditions)
ensuringNot_ $ do
TableHasColumn tblNm'' colNm' _ :: TableHasColumn be <-
findPreConditions
guard (tblNm'' == tblNm && colNm == colNm')
(constraintsP, constraints) <-
pure . unzip $ do
constraintP@
(TableColumnHasConstraint tblNm'' colNm' c
:: TableColumnHasConstraint be) <-
findPostConditions
guard (tblNm == tblNm'')
guard (colNm == colNm')
pure (p constraintP, c)
let cmd = alterTableCmd (alterTableSyntax (qnameAsTableName tblNm) (addColumnSyntax colNm schema))
schema = columnSchemaSyntax colType Nothing constraints Nothing
pure (PotentialAction mempty (HS.fromList ([SomeDatabasePredicate colP] ++ constraintsP))
(Seq.singleton (MigrationCommand cmd MigrationKeepsData))
("Add column " <> colNm <> " to " <> qnameAsText tblNm)
(addColumnWeight + fromIntegral (T.length (qnameAsText tblNm) + T.length colNm)))
dropColumnProvider :: forall be
. ( Typeable be, BeamMigrateOnlySqlBackend be )
=> ActionProvider be
dropColumnProvider = ActionProvider provider
where
provider :: ActionProviderFn be
provider findPreConditions _ =
do colP@(TableHasColumn tblNm colNm _ :: TableHasColumn be)
<- findPreConditions
relatedPreds <-
pure $ do p'@(SomeDatabasePredicate pred') <- findPreConditions
guard (pred' `predicateCascadesDropOn` colP)
pure p'
let cmd = alterTableCmd (alterTableSyntax (qnameAsTableName tblNm) (dropColumnSyntax colNm))
pure (PotentialAction (HS.fromList (SomeDatabasePredicate colP:relatedPreds)) mempty
(Seq.singleton (MigrationCommand cmd MigrationLosesData))
("Drop column " <> colNm <> " from " <> qnameAsText tblNm)
(dropColumnWeight + fromIntegral (T.length (qnameAsText tblNm) + T.length colNm)))
addColumnNullProvider :: forall be
. ( Typeable be, BeamMigrateOnlySqlBackend be )
=> ActionProvider be
addColumnNullProvider = ActionProvider provider
where
provider :: ActionProviderFn be
provider findPreConditions findPostConditions =
do colP@(TableColumnHasConstraint tblNm colNm _ :: TableColumnHasConstraint be)
<- findPostConditions
TableExistsPredicate tblNm' <- findPreConditions
guard (tblNm == tblNm')
TableHasColumn tblNm'' colNm' _ :: TableHasColumn be <-
findPreConditions
guard (tblNm == tblNm'' && colNm == colNm')
let cmd = alterTableCmd (alterTableSyntax (qnameAsTableName tblNm) (alterColumnSyntax colNm setNotNullSyntax))
pure (PotentialAction mempty (HS.fromList [SomeDatabasePredicate colP])
(Seq.singleton (MigrationCommand cmd MigrationKeepsData))
("Add not null constraint to " <> colNm <> " on " <> qnameAsText tblNm) 100)
dropColumnNullProvider :: forall be
. ( Typeable be, BeamMigrateOnlySqlBackend be )
=> ActionProvider be
dropColumnNullProvider = ActionProvider provider
where
provider :: ActionProviderFn be
provider findPreConditions _ =
do colP@(TableColumnHasConstraint tblNm colNm _ :: TableColumnHasConstraint be)
<- findPreConditions
TableExistsPredicate tblNm' <- findPreConditions
guard (tblNm == tblNm')
TableHasColumn tblNm'' colNm' _ :: TableHasColumn be <-
findPreConditions
guard (tblNm == tblNm'' && colNm == colNm')
let cmd = alterTableCmd (alterTableSyntax (qnameAsTableName tblNm) (alterColumnSyntax colNm setNullSyntax))
pure (PotentialAction (HS.fromList [SomeDatabasePredicate colP]) mempty
(Seq.singleton (MigrationCommand cmd MigrationKeepsData))
("Drop not null constraint for " <> colNm <> " on " <> qnameAsText tblNm) 100)
defaultActionProvider :: ( Typeable be
, BeamMigrateOnlySqlBackend be )
=> ActionProvider be
defaultActionProvider =
mconcat
[ createTableActionProvider
, dropTableActionProvider
, addColumnProvider
, dropColumnProvider
, addColumnNullProvider
, dropColumnNullProvider ]
data Solver cmd where
ProvideSolution :: [ MigrationCommand cmd ] -> Solver cmd
SearchFailed :: [ DatabaseState cmd ] -> Solver cmd
ChooseActions :: { choosingActionsAtState :: !(DatabaseState cmd)
, getPotentialActionChoice :: f -> PotentialAction cmd
, potentialActionChoices :: [ f ]
, continueSearch :: [ f ] -> Solver cmd
} -> Solver cmd
data FinalSolution be
= Solved [ MigrationCommand be ]
| Candidates [ DatabaseState be ]
deriving instance Show (BeamSqlBackendSyntax be) => Show (FinalSolution be)
solvedState :: HS.HashSet SomeDatabasePredicate -> DatabaseState be -> Bool
solvedState goal (DatabaseState _ cur _) = goal == cur
finalSolution :: Solver be -> FinalSolution be
finalSolution (SearchFailed sts) = Candidates sts
finalSolution (ProvideSolution cmds) = Solved cmds
finalSolution (ChooseActions _ _ actions next) =
finalSolution (next actions)
{-# INLINE heuristicSolver #-}
heuristicSolver :: ActionProvider be
-> [ SomeDatabasePredicate ]
-> [ SomeDatabasePredicate ]
-> Solver be
heuristicSolver provider preConditionsL postConditionsL =
heuristicSolver' initQueue mempty PQ.empty
where
rejectedCount = 10
postConditions = HS.fromList postConditionsL
preConditions = HS.fromList preConditionsL
allToFalsify = preConditions `HS.difference` postConditions
measureDb = measureDb' allToFalsify postConditions
initQueue = PQ.singleton (measureDb 0 initDbState)
initDbState = DatabaseState (DatabaseStateSourceOriginal <$ HS.toMap preConditions)
preConditions
mempty
findPredicate :: forall predicate. Typeable predicate
=> SomeDatabasePredicate
-> [ predicate ] -> [ predicate ]
findPredicate
| Just (Refl :: predicate :~: SomeDatabasePredicate) <- eqT =
(:)
| otherwise =
\(SomeDatabasePredicate pred') ps ->
maybe ps (:ps) (cast pred')
findPredicates :: forall predicate f. (Typeable predicate, Foldable f)
=> f SomeDatabasePredicate -> [ predicate ]
findPredicates = foldr findPredicate []
heuristicSolver' !q !visited !bestRejected =
case PQ.minView q of
Nothing -> SearchFailed (measuredDbState <$> PQ.toList bestRejected)
Just (mdbState@(MeasuredDatabaseState _ _ dbState), q')
| dbStateKey dbState `HS.member` visited -> heuristicSolver' q' visited bestRejected
| solvedState postConditions (measuredDbState mdbState) ->
ProvideSolution (toList (dbStateCmdSequence dbState))
| otherwise ->
let steps = getPotentialActions
provider
(findPredicates (dbStateKey dbState))
(findPredicates postConditionsL)
steps' = filter (not . (`HS.member` visited) . dbStateKey . measuredDbState . snd) $
withStrategy (parList rseq) $
map (\step -> let dbState' = applyStep step mdbState
in dbState' `seq` (step, dbState')) steps
applyStep step (MeasuredDatabaseState score _ dbState') =
let dbState'' = dbStateAfterAction dbState' step
in measureDb (score + 1) dbState''
in case steps' of
[] -> heuristicSolver' q' visited (reject mdbState bestRejected)
_ -> ChooseActions dbState fst steps' $ \chosenSteps ->
let q'' = foldr (\(_, dbState') -> PQ.insert dbState') q' chosenSteps
visited' = HS.insert (dbStateKey dbState) visited
in withStrategy (rparWith rseq) q'' `seq` heuristicSolver' q'' visited' bestRejected
reject :: MeasuredDatabaseState cmd -> PQ.MinQueue (MeasuredDatabaseState cmd)
-> PQ.MinQueue (MeasuredDatabaseState cmd)
reject mdbState q =
let q' = PQ.insert mdbState q
in PQ.fromAscList (PQ.take rejectedCount q')
dbStateAfterAction (DatabaseState curState _ cmds) action =
let curState' = ((curState `HM.difference` HS.toMap (actionPreConditions action))
`HM.union` (DatabaseStateSourceDerived <$ HS.toMap (actionPostConditions action)))
in DatabaseState curState' (HS.fromMap (() <$ curState'))
(cmds <> actionCommands action)