Skip to content

Default Id Field in the Quasiquoter #1230

@parsonsmatt

Description

@parsonsmatt

Currently, if you define a model without an explicit Id or Primary declaration, then you get a default id field.

User
    name Text
    age  Int

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.

  • Modfying psIdName to 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 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()")
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions