packages feed

persistent-2.11.0.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(..)
    , emptyBackendSpecificOverrides
    ) where

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

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

-- | 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.
--
-- @since 2.11
data BackendSpecificOverrides = BackendSpecificOverrides
    { backendSpecificForeignKeyName :: Maybe (DBName -> DBName -> DBName)
    }

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

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, entityUniques t, entityForeigns t)
  where
    cols :: [Column]
    cols = map goId idCol `mappend` map go (entityFields t)

    idCol :: [FieldDef]
    idCol = case entityPrimary t of
        Just _ -> []
        Nothing -> [entityId t]

    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 :: DBName
    tableName = entityDB t


    go :: FieldDef -> Column
    go fd =
        Column
            { cName = fieldDB fd
            , cNull = nullable (fieldAttrs fd) /= NotNullable || entitySum 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 Restrict
            , fcOnDelete = del <|> Just Restrict
            }

    ref :: DBName
        -> ReferenceDef
        -> [FieldAttr]
        -> Maybe (DBName, DBName) -- 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 (DBName x, constraintName)
        FieldAttrConstraint x -> do
            (tableName_, _) <- ref c fe as
            pure (tableName_, DBName x)
        _ -> ref c fe as

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

resolveTableName :: [EntityDef] -> HaskellName -> DBName
resolveTableName [] (HaskellName hn) = error $ "Table not found: " `Data.Monoid.mappend` T.unpack hn
resolveTableName (e:es) hn
    | entityHaskell e == hn = entityDB e
    | otherwise = resolveTableName es hn