packages feed

persistent-2.18.1.0: Database/Persist/Sql/Internal.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TupleSections #-}

-- | Intended for creating new backends.
module Database.Persist.Sql.Internal
    ( mkColumns
    , defaultAttribute
    , BackendSpecificOverrides (..)
    , getBackendSpecificForeignKeyName
    , setBackendSpecificForeignKeyName
    , getBackendSpecificForeignKeyCascadeDefault
    , setBackendSpecificForeignKeyCascadeDefault
    , emptyBackendSpecificOverrides
    ) where

import Control.Applicative ((<|>))
import Data.Monoid (mappend, mconcat)
import Data.Text (Text)
import qualified Data.Text as T

import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)
import Database.Persist.EntityDef
import Database.Persist.Sql.Types
import Database.Persist.Types

-- | Record of functions to override the default behavior in 'mkColumns'.  It is
-- recommended you initialize this with 'emptyBackendSpecificOverrides' and
-- override the default values, so that as new fields are added, your code still
-- compiles.
--
-- For added safety, use the @getBackendSpecific*@ and @setBackendSpecific*@
-- functions, as a breaking change to the record field labels won't be reflected
-- in a major version bump of the library.
--
-- @since 2.11
data BackendSpecificOverrides = BackendSpecificOverrides
    { backendSpecificForeignKeyName
        :: Maybe (EntityNameDB -> FieldNameDB -> ConstraintNameDB)
    , backendSpecificForeignKeyCascadeDefault :: CascadeAction
    }

-- | If the override is defined, then this returns a function that accepts an
-- entity name and field name and provides the 'ConstraintNameDB' for the
-- foreign key constraint.
--
-- An abstract accessor for the 'BackendSpecificOverrides'
--
-- @since 2.13.0.0
getBackendSpecificForeignKeyName
    :: BackendSpecificOverrides
    -> Maybe (EntityNameDB -> FieldNameDB -> ConstraintNameDB)
getBackendSpecificForeignKeyName =
    backendSpecificForeignKeyName

-- | Set the backend's foreign key generation function to this value.
--
-- @since 2.13.0.0
setBackendSpecificForeignKeyName
    :: (EntityNameDB -> FieldNameDB -> ConstraintNameDB)
    -> BackendSpecificOverrides
    -> BackendSpecificOverrides
setBackendSpecificForeignKeyName func bso =
    bso{backendSpecificForeignKeyName = Just func}

-- | If the override is defined, then this specifies what cascade action
-- should be used if there is none defined for the column.
--
-- @since 2.18.1.0
getBackendSpecificForeignKeyCascadeDefault
    :: BackendSpecificOverrides
    -> CascadeAction
getBackendSpecificForeignKeyCascadeDefault =
    backendSpecificForeignKeyCascadeDefault

-- | Set the backend's default cascade action.
--
-- @since 2.18.1.0
setBackendSpecificForeignKeyCascadeDefault
    :: CascadeAction
    -> BackendSpecificOverrides
    -> BackendSpecificOverrides
setBackendSpecificForeignKeyCascadeDefault action bso =
    bso{backendSpecificForeignKeyCascadeDefault = action}

findMaybe :: (a -> Maybe b) -> [a] -> Maybe b
findMaybe p = listToMaybe . mapMaybe p

-- | Creates an empty 'BackendSpecificOverrides' (i.e. use the default behavior; no overrides)
--
-- @since 2.11
emptyBackendSpecificOverrides :: BackendSpecificOverrides
emptyBackendSpecificOverrides = BackendSpecificOverrides Nothing Restrict

defaultAttribute :: [FieldAttr] -> Maybe Text
defaultAttribute = findMaybe $ \case
    FieldAttrDefault x -> Just x
    _ -> Nothing

-- | Create the list of columns for the given entity.
mkColumns
    :: [EntityDef]
    -> EntityDef
    -> BackendSpecificOverrides
    -> ([Column], [UniqueDef], [ForeignDef])
mkColumns allDefs t overrides =
    (cols, getEntityUniquesNoPrimaryKey t, getEntityForeignDefs t)
  where
    cols :: [Column]
    cols = map goId idCol `mappend` map go (getEntityFieldsDatabase t)

    idCol :: [FieldDef]
    idCol =
        case getEntityId t of
            EntityIdNaturalKey _ ->
                []
            EntityIdField fd ->
                [fd]

    goId :: FieldDef -> Column
    goId fd =
        Column
            { cName = fieldDB fd
            , cNull = False
            , cSqlType = fieldSqlType fd
            , cDefault =
                case defaultAttribute $ fieldAttrs fd of
                    Nothing ->
                        -- So this is not necessarily a problem...
                        -- because you can use eg `inserKey` to insert
                        -- a value into the database without ever asking
                        -- for a default attribute.
                        Nothing
                    -- But we need to be able to say "Hey, if this is
                    -- an *auto generated ID column*, then I need to
                    -- specify that it has the default serial picking
                    -- behavior for whatever SQL backend this is using.
                    -- Because naturally MySQL, Postgres, MSSQL, etc
                    -- all do ths differently, sigh.
                    -- Really, this should be something like,
                    --
                    -- > data ColumnDefault
                    -- >     = Custom Text
                    -- >     | AutogenerateId
                    -- >     | NoDefault
                    --
                    -- where Autogenerated is determined by the
                    -- MkPersistSettings.
                    Just def ->
                        Just def
            , cGenerated = fieldGenerated fd
            , cDefaultConstraintName = Nothing
            , cMaxLen = maxLen $ fieldAttrs fd
            , cReference = mkColumnReference fd
            }

    tableName :: EntityNameDB
    tableName = getEntityDBName t

    go :: FieldDef -> Column
    go fd =
        Column
            { cName = fieldDB fd
            , cNull =
                case isFieldNullable fd of
                    Nullable _ -> True
                    NotNullable -> isFieldMaybe fd || isEntitySum t
            , cSqlType = fieldSqlType fd
            , cDefault = defaultAttribute $ fieldAttrs fd
            , cGenerated = fieldGenerated fd
            , cDefaultConstraintName = Nothing
            , cMaxLen = maxLen $ fieldAttrs fd
            , cReference = mkColumnReference fd
            }

    maxLen :: [FieldAttr] -> Maybe Integer
    maxLen = findMaybe $ \case
        FieldAttrMaxlen n -> Just n
        _ -> Nothing

    refNameFn = fromMaybe refName (backendSpecificForeignKeyName overrides)

    mkColumnReference :: FieldDef -> Maybe ColumnReference
    mkColumnReference fd =
        fmap
            ( \(tName, cName) ->
                ColumnReference tName cName $ overrideNothings $ fieldCascade fd
            )
            $ ref (fieldDB fd) (fieldReference fd) (fieldAttrs fd)

    -- a 'Nothing' in the definition means that the QQ migration doesn't
    -- specify behavior. the default is RESTRICT. setting this here
    -- explicitly makes migrations run smoother.
    overrideNothings (FieldCascade{fcOnUpdate = upd, fcOnDelete = del}) =
        FieldCascade
            { fcOnUpdate = upd <|> Just defaultAction
            , fcOnDelete = del <|> Just defaultAction
            }
      where
        defaultAction = (backendSpecificForeignKeyCascadeDefault overrides)

    ref
        :: FieldNameDB
        -> ReferenceDef
        -> [FieldAttr]
        -> Maybe (EntityNameDB, ConstraintNameDB) -- table name, constraint name
    ref c fe []
        | ForeignRef f <- fe =
            Just (resolveTableName allDefs f, refNameFn tableName c)
        | otherwise = Nothing
    ref _ _ (FieldAttrNoreference : _) = Nothing
    ref c fe (a : as) = case a of
        FieldAttrReference x -> do
            (_, constraintName) <- ref c fe as
            pure (EntityNameDB x, constraintName)
        FieldAttrConstraint x -> do
            (tableName_, _) <- ref c fe as
            pure (tableName_, ConstraintNameDB x)
        _ -> ref c fe as

refName :: EntityNameDB -> FieldNameDB -> ConstraintNameDB
refName (EntityNameDB table) (FieldNameDB column) =
    ConstraintNameDB $ Data.Monoid.mconcat [table, "_", column, "_fkey"]

resolveTableName :: [EntityDef] -> EntityNameHS -> EntityNameDB
resolveTableName [] (EntityNameHS t) = error $ "Table not found: " `Data.Monoid.mappend` T.unpack t
resolveTableName (e : es) hn
    | getEntityHaskellName e == hn = getEntityDBName e
    | otherwise = resolveTableName es hn