Currently, if you define a model without an explicit Id or Primary declaration, then you get a default id field.
This is syntax sugar for:
User
Id (BackendKey SqlBackend) sql="SERIAL PRIMARY KEY"
name Text
age Int
However, a common pattern I've seen is to instead use UUIDs that are automatically generated from the database, or to use UUIDs that are provided by the application.
User
Id UUID default=uuid_generate_v1mc()
name Text
age Int
This Id UUID default=uuid_generate_v1mc() line is repeated for nearly every single model.
Proposal:
Add a field to MkPersistSettings that allows you to specify a custom/implicit Id column.
data MkPersistSettings = MkPersistSettings
{ ...
, mpsImplicitId :: Maybe ImplicitIdColumn
}
data ImplicitIdColumn = ...
The default value for this will be what we have now, so no change in behavior should be observed. This is the function which returns the default ID currently:
-- Database.Persist.Quasi
mkAutoIdField :: PersistSettings -> EntityNameHS -> Maybe FieldNameDB -> SqlType -> FieldDef
mkAutoIdField ps entName idName idSqlType =
FieldDef
{ fieldHaskell = FieldNameHS "Id"
-- this should be modeled as a Maybe
-- but that sucks for non-ID field
-- TODO: use a sumtype FieldDef | IdFieldDef
, fieldDB = fromMaybe (FieldNameDB $ psIdName ps) idName
, fieldType = FTTypeCon Nothing $ keyConName $ unEntityNameHS entName
, fieldSqlType = idSqlType
-- the primary field is actually a reference to the entity
, fieldReference = ForeignRef entName defaultReferenceTypeCon
, fieldAttrs = []
, fieldStrict = True
, fieldComments = Nothing
, fieldCascade = noCascade
, fieldGenerated = Nothing
}
-- Along with the only use for determining the type, in mkEntityDef:
-- | Construct an entity definition.
mkEntityDef :: PersistSettings
-> Text -- ^ name
-> [Attr] -- ^ entity attributes
-> [Line] -- ^ indented lines
-> UnboundEntityDef
mkEntityDef ps name entattribs lines =
UnboundEntityDef foreigns $
EntityDef
{ entityHaskell = EntityNameHS name'
, entityDB = EntityNameDB $ getDbName ps name' entattribs
-- idField is the user-specified Id
-- otherwise useAutoIdField
-- but, adjust it if the user specified a Primary
, entityId = setComposite primaryComposite $ fromMaybe autoIdField idField
-- ... snip ...
autoIdField = mkAutoIdField ps entName (FieldNameDB `fmap` idName) idSqlType
idSqlType = maybe SqlInt64 (const $ SqlOther "Primary Key") primaryComposite
(idField, primaryComposite, uniqs, foreigns) = foldl' (\(mid, mp, us, fs) attr ->
let (i, p, u, f) = takeConstraint ps name' cols attr
squish xs m = xs `mappend` maybeToList m
in (just1 mid i, just1 mp p, squish us u, squish fs f)) (Nothing, Nothing, [],[]) textAttribs
So right now, we do fromMaybe autoIdField, where autoIdField is specified here - specifically, the idSqlType, which is taken to be either SqlInt64 if there isn't an idField or primaryComposite field specified.
In mkAutoIdField, we defer to the EntityId type for the Haskell type of the ID. So, at this phase (with PersistSettings in scope), we can control the SqlType of the default ID. But we don't yet know what that is.
The generation of the fields of the Key record type is here:
-- Database.Persist.TH
keyFields :: MkPersistSettings -> EntityDef -> [(Name, Strict, Type)]
keyFields mps entDef = case entityPrimary entDef of
Just pdef -> map primaryKeyVar (compositeFields pdef)
Nothing -> if defaultIdType entDef
then [idKeyVar backendKeyType]
else [idKeyVar $ ftToType $ fieldType $ entityId entDef]
where
backendKeyType
| mpsGeneric mps = ConT ''BackendKey `AppT` backendT
| otherwise = ConT ''BackendKey `AppT` mpsBackend mps
idKeyVar ft = (unKeyName entDef, notStrict, ft)
primaryKeyVar fieldDef = ( keyFieldName mps entDef fieldDef
, notStrict
, ftToType $ fieldType fieldDef
)
defaultIdType :: EntityDef -> Bool
defaultIdType entDef =
fieldType (entityId entDef) == FTTypeCon Nothing (keyIdText entDef)
So, we check to see if it's the default ID type from the Quasi module - aka, ${EntityName}Id. If it is, then we use backendKeyType, which is a Template Haskell Type. This would correspond with a definition like:
User
Id UserId
name Text
age Int
I wrote a test to verify this, and yes, this does have the same behavior.
(As an aside, man it would be nice if the output from Quasi wasn't the same as the output from Persist.TH - there's a lot of fiddling around with this stuff when a mere separation of types would make a lot of this logic easier)
There's no default attribute set for this - that is the responsibility of the underlying SqlBackend while performing migrations.
data ImplicitIdColumn
I think this could probably just be an abstracted mkAutoIdField in PersistSettings.
-- copied again,
mkAutoIdField :: PersistSettings -> EntityNameHS -> Maybe FieldNameDB -> SqlType -> FieldDef
mkAutoIdField ps entName idName idSqlType =
FieldDef
{ fieldHaskell = FieldNameHS "Id"
-- this should be modeled as a Maybe
-- but that sucks for non-ID field
-- TODO: use a sumtype FieldDef | IdFieldDef
, fieldDB = fromMaybe (FieldNameDB $ psIdName ps) idName
, fieldType = FTTypeCon Nothing $ keyConName $ unEntityNameHS entName
, fieldSqlType = idSqlType
-- the primary field is actually a reference to the entity
, fieldReference = ForeignRef entName defaultReferenceTypeCon
, fieldAttrs = []
, fieldStrict = True
, fieldComments = Nothing
, fieldCascade = noCascade
, fieldGenerated = Nothing
}
We can drop the idName parameter, since it's entirely deprecated:
idName
| Just _ <- attribPrefix "id" =
error "id= is deprecated, ad a field named 'Id' and use sql="
| otherwise =
Nothing
Simplifying the code a bit, we can also drop the SqlType parameter - this is used to conditionally set to SqlType to be SqlOther "Primary Key" when primaryComposite is set.
So, simplifying it, we get:
mkAutoIdField :: PersistSettings -> EntityNameHS -> FieldDef
mkAutoIdField ps entName =
FieldDef
{ fieldHaskell = FieldNameHS "Id"
-- this should be modeled as a Maybe
-- but that sucks for non-ID field
-- TODO: use a sumtype FieldDef | IdFieldDef
, fieldDB = FieldNameDB $ psIdName ps
, fieldType = FTTypeCon Nothing $ keyConName $ unEntityNameHS entName
, fieldSqlType =
SqlInt64
-- the primary field is actually a reference to the entity
, fieldReference = ForeignRef entName defaultReferenceTypeCon
, fieldAttrs = []
, fieldStrict = True
, fieldComments = Nothing
, fieldCascade = noCascade
, fieldGenerated = Nothing
}
We only use PersistSettings to set the fieldDB attribute using the default psIdName ps provided.
So that gives us: newtype ImplicitIdColumn = ImplicitIdColumn (EntityNameHS -> FieldDef) - and a new implementation of mkAutoIdField is:
-- is now a Maybe since you can opt to omit an implicit default id
mkAutoIdField :: PersistSettings -> EntityNameHS -> Maybe FieldDef
mkAutoIdField ps entName = do
ImplicitIdColumn mk <- implicitIdColumn ps
pure $ optionalSetFieldDb $ mk entName
where
optionalSetFieldDb fd
| fieldDB fd == FieldNameDB "__default__" =
fd { fieldDB = FieldNameDB (psIdNAme ps) }
| otherwise =
fd
defaultAutoIdField :: EntityNameHS -> FieldDef
defaultAutoIdField entName =
FieldDef
{ fieldHaskell = FieldNameHS "Id"
-- this should be modeled as a Maybe
-- but that sucks for non-ID field
-- TODO: use a sumtype FieldDef | IdFieldDef
, fieldDB = FieldNameDB $ "__default__"
, fieldType = FTTypeCon Nothing $ keyConName $ unEntityNameHS entName
, fieldSqlType =
SqlInt64
-- the primary field is actually a reference to the entity
, fieldReference = ForeignRef entName defaultReferenceTypeCon
, fieldAttrs = []
, fieldStrict = True
, fieldComments = Nothing
, fieldCascade = noCascade
, fieldGenerated = Nothing
}
So, a function to set it to the Id UUID default=uuid_generate_v1mc() would look like:
lowercaseSettings
{ implicitIdColumn = Just $ ImplicitIdColumn $ \entNameHs ->
(defaultAutoIdField entNameHs)
{ fieldSqlType = SqlOther "UUID"
, fieldAttrs = [DefaultAttr "uuid_generate_v1mc()"]
}
well, ok, maybe it's better to have a function FieldDef -> FieldDef rather than EntityNameHS -> FieldDef,
{ implicitIdColumn = Just (setFieldSqlType (SqlOther "UUID") . setDefaultAttr "uuid_generate_v1mc()")
}
Currently, if you define a model without an explicit
IdorPrimarydeclaration, then you get a defaultidfield.This is syntax sugar for:
However, a common pattern I've seen is to instead use UUIDs that are automatically generated from the database, or to use UUIDs that are provided by the application.
This
Id UUID default=uuid_generate_v1mc()line is repeated for nearly every single model.Proposal:
Add a field to
MkPersistSettingsthat allows you to specify a custom/implicitIdcolumn.The default value for this will be what we have now, so no change in behavior should be observed. This is the function which returns the default ID currently:
So right now, we do
fromMaybe autoIdField, whereautoIdFieldis specified here - specifically, theidSqlType, which is taken to be eitherSqlInt64if there isn't anidFieldorprimaryCompositefield specified.In
mkAutoIdField, we defer to theEntityIdtype for the Haskell type of the ID. So, at this phase (withPersistSettingsin scope), we can control theSqlTypeof the default ID. But we don't yet know what that is.The generation of the fields of the
Key recordtype is here:So, we check to see if it's the default ID type from the Quasi module - aka,
${EntityName}Id. If it is, then we usebackendKeyType, which is aTemplate HaskellType. This would correspond with a definition like:I wrote a test to verify this, and yes, this does have the same behavior.
(As an aside, man it would be nice if the output from
Quasiwasn't the same as the output fromPersist.TH- there's a lot of fiddling around with this stuff when a mere separation of types would make a lot of this logic easier)There's no
defaultattribute set for this - that is the responsibility of the underlying SqlBackend while performing migrations.data ImplicitIdColumnI think this could probably just be an abstracted
mkAutoIdFieldinPersistSettings.We can drop the
idNameparameter, since it's entirely deprecated:Simplifying the code a bit, we can also drop the
SqlTypeparameter - this is used to conditionally set toSqlTypeto beSqlOther "Primary Key"whenprimaryCompositeis set.So, simplifying it, we get:
We only use
PersistSettingsto set thefieldDBattribute using the defaultpsIdName psprovided.psIdNameto be optional or to not override this would be great. Deprecating it entirely in favor of this might be good, too.So that gives us:
newtype ImplicitIdColumn = ImplicitIdColumn (EntityNameHS -> FieldDef)- and a new implementation ofmkAutoIdFieldis:So, a function to set it to the
Id UUID default=uuid_generate_v1mc()would look like:lowercaseSettings { implicitIdColumn = Just $ ImplicitIdColumn $ \entNameHs -> (defaultAutoIdField entNameHs) { fieldSqlType = SqlOther "UUID" , fieldAttrs = [DefaultAttr "uuid_generate_v1mc()"] }well, ok, maybe it's better to have a function
FieldDef -> FieldDefrather thanEntityNameHS -> FieldDef,