beam-core-0.10.1.0: test/Database/Beam/Test/Schema.hs
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE DeriveAnyClass #-}
module Database.Beam.Test.Schema
( EmployeeT(..), DepartmentT(..)
, RoleT(..), FunnyT (..)
, EmployeeDb(..)
, PrimaryKey(..)
, DummyBackend
, employeeDbSettings
, tests ) where
import Database.Beam
import Database.Beam.Schema.Tables
import Database.Beam.Backend
import Data.Int
import Data.List.NonEmpty ( NonEmpty((:|)) )
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock (UTCTime)
import Lens.Micro.Extras (view)
import Test.Tasty
import Test.Tasty.HUnit
tests :: TestTree
tests = testGroup "Schema Tests"
[ basicSchemaGeneration
, ruleBasedRenaming
, parametricBeamSchemaGeneration
, parametricAndFixedNestedBeamsAreEquivalent
-- , automaticNestedFieldsAreUnset
-- , nullableForeignKeysGivenMaybeType
, underscoresAreHandledGracefully
, embeddedDatabases ]
data DummyBackend
instance BeamBackend DummyBackend where
-- Pretty much everything we can shove in a database satisfies show
type BackendFromField DummyBackend = Show
data EmployeeT f
= EmployeeT
{ _employeeFirstName :: Columnar f Text
, _employeeLastName :: Columnar f Text
, _employeePhoneNumber :: Columnar f Text
, _employeeAge :: Columnar f Int32
, _employeeSalary :: Columnar f Double
, _employeeHireDate :: Columnar f UTCTime
, _employeeLeaveDate :: Columnar f (Maybe UTCTime)
, _employeeCreated :: Columnar f UTCTime
} deriving Generic
instance Beamable EmployeeT
instance Table EmployeeT where
data PrimaryKey EmployeeT f = EmployeeId (Columnar f Text) (Columnar f Text) (Columnar f UTCTime)
deriving Generic
primaryKey e = EmployeeId (_employeeFirstName e) (_employeeLastName e) (_employeeCreated e)
instance Beamable (PrimaryKey EmployeeT)
deriving instance Show (TableSettings EmployeeT)
deriving instance Eq (TableSettings EmployeeT)
deriving instance (Show (Columnar f Text), Show (Columnar f UTCTime)) => Show (PrimaryKey EmployeeT f)
deriving instance (Eq (Columnar f Text), Eq (Columnar f UTCTime)) => Eq (PrimaryKey EmployeeT f)
-- * Verify that the schema is generated properly
employeeTableSchema :: TableSettings EmployeeT
employeeTableSchema = defTblFieldSettings
expectedEmployeeTableSchema :: TableSettings EmployeeT
expectedEmployeeTableSchema =
EmployeeT { _employeeFirstName = TableField (pure "_employeeFirstName") "first_name"
, _employeeLastName = TableField (pure "_employeeLastName") "last_name"
, _employeePhoneNumber = TableField (pure "_employeePhoneNumber") "phone_number"
, _employeeAge = TableField (pure "_employeeAge") "age"
, _employeeSalary = TableField (pure "_employeeSalary") "salary"
, _employeeHireDate = TableField (pure "_employeeHireDate") "hire_date"
, _employeeLeaveDate = TableField (pure "_employeeLeaveDate") "leave_date"
, _employeeCreated = TableField (pure "_employeeCreated") "created"
}
basicSchemaGeneration :: TestTree
basicSchemaGeneration =
testCase "Basic Schema Generation" $
do _employeeFirstName employeeTableSchema @?= _employeeFirstName expectedEmployeeTableSchema
_employeeLastName employeeTableSchema @?= _employeeLastName expectedEmployeeTableSchema
_employeePhoneNumber employeeTableSchema @?= _employeePhoneNumber expectedEmployeeTableSchema
_employeeAge employeeTableSchema @?= _employeeAge expectedEmployeeTableSchema
_employeeSalary employeeTableSchema @?= _employeeSalary expectedEmployeeTableSchema
_employeeHireDate employeeTableSchema @?= _employeeHireDate expectedEmployeeTableSchema
_employeeLeaveDate employeeTableSchema @?= _employeeLeaveDate expectedEmployeeTableSchema
_employeeCreated employeeTableSchema @?= _employeeCreated expectedEmployeeTableSchema
-- * Ensure that automatic fields are unset when nesting
data RoleT f
= RoleT
{ _roleForEmployee :: PrimaryKey EmployeeT f
, _roleName :: Columnar f Text
, _roleStarted :: Columnar f UTCTime }
deriving Generic
instance Beamable RoleT
instance Table RoleT where
data PrimaryKey RoleT f = RoleId (PrimaryKey EmployeeT f) (Columnar f UTCTime)
deriving Generic
primaryKey (RoleT e _ s) = RoleId e s
instance Beamable (PrimaryKey RoleT)
deriving instance Show (TableSettings (PrimaryKey RoleT))
deriving instance Eq (TableSettings (PrimaryKey RoleT))
-- * Ensure that fields of a nullable primary key are given the proper Maybe type
data DepartmentT f
= DepartmentT
{ _departmentName :: Columnar f Text
, _departmentHead :: PrimaryKey EmployeeT (Nullable f) -- ^ Departments may currently lack a department head
} deriving Generic
instance Beamable DepartmentT
instance Table DepartmentT where
data PrimaryKey DepartmentT f = DepartmentId (Columnar f Text) deriving Generic
primaryKey (DepartmentT name _) = DepartmentId name
instance Beamable (PrimaryKey DepartmentT)
deriving instance Show (TableSettings DepartmentT)
deriving instance Eq (TableSettings DepartmentT)
-- nullableForeignKeysGivenMaybeType :: TestTree
-- nullableForeignKeysGivenMaybeType =
-- testCase "Nullable foreign keys are given maybe type" $
-- do _departmentHead departmentTableSchema @?=
-- EmployeeId (TableField "head__first_name" (DummyField True False (DummyFieldMaybe DummyFieldText)))
-- (TableField "head__last_name" (DummyField True False (DummyFieldMaybe DummyFieldText)))
-- (TableField "head__created" (DummyField True False (DummyFieldMaybe DummyFieldUTCTime)))
-- * Ensure that fields with underscores are handled properly
data FunnyT f
= FunnyT
{ funny_field1 :: Columnar f Text
, funny_field_2 :: Columnar f Text
, funny_first_name :: Columnar f Text
, _funny_lastName :: Columnar f Text
, _funny_middle_Name :: Columnar f Text
, ___ :: Columnar f Int32 }
deriving Generic
instance Beamable FunnyT
instance Table FunnyT where
data PrimaryKey FunnyT f = FunnyId (Columnar f Text) deriving Generic
primaryKey = FunnyId . funny_field1
instance Beamable (PrimaryKey FunnyT)
funnyTableSchema :: TableSettings FunnyT
funnyTableSchema = defTblFieldSettings
underscoresAreHandledGracefully :: TestTree
underscoresAreHandledGracefully =
testCase "Underscores in field names are handled gracefully" $
do let fieldNames = allBeamValues (\(Columnar' f) -> (_fieldPath f, _fieldName f)) funnyTableSchema
fieldNames @?= [ ( pure "funny_field1", "funny_field1")
, ( pure "funny_field_2", "funny_field_2")
, ( pure "funny_first_name", "funny_first_name")
, ( pure "_funny_lastName", "funny_lastName")
, ( pure "_funny_middle_Name", "funny_middle_Name")
, ( pure "___", "___") ]
ruleBasedRenaming :: TestTree
ruleBasedRenaming =
testCase "Rule based renaming works correctly" $
do let (DatabaseEntity (DatabaseTable { dbTableSettings = funny })) = _funny employeeDbSettingsRuleMods
(DatabaseEntity (DatabaseTable { dbTableSettings = departments })) = _departments employeeDbSettingsRuleMods
funnyFieldNames = allBeamValues (\(Columnar' f) -> (_fieldPath f, _fieldName f)) funny
deptFieldNames = allBeamValues (\(Columnar' f) -> (_fieldPath f, _fieldName f)) departments
funnyFieldNames @?= [ ( pure "funny_field1", "pfx_funny_field1")
, ( pure "funny_field_2", "pfx_funny_field_2")
, ( pure "funny_first_name", "pfx_funny_first_name")
, ( pure "_funny_lastName", "pfx_funny_lastName")
, ( pure "_funny_middle_Name", "pfx_funny_middle_Name")
, ( pure "___", "___") ]
deptFieldNames @?= [ (pure "_departmentName", "name")
, ( "_departmentHead" :| [ "_employeeFirstName" ], "head__first_name")
, ( "_departmentHead" :| [ "_employeeLastName" ], "head__last_name")
, ( "_departmentHead" :| [ "_employeeCreated" ], "head__created") ]
-- * Ensure it is possible to use nested paremetric beams
parametricBeamSchemaGeneration :: TestTree
parametricBeamSchemaGeneration =
testCase "Parametric schema generation" $
do let (DatabaseEntity (DatabaseTable { dbTableSettings = deptVehiculesA })) = _departmentVehiculesA employeeDbSettings
deptVehiculesFieldNamesA = allBeamValues (\(Columnar' f) -> (_fieldPath f, _fieldName f)) deptVehiculesA
deptVehiculesFieldNamesA @?= [ ( "_aDepartament" :| [ "_departmentName" ] , "departament__name")
, ( "_aRelatesTo" :| [ "_vehiculeId" ], "relates_to__id")
, ( "_aRelatesTo" :| [ "_vehiculeType" ], "relates_to__type")
, ( "_aRelatesTo" :| [ "_numberOfWheels" ], "relates_to__of_wheels")
, ( "_aMetaInfo" :| [ "_price" ], "meta_info__price")
]
-- * Ensure it doesn't matter whether we abstract over a beam's parameters, or if we them fixed.
parametricAndFixedNestedBeamsAreEquivalent :: TestTree
parametricAndFixedNestedBeamsAreEquivalent =
testCase "Parametric and fixed nested beams are equivalent" $
do let (DatabaseEntity (DatabaseTable { dbTableSettings = deptVehiculesA })) = _departmentVehiculesA employeeDbSettings
(DatabaseEntity (DatabaseTable { dbTableSettings = deptVehiculesB })) = _departmentVehiculesB employeeDbSettings
deptVehiculesFieldNamesA = allBeamValues (\(Columnar' f) -> _fieldName f) deptVehiculesA
deptVehiculesFieldNamesB = allBeamValues (\(Columnar' f) -> _fieldName f) deptVehiculesB
deptVehiculesFieldNamesB @?= deptVehiculesFieldNamesA
-- `ADepartmentVehiculeT` and `BDepartmentVehiculeT` are equivalent, but one was using params while
-- the other had its sub-beams fixed.
type ADepartmentVehiculeT = DepartamentRelatedT VehiculeInformationT VehiculeT
data DepartamentRelatedT metaInfo prop f = DepartamentProperty
{ _aDepartament :: PrimaryKey DepartmentT f
, _aRelatesTo :: prop f -- checking we can nest both, nullable and non-nullable beams.
, _aMetaInfo :: metaInfo (Nullable f)
} deriving Generic
data BDepartmentVehiculeT f = BDepartmentVehicule
{ _bDepartament :: PrimaryKey DepartmentT f
, _bRelatesTo :: VehiculeT f
, _bMetaInfo :: VehiculeInformationT (Nullable f)
} deriving Generic
instance (Beamable metaInfo, Beamable prop) => Beamable (DepartamentRelatedT metaInfo prop)
instance (Table metaInfo, Table prop) => Table (DepartamentRelatedT metaInfo prop) where
data PrimaryKey (DepartamentRelatedT metaInfo prop) f = DepReKeyA (PrimaryKey DepartmentT f)
(PrimaryKey prop f)
deriving(Generic)
primaryKey = DepReKeyA <$> _aDepartament <*> (primaryKey._aRelatesTo)
instance (Table metaInfo, Table prop) => Beamable (PrimaryKey (DepartamentRelatedT metaInfo prop))
data VehiculeT f = VehiculeT
{ _vehiculeId :: C f Text
, _vehiculeType :: C f Text
, _numberOfWheels :: C f Int32
} deriving Generic
instance Beamable VehiculeT
instance Beamable (PrimaryKey VehiculeT)
instance Table VehiculeT where
data PrimaryKey VehiculeT f = VehiculeId (C f Text) deriving(Generic)
primaryKey = VehiculeId <$> _vehiculeId
data VehiculeInformationT f = VehiculeInformationT
{ _price :: C f Double
} deriving Generic
instance Beamable VehiculeInformationT
instance Beamable (PrimaryKey VehiculeInformationT)
instance Table VehiculeInformationT where
data PrimaryKey VehiculeInformationT f = VehiculeInformationKey deriving(Generic)
primaryKey _ = VehiculeInformationKey
instance Beamable BDepartmentVehiculeT
instance Beamable (PrimaryKey BDepartmentVehiculeT)
instance Table BDepartmentVehiculeT where
data PrimaryKey BDepartmentVehiculeT f = DepReKeyB (PrimaryKey DepartmentT f)
(PrimaryKey VehiculeT f)
deriving(Generic)
primaryKey = DepReKeyB <$> _bDepartament <*> (primaryKey._bRelatesTo)
data NoPrimaryKeyT f
= NoPrimaryKeyT
{ _npkField1 :: C f Text
, _npkField2 :: C f Double
} deriving Generic
instance Beamable NoPrimaryKeyT
instance Table NoPrimaryKeyT where
data PrimaryKey NoPrimaryKeyT f = NoPrimaryKey
deriving Generic
primaryKey _ = NoPrimaryKey
instance Beamable (PrimaryKey NoPrimaryKeyT)
-- * Database schema is derived correctly
data EmployeeDb f
= EmployeeDb
{ _employees :: f (TableEntity EmployeeT)
, _departments :: f (TableEntity DepartmentT)
, _roles :: f (TableEntity RoleT)
, _funny :: f (TableEntity FunnyT)
, _departmentVehiculesA :: f (TableEntity ADepartmentVehiculeT)
, _departmentVehiculesB :: f (TableEntity BDepartmentVehiculeT)
, _noPrimaryKey :: f (TableEntity NoPrimaryKeyT)
} deriving Generic
instance Database be EmployeeDb
employeeDbSettings :: DatabaseSettings be EmployeeDb
employeeDbSettings = defaultDbSettings
employeeDbSettingsRuleMods :: DatabaseSettings be EmployeeDb
employeeDbSettingsRuleMods = defaultDbSettings `withDbModification`
renamingFields (\field ->
let defName = defaultFieldName field
in case T.stripPrefix "funny" defName of
Nothing -> defName
Just _ -> "pfx_" <> defName)
data VehicleDb f
= VehicleDb
{ _vdbVehiculesA :: f (TableEntity ADepartmentVehiculeT)
, _vdbVehiculesB :: f (TableEntity BDepartmentVehiculeT)
} deriving Generic
instance Database be VehicleDb
data SuperDb f
= SuperDb
{ _embedEmployeeDb :: EmployeeDb f
, _embedVehicleDb :: VehicleDb f
} deriving Generic
instance Database be SuperDb
superDbSettingsDefault :: DatabaseSettings be SuperDb
superDbSettingsDefault = defaultDbSettings
superDbSettingsCustom :: DatabaseSettings be SuperDb
superDbSettingsCustom = defaultDbSettings `withDbModification` dbModification { _embedVehicleDb = embedDatabase customVehicleDb }
customVehicleDb :: DatabaseSettings be VehicleDb
customVehicleDb = defaultDbSettings `withDbModification` dbModification
{ _vdbVehiculesA = setEntityName "something_random" }
embeddedDatabases :: TestTree
embeddedDatabases =
testGroup "Embedded databases"
[ testCase "Databases can be embedded" $ do
view (dbEntityDescriptor . dbEntityName) (_vdbVehiculesA (_embedVehicleDb superDbSettingsDefault)) @?= "vehicules_a"
view (dbEntityDescriptor . dbEntityName) (_vdbVehiculesB (_embedVehicleDb superDbSettingsDefault)) @?= "vehicules_b"
, testCase "Databases can be customized when embedded" $ do
view (dbEntityDescriptor . dbEntityName) (_vdbVehiculesA (_embedVehicleDb superDbSettingsCustom)) @?= "something_random"
]