orville-postgresql-1.0.0.0: src/Orville/PostgreSQL/Schema/PrimaryKey.hs
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
{- |
Copyright : Flipstone Technology Partners 2023
License : MIT
Stability : Stable
@since 1.0.0.0
-}
module Orville.PostgreSQL.Schema.PrimaryKey
( PrimaryKey
, primaryKeyDescription
, primaryKeyFieldNames
, primaryKeyToSql
, primaryKey
, PrimaryKeyPart
, compositePrimaryKey
, primaryKeyPart
, mapPrimaryKeyParts
, mkPrimaryKeyExpr
, primaryKeyEquals
, primaryKeyIn
)
where
import qualified Data.List as List
import Data.List.NonEmpty (NonEmpty ((:|)), toList)
import qualified Orville.PostgreSQL.Expr as Expr
import qualified Orville.PostgreSQL.Internal.Extra.NonEmpty as ExtraNonEmpty
import Orville.PostgreSQL.Marshall.FieldDefinition (FieldDefinition, FieldName, NotNull, fieldColumnName, fieldEquals, fieldIn, fieldName, fieldNameToString, fieldValueToSqlValue)
import qualified Orville.PostgreSQL.Raw.SqlValue as SqlValue
{- |
A Haskell description of the 'FieldDefinition's that make up the primary
key of a SQL table. This type supports composite primary keys as well
as singular ones.
@since 1.0.0.0
-}
data PrimaryKey key
= PrimaryKey (PrimaryKeyPart key) [PrimaryKeyPart key]
{- |
A 'PrimaryKeyPart' describes one field of a composite primary key. Values
are built using 'primaryKeyPart' and then used with 'compositePrimaryKey'
to build a 'PrimaryKey'.
@since 1.0.0.0
-}
data PrimaryKeyPart key
= forall part. PrimaryKeyPart (key -> part) (FieldDefinition NotNull part)
{- |
'primaryKeyDescription' builds a user-readable representation of the
primary key for use in error messages and such. It is a comma-delimited
list of the names of the fields that make up the primary key.
@since 1.0.0.0
-}
primaryKeyDescription :: PrimaryKey key -> String
primaryKeyDescription =
List.intercalate ", "
. map fieldNameToString
. toList
. primaryKeyFieldNames
{- |
Retrieves the names of the fields that are part of the primary key.
@since 1.0.0.0
-}
primaryKeyFieldNames :: PrimaryKey key -> NonEmpty FieldName
primaryKeyFieldNames =
let
partName :: (part -> key) -> FieldDefinition NotNull a -> FieldName
partName _ field =
fieldName field
in
mapPrimaryKeyParts partName
{- |
'primaryKeyToSql' converts a Haskell value for a primary key into the
(possibly multiple) SQL values that represent the primary key in the
database.
@since 1.0.0.0
-}
primaryKeyToSql :: PrimaryKey key -> key -> NonEmpty SqlValue.SqlValue
primaryKeyToSql keyDef key =
mapPrimaryKeyParts (partSqlValue key) keyDef
{- |
'partSqlValue' is an internal helper function that builds the
'SqlValue.SqlValue' for one part of a (possible composite) primary key.
@since 1.0.0.0
-}
partSqlValue :: key -> (key -> part) -> FieldDefinition NotNull part -> SqlValue.SqlValue
partSqlValue key getPart partField =
fieldValueToSqlValue partField (getPart key)
{- |
'primaryKey' constructs a single-field primary key from the 'FieldDefinition'
that corresponds to the primary key's column. This is generally used while
building a 'Orville.PostgreSQL.TableDefinition'.
@since 1.0.0.0
-}
primaryKey :: FieldDefinition NotNull key -> PrimaryKey key
primaryKey fieldDef =
PrimaryKey (PrimaryKeyPart id fieldDef) []
{- |
'compositePrimaryKey' constructs a multi-field primary key from the given
parts, each of which corresponds to one field in the primary key. You should
use this while building a 'Orville.PostgreSQL.TableDefinition' for a table
that you want to have a multi-column primary key. See 'primaryKeyPart' for
how to build the parts to be passed as parameters. Note: there is no special
significance to the first argument other than requiring that there is at
least one field in the primary key.
@since 1.0.0.0
-}
compositePrimaryKey ::
PrimaryKeyPart key ->
[PrimaryKeyPart key] ->
PrimaryKey key
compositePrimaryKey =
PrimaryKey
{- |
'primaryKeyPart' constructs a building block for a composite primary key
based on a 'FieldDefinition' and an accessor function to extract the value for
that field from the Haskell @key@ type that represents the overall composite
key. 'PrimaryKeyPart' values built using this function are usually then
passed in a list to 'compositePrimaryKey' to build a 'PrimaryKey'.
@since 1.0.0.0
-}
primaryKeyPart ::
(key -> part) ->
FieldDefinition NotNull part ->
PrimaryKeyPart key
primaryKeyPart =
PrimaryKeyPart
{- |
'mapPrimaryKeyParts' provides a way to access the innards of a 'PrimaryKey'
definition to extract information. The given function will be called on
each part of the primary key in order and the list of results is returned.
Note that single-field and multi-field primary keys are treated the same by
this function, with the single-field case simply behaving as a composite key
with just one part.
@since 1.0.0.0
-}
mapPrimaryKeyParts ::
( forall part.
(key -> part) ->
FieldDefinition NotNull part ->
a
) ->
PrimaryKey key ->
NonEmpty a
mapPrimaryKeyParts f (PrimaryKey first rest) =
let
doPart (PrimaryKeyPart getPart field) =
f getPart field
in
fmap doPart (first :| rest)
{- |
Builds a 'Expr.PrimaryKeyExpr' that is suitable to be used when creating
a table to define the primary key on the table.
@since 1.0.0.0
-}
mkPrimaryKeyExpr :: PrimaryKey key -> Expr.PrimaryKeyExpr
mkPrimaryKeyExpr keyDef =
let
names =
mapPrimaryKeyParts (\_ field -> fieldColumnName field) keyDef
in
Expr.primaryKeyExpr names
{- |
'primaryKeyEquals' builds a 'Expr.BooleanExpr' that will match the row where
the primary key is equal to the given value. For single-field primary keys,
this is equivalent to 'fieldEquals', but 'primaryKeyEquals' also handles
composite primary keys.
@since 1.0.0.0
-}
primaryKeyEquals :: PrimaryKey key -> key -> Expr.BooleanExpr
primaryKeyEquals keyDef key =
ExtraNonEmpty.foldl1'
Expr.andExpr
(mapPrimaryKeyParts (partEquals key) keyDef)
{- |
'primaryKeyIn' builds a 'Expr.BooleanExpr' that will match rows where the
primary key is contained in the given list. For single-field primary keys,
this is equivalent to 'fieldIn', but 'primaryKeyIn' also handles composite
primary keys.
@since 1.0.0.0
-}
primaryKeyIn :: PrimaryKey key -> NonEmpty key -> Expr.BooleanExpr
primaryKeyIn keyDef keys =
case keyDef of
PrimaryKey (PrimaryKeyPart getPart field) [] ->
fieldIn field (fmap getPart keys)
_ ->
ExtraNonEmpty.foldl1'
Expr.orExpr
(fmap (primaryKeyEquals keyDef) keys)
{- |
INTERNAL: builds the where condition for a single part of the key
@since 1.0.0.0
-}
partEquals :: key -> (key -> a) -> FieldDefinition nullability a -> Expr.BooleanExpr
partEquals key getPart partField =
fieldEquals partField (getPart key)