beam-migrate-0.5.5.0: Database/Beam/Migrate/Checks.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Defines common 'DatabasePredicate's that are shared among backends
module Database.Beam.Migrate.Checks where
import Database.Beam.Backend.SQL (BeamSqlBackendSyntax)
import Database.Beam.Backend.SQL.SQL92
import Database.Beam.Migrate.SQL.SQL92
import Database.Beam.Migrate.SQL.Types
import Database.Beam.Migrate.Serialization
import Database.Beam.Migrate.Types.Predicates
import Data.Aeson ((.:), (.:?), (.=), withObject, object)
import Data.Maybe (fromMaybe)
import Data.Aeson.Types (Parser, Value)
import Data.Hashable (Hashable(..))
import qualified Data.List.NonEmpty as NE (NonEmpty)
import Data.Text (Text)
import Data.Typeable (Typeable, cast)
import GHC.Generics (Generic)
-- * Schema checks
-- | Asserts that a schema with the given name exists in a database
data SchemaExistsPredicate = SchemaExistsPredicate Text {-^ Table name -}
deriving (Show, Eq, Ord, Typeable, Generic)
instance Hashable SchemaExistsPredicate
instance DatabasePredicate SchemaExistsPredicate where
englishDescription (SchemaExistsPredicate s) =
"Schema " <> show s <> " must exist"
serializePredicate (SchemaExistsPredicate s) =
object [ "schema-exists" .= s ]
predicateSpecificity _ = PredicateSpecificityAllBackends
-- * Table checks
-- | Asserts that a table with the given name exists in a database
data TableExistsPredicate = TableExistsPredicate QualifiedName {-^ Table name -}
deriving (Show, Eq, Ord, Typeable, Generic)
instance Hashable TableExistsPredicate
instance DatabasePredicate TableExistsPredicate where
englishDescription (TableExistsPredicate t) =
"Table " <> show t <> " must exist"
serializePredicate (TableExistsPredicate t) =
object [ "table-exists" .= t ]
predicateSpecificity _ = PredicateSpecificityAllBackends
-- | A class that can check whether a particular data type is present
-- in a set of preconditions.
class HasDataTypeCreatedCheck dataType where
dataTypeHasBeenCreated :: dataType -> (forall preCondition. Typeable preCondition => [ preCondition ]) -> Bool
-- | Asserts that the table specified has a column with the given data type. The
-- type paramater @syntax@ should be an instance of 'IsSql92ColumnSchemaSyntax'.
data TableHasColumn be where
TableHasColumn
:: ( HasDataTypeCreatedCheck (BeamMigrateSqlBackendDataTypeSyntax be) )
=> { hasColumn_table :: QualifiedName {-^ Table name -}
, hasColumn_column :: Text {-^ Column name -}
, hasColumn_type :: BeamMigrateSqlBackendDataTypeSyntax be {-^ Data type -}
}
-> TableHasColumn be
instance Hashable (BeamMigrateSqlBackendDataTypeSyntax be) => Hashable (TableHasColumn be) where
hashWithSalt salt (TableHasColumn t c s) = hashWithSalt salt (t, c, s)
instance Eq (BeamMigrateSqlBackendDataTypeSyntax be) => Eq (TableHasColumn be) where
TableHasColumn aTbl aCol aDt == TableHasColumn bTbl bCol bDt =
aTbl == bTbl && aCol == bCol && aDt == bDt
instance ( Typeable be
, BeamMigrateOnlySqlBackend be
, Hashable (BeamMigrateSqlBackendDataTypeSyntax be) ) =>
DatabasePredicate (TableHasColumn be) where
englishDescription (TableHasColumn tbl col type_) =
"Table " <> show tbl <> " must have a column " <> show col <> " of " <> displaySyntax type_
predicateSpecificity _ = PredicateSpecificityAllBackends
serializePredicate (TableHasColumn tbl col type_) =
object [ "has-column" .= object [ "table" .= tbl, "column" .= col
, "type" .= serializeDataType type_ ]]
predicateCascadesDropOn (TableHasColumn tblNm _ _) p'
| Just (TableExistsPredicate tblNm') <- cast p' = tblNm' == tblNm
| otherwise = False
-- | Asserts that a particular column of a table has a given constraint. The
-- @syntax@ type parameter should be an instance of 'IsSql92ColumnSchemaSyntax'
data TableColumnHasConstraint be
= TableColumnHasConstraint
{ hasConstraint_table :: QualifiedName {-^ Table name -}
, hasConstraint_column :: Text {-^ Column name -}
, hasConstraint_defn :: BeamSqlBackendColumnConstraintDefinitionSyntax be {-^ Constraint definition -}
} deriving Generic
instance Hashable (BeamSqlBackendColumnConstraintDefinitionSyntax be) => Hashable (TableColumnHasConstraint be)
deriving instance Eq (BeamSqlBackendColumnConstraintDefinitionSyntax be) => Eq (TableColumnHasConstraint be)
instance ( Typeable be, BeamMigrateOnlySqlBackend be
, Hashable (BeamSqlBackendColumnConstraintDefinitionSyntax be) ) =>
DatabasePredicate (TableColumnHasConstraint be) where
englishDescription (TableColumnHasConstraint tbl col cns) =
"Column " <> show tbl <> "." <> show col <> " has constraint " <> displaySyntax cns
predicateSpecificity _ = PredicateSpecificityAllBackends
serializePredicate (TableColumnHasConstraint tbl col cns) =
object [ "has-column-constraint" .= object [ "table" .= tbl, "column" .= col
, "constraint" .= serializeConstraint cns ] ]
predicateCascadesDropOn (TableColumnHasConstraint tblNm colNm _) p'
| Just (TableExistsPredicate tblNm') <- cast p' = tblNm' == tblNm
| Just (TableHasColumn tblNm' colNm' _ :: TableHasColumn be) <- cast p' = tblNm' == tblNm && colNm' == colNm
| otherwise = False
-- | Asserts that the given table has a secondary index with the given name
-- covering the given columns (in order). Create these predicates with
-- 'Database.Beam.Migrate.Types.CheckedEntities.addTableIndex'.
data TableHasIndex be
= TableHasIndex
{ hasIndex_table :: QualifiedName -- ^ table name
, hasIndex_name :: Text -- ^ index name
, hasIndex_columns :: NE.NonEmpty Text -- ^ ordered column names
, hasIndex_opts :: BeamSqlBackendIndexSyntax be -- ^ index options (e.g. uniqueness)
} deriving Generic
deriving instance Show (BeamSqlBackendIndexSyntax be) => Show (TableHasIndex be)
deriving instance Eq (BeamSqlBackendIndexSyntax be) => Eq (TableHasIndex be)
instance Hashable (BeamSqlBackendIndexSyntax be) => Hashable (TableHasIndex be)
instance ( Typeable be
, IsSql92UniqueIndexSyntax (BeamSqlBackendSyntax be) ) =>
DatabasePredicate (TableHasIndex be) where
englishDescription (TableHasIndex { hasIndex_table = tbl, hasIndex_name = nm
, hasIndex_columns = cols, hasIndex_opts = opts }) =
(if indexIsUnique @(BeamSqlBackendSyntax be) opts then "Unique index " else "Index ") <>
show nm <> " on table " <> show tbl <> " covering columns " <> show cols
predicateSpecificity _ = PredicateSpecificityAllBackends
serializePredicate (TableHasIndex { hasIndex_table = tbl, hasIndex_name = nm
, hasIndex_columns = cols, hasIndex_opts = opts }) =
object [ "has-index" .= object [ "table" .= tbl, "name" .= nm
, "columns" .= cols
, "options" .= serializeIndexOptions @(BeamSqlBackendSyntax be) opts ] ]
predicateCascadesDropOn (TableHasIndex { hasIndex_table = tblNm }) p'
| Just (TableExistsPredicate tblNm') <- cast p' = tblNm' == tblNm
| otherwise = False
-- | Asserts that the given table has a primary key made of the given columns.
-- The order of the columns is significant.
data TableHasPrimaryKey
= TableHasPrimaryKey
{ hasPrimaryKey_table :: QualifiedName {-^ Table name -}
, hasPrimaryKey_cols :: [Text] {-^ Column names -}
} deriving (Show, Eq, Generic)
instance Hashable TableHasPrimaryKey
instance DatabasePredicate TableHasPrimaryKey where
englishDescription (TableHasPrimaryKey tblName colNames) =
"Table " <> show tblName <> " has primary key " <> show colNames
predicateSpecificity _ = PredicateSpecificityAllBackends
serializePredicate (TableHasPrimaryKey tbl cols) =
object [ "has-primary-key" .= object [ "table" .= tbl
, "columns" .= cols ] ]
predicateCascadesDropOn (TableHasPrimaryKey tblNm _) p'
| Just (TableExistsPredicate tblNm') <- cast p' = tblNm' == tblNm
| otherwise = False
-- | Asserts that the given table has a foreign key referencing another table.
-- Create these predicates with
-- 'Database.Beam.Migrate.Types.CheckedEntities.addTableForeignKey'.
data TableHasForeignKey
= TableHasForeignKey
{ hasForeignKey_table :: QualifiedName -- ^ source table
, hasForeignKey_columns :: NE.NonEmpty Text -- ^ local columns (in order)
, hasForeignKey_refTable :: QualifiedName -- ^ referenced table
, hasForeignKey_refColumns :: NE.NonEmpty Text -- ^ referenced columns (in order)
, hasForeignKey_onUpdate :: ForeignKeyAction
, hasForeignKey_onDelete :: ForeignKeyAction
} deriving (Show, Eq, Generic)
instance Hashable TableHasForeignKey
instance DatabasePredicate TableHasForeignKey where
englishDescription (TableHasForeignKey { hasForeignKey_table = tbl
, hasForeignKey_columns = cols
, hasForeignKey_refTable = refTbl
, hasForeignKey_refColumns = refCols
, hasForeignKey_onUpdate = onUpd
, hasForeignKey_onDelete = onDel }) =
"Table " <> show tbl <> " has foreign key on " <> show cols <>
" referencing " <> show refTbl <> " " <> show refCols <>
" ON UPDATE " <> show onUpd <>
" ON DELETE " <> show onDel
predicateSpecificity _ = PredicateSpecificityAllBackends
serializePredicate (TableHasForeignKey { hasForeignKey_table = tbl
, hasForeignKey_columns = cols
, hasForeignKey_refTable = refTbl
, hasForeignKey_refColumns = refCols
, hasForeignKey_onUpdate = onUpd
, hasForeignKey_onDelete = onDel }) =
object [ "has-foreign-key" .= object
[ "table" .= tbl
, "columns" .= cols
, "ref-table" .= refTbl
, "ref-columns" .= refCols
, "on-update" .= serializeForeignKeyAction onUpd
, "on-delete" .= serializeForeignKeyAction onDel
] ]
predicateCascadesDropOn (TableHasForeignKey { hasForeignKey_table = tblNm }) p'
| Just (TableExistsPredicate tblNm') <- cast p' = tblNm' == tblNm
| otherwise = False
serializeForeignKeyAction :: ForeignKeyAction -> Text
serializeForeignKeyAction ForeignKeyActionCascade = "cascade"
serializeForeignKeyAction ForeignKeyActionSetNull = "set-null"
serializeForeignKeyAction ForeignKeyActionSetDefault = "set-default"
serializeForeignKeyAction ForeignKeyActionRestrict = "restrict"
serializeForeignKeyAction ForeignKeyNoAction = "no-action"
deserializeForeignKeyAction :: Text -> Maybe ForeignKeyAction
deserializeForeignKeyAction "cascade" = Just ForeignKeyActionCascade
deserializeForeignKeyAction "set-null" = Just ForeignKeyActionSetNull
deserializeForeignKeyAction "set-default" = Just ForeignKeyActionSetDefault
deserializeForeignKeyAction "restrict" = Just ForeignKeyActionRestrict
deserializeForeignKeyAction "no-action" = Just ForeignKeyNoAction
deserializeForeignKeyAction _ = Nothing
-- * Deserialization
-- | 'BeamDeserializers' for all the predicates defined in this module
beamCheckDeserializers
:: forall be
. ( Typeable be, BeamMigrateOnlySqlBackend be
, HasDataTypeCreatedCheck (BeamMigrateSqlBackendDataTypeSyntax be)
, IsSql92UniqueIndexSyntax (BeamSqlBackendSyntax be) )
=> BeamDeserializers be
beamCheckDeserializers = mconcat
[ beamDeserializer (const deserializeSchemaExistsPredicate)
, beamDeserializer (const deserializeTableExistsPredicate)
, beamDeserializer (const deserializeTableHasPrimaryKeyPredicate)
, beamDeserializer (const deserializeTableHasIndexPredicate)
, beamDeserializer (const deserializeTableHasForeignKeyPredicate)
, beamDeserializer deserializeTableHasColumnPredicate
, beamDeserializer deserializeTableColumnHasConstraintPredicate
]
where
deserializeSchemaExistsPredicate :: Value -> Parser SomeDatabasePredicate
deserializeSchemaExistsPredicate =
withObject "SchemaExistsPredicate" $ \v ->
SomeDatabasePredicate <$> (SchemaExistsPredicate <$> v .: "schema-exists")
deserializeTableExistsPredicate :: Value -> Parser SomeDatabasePredicate
deserializeTableExistsPredicate =
withObject "TableExistPredicate" $ \v ->
SomeDatabasePredicate <$> (TableExistsPredicate <$> v .: "table-exists")
deserializeTableHasPrimaryKeyPredicate :: Value -> Parser SomeDatabasePredicate
deserializeTableHasPrimaryKeyPredicate =
withObject "TableHasPrimaryKey" $ \v ->
v .: "has-primary-key" >>=
(withObject "TableHasPrimaryKey" $ \v' ->
SomeDatabasePredicate <$> (TableHasPrimaryKey <$> v' .: "table" <*> v' .: "columns"))
deserializeTableHasIndexPredicate :: Value -> Parser SomeDatabasePredicate
deserializeTableHasIndexPredicate =
withObject "TableHasIndex" $ \v ->
v .: "has-index" >>=
(withObject "TableHasIndex" $ \v' ->
SomeDatabasePredicate <$>
fmap (id @(TableHasIndex be))
(TableHasIndex <$> v' .: "table" <*> v' .: "name"
<*> v' .: "columns"
<*> (deserializeIndexOptions @(BeamSqlBackendSyntax be) =<< v' .: "options")))
deserializeTableHasForeignKeyPredicate :: Value -> Parser SomeDatabasePredicate
deserializeTableHasForeignKeyPredicate =
withObject "TableHasForeignKey" $ \v ->
v .: "has-foreign-key" >>=
(withObject "TableHasForeignKey" $ \v' ->
SomeDatabasePredicate <$>
(TableHasForeignKey
<$> v' .: "table"
<*> v' .: "columns"
<*> v' .: "ref-table"
<*> v' .: "ref-columns"
<*> (fromMaybe ForeignKeyNoAction . (>>= deserializeForeignKeyAction) <$> v' .:? "on-update")
<*> (fromMaybe ForeignKeyNoAction . (>>= deserializeForeignKeyAction) <$> v' .:? "on-delete")))
deserializeTableHasColumnPredicate :: BeamDeserializers be'
-> Value -> Parser SomeDatabasePredicate
deserializeTableHasColumnPredicate d =
withObject "TableHasColumn" $ \v ->
v .: "has-column" >>=
(withObject "TableHasColumn" $ \v' ->
SomeDatabasePredicate <$>
fmap (id @(TableHasColumn be))
(TableHasColumn <$> v' .: "table" <*> v' .: "column"
<*> (beamDeserialize d =<< v' .: "type")))
deserializeTableColumnHasConstraintPredicate :: BeamDeserializers be'
-> Value -> Parser SomeDatabasePredicate
deserializeTableColumnHasConstraintPredicate d =
withObject "TableColumnHasConstraint" $ \v ->
v .: "has-column-constraint" >>=
(withObject "TableColumnHasConstraint" $ \v' ->
SomeDatabasePredicate <$>
fmap (id @(TableColumnHasConstraint be))
(TableColumnHasConstraint <$> v' .: "table" <*> v' .: "column"
<*> (beamDeserialize d =<< v' .: "constraint")))