orville-postgresql-1.1.0.0: src/Orville/PostgreSQL/PgCatalog/PgConstraint.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{- |
Copyright : Flipstone Technology Partners 2023
License : MIT
Stability : Stable
@since 1.0.0.0
-}
module Orville.PostgreSQL.PgCatalog.PgConstraint
( PgConstraint (..)
, ConstraintType (..)
, ConstraintName
, constraintNameToString
, pgConstraintTable
, constraintRelationOidField
)
where
import qualified Data.Attoparsec.Text as AttoText
import qualified Data.List as List
import qualified Data.String as String
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Builder as LTB
import qualified Database.PostgreSQL.LibPQ as LibPQ
import qualified Orville.PostgreSQL as Orville
import Orville.PostgreSQL.PgCatalog.OidField (oidField, oidTypeField)
import Orville.PostgreSQL.PgCatalog.PgAttribute (AttributeNumber, attributeNumberParser, attributeNumberTextBuilder)
{- | The Haskell representation of data read from the @pg_catalog.pg_constraint@
table. Rows in this table correspond to check, primary key, unique, foreign
key and exclusion constraints on tables.
@since 1.0.0.0
-}
data PgConstraint = PgConstraint
{ pgConstraintOid :: LibPQ.Oid
-- ^ The PostgreSQL @oid@ for the constraint.
, pgConstraintName :: ConstraintName
-- ^ The constraint name (which may not be unique).
, pgConstraintNamespaceOid :: LibPQ.Oid
-- ^ The oid of the namespace that contains the constraint.
, pgConstraintType :: ConstraintType
-- ^ The type of constraint.
, pgConstraintRelationOid :: LibPQ.Oid
-- ^ The PostgreSQL @oid@ of the table that the constraint is on
-- (or @0@ if not a table constraint).
, pgConstraintIndexOid :: LibPQ.Oid
-- ^ The PostgreSQL @oid@ ef the index supporting this constraint, if it's a
-- unique, primary key, foreign key or exclusion constraint. Otherwise @0@.
, pgConstraintKey :: Maybe [AttributeNumber]
-- ^ For table constraints, the attribute numbers of the constrained columns.
-- These correspond to the 'Orville.PostgreSQL.PGCatalog.pgAttributeNumber'
-- field of 'Orville.PostgreSQL.PGCatalog.PgAttribute'.
, pgConstraintForeignRelationOid :: LibPQ.Oid
-- ^ For foreign key constraints, the PostgreSQL @oid@ of the table the
-- foreign key references.
, pgConstraintForeignKey :: Maybe [AttributeNumber]
-- ^ For foreign key constraints, the attribute numbers of the referenced
-- columns. These correspond to the
-- 'Orville.PostgreSQL.PGCatalog.pgAttributeNumber' field of
-- 'Orville.PostgreSQL.PGCatalog.PgAttribute'.
, pgConstraintForeignKeyOnUpdateType :: Maybe Orville.ForeignKeyAction
-- ^ For foreign key constraints, the on update action type.
, pgConstraintForeignKeyOnDeleteType :: Maybe Orville.ForeignKeyAction
-- ^ For foreign key constraints, the on delete action type.
}
{- | A Haskell type for the name of the constraint represented by a
'PgConstraint'.
@since 1.0.0.0
-}
newtype ConstraintName
= ConstraintName T.Text
deriving
( -- | @since 1.0.0.0
Show
, -- | @since 1.0.0.0
Eq
, -- | @since 1.0.0.0
Ord
, -- | @since 1.0.0.0
String.IsString
)
{- | Converts a 'ConstraintName' to a plain 'String'.
@since 1.0.0.0
-}
constraintNameToString :: ConstraintName -> String
constraintNameToString (ConstraintName txt) =
T.unpack txt
{- | The type of constraint that a 'PgConstraint' represents, as described at
https://www.postgresql.org/docs/13/catalog-pg-constraint.html.
@since 1.0.0.0
-}
data ConstraintType
= CheckConstraint
| ForeignKeyConstraint
| PrimaryKeyConstraint
| UniqueConstraint
| ConstraintTrigger
| ExclusionConstraint
deriving
( -- | @since 1.0.0.0
Show
, -- | @since 1.0.0.0
Eq
)
{- | Converts a 'ConstraintType' to the corresponding single character text
representation used by PostgreSQL.
See also 'pgTextToConstraintType'
@since 1.0.0.0
-}
constraintTypeToPgText :: ConstraintType -> T.Text
constraintTypeToPgText conType =
T.pack $
case conType of
CheckConstraint -> "c"
ForeignKeyConstraint -> "f"
PrimaryKeyConstraint -> "p"
UniqueConstraint -> "u"
ConstraintTrigger -> "t"
ExclusionConstraint -> "x"
{- | Attempts to parse a PostgreSQL single character textual value as a
'ConstraintType'
See also 'constraintTypeToPgText'
@since 1.0.0.0
-}
pgTextToConstraintType :: T.Text -> Either String ConstraintType
pgTextToConstraintType text =
case T.unpack text of
"c" -> Right CheckConstraint
"f" -> Right ForeignKeyConstraint
"p" -> Right PrimaryKeyConstraint
"u" -> Right UniqueConstraint
"t" -> Right ConstraintTrigger
"x" -> Right ExclusionConstraint
typ -> Left ("Unrecognized PostgreSQL constraint type: " <> typ)
{- | Converts a 'Maybe Orville.ForeignKeyAction' to the corresponding single character
text representation used by PostgreSQL.
See also 'pgTextToForeignKeyAction'
@since 1.0.0.0
-}
foreignKeyActionToPgText :: Maybe Orville.ForeignKeyAction -> T.Text
foreignKeyActionToPgText mbfkAction =
T.pack $
case mbfkAction of
Just Orville.NoAction -> "a"
Just Orville.Restrict -> "r"
Just Orville.Cascade -> "c"
Just Orville.SetNull -> "n"
Just Orville.SetDefault -> "d"
Nothing -> " "
{- | Attempts to parse a PostgreSQL single character textual value as a
'Maybe Orville.ForeignKeyAction'
See also 'foreignKeyActionToPgText'
@since 1.0.0.0
-}
pgTextToForeignKeyAction :: T.Text -> Either String (Maybe Orville.ForeignKeyAction)
pgTextToForeignKeyAction text =
case T.unpack text of
"a" -> Right $ Just Orville.NoAction
"r" -> Right $ Just Orville.Restrict
"c" -> Right $ Just Orville.Cascade
"n" -> Right $ Just Orville.SetNull
"d" -> Right $ Just Orville.SetDefault
" " -> Right Nothing
typ -> Left ("Unrecognized PostgreSQL foreign key action type: " <> typ)
{- | An Orville 'Orville.TableDefinition' for querying the
@pg_catalog.pg_constraint@ table.
@since 1.0.0.0
-}
pgConstraintTable :: Orville.TableDefinition (Orville.HasKey LibPQ.Oid) PgConstraint PgConstraint
pgConstraintTable =
Orville.setTableSchema "pg_catalog" $
Orville.mkTableDefinition
"pg_constraint"
(Orville.primaryKey oidField)
pgConstraintMarshaller
pgConstraintMarshaller :: Orville.SqlMarshaller PgConstraint PgConstraint
pgConstraintMarshaller =
PgConstraint
<$> Orville.marshallField pgConstraintOid oidField
<*> Orville.marshallField pgConstraintName constraintNameField
<*> Orville.marshallField pgConstraintNamespaceOid constraintNamespaceOidField
<*> Orville.marshallField pgConstraintType constraintTypeField
<*> Orville.marshallField pgConstraintRelationOid constraintRelationOidField
<*> Orville.marshallField pgConstraintIndexOid constraintIndexOidField
<*> Orville.marshallField pgConstraintKey constraintKeyField
<*> Orville.marshallField pgConstraintForeignRelationOid constraintForeignRelationOidField
<*> Orville.marshallField pgConstraintForeignKey constraintForeignKeyField
<*> Orville.marshallField pgConstraintForeignKeyOnUpdateType constraintForeignKeyOnUpdateTypeField
<*> Orville.marshallField pgConstraintForeignKeyOnDeleteType constraintForeignKeyOnDeleteTypeField
{- | The @conname@ column of the @pg_constraint@ table.
@since 1.0.0.0
-}
constraintNameField :: Orville.FieldDefinition Orville.NotNull ConstraintName
constraintNameField =
Orville.coerceField $
Orville.unboundedTextField "conname"
{- | The @connamespace@ column of the @pg_constraint@ table.
@since 1.0.0.0
-}
constraintNamespaceOidField :: Orville.FieldDefinition Orville.NotNull LibPQ.Oid
constraintNamespaceOidField =
oidTypeField "connamespace"
{- | The @contype@ column of the @pg_constraint@ table.
@since 1.0.0.0
-}
constraintTypeField :: Orville.FieldDefinition Orville.NotNull ConstraintType
constraintTypeField =
Orville.convertField
(Orville.tryConvertSqlType constraintTypeToPgText pgTextToConstraintType)
(Orville.unboundedTextField "contype")
{- | The @conrelid@ column of the @pg_constraint@ table.
@since 1.0.0.0
-}
constraintRelationOidField :: Orville.FieldDefinition Orville.NotNull LibPQ.Oid
constraintRelationOidField =
oidTypeField "conrelid"
{- | The @conindid@ column of the @pg_constraint@ table.
@since 1.0.0.0
-}
constraintIndexOidField :: Orville.FieldDefinition Orville.NotNull LibPQ.Oid
constraintIndexOidField =
oidTypeField "conindid"
{- | The @conkey@ column of the @pg_constraint@ table.
@since 1.0.0.0
-}
constraintKeyField :: Orville.FieldDefinition Orville.Nullable (Maybe [AttributeNumber])
constraintKeyField =
Orville.nullableField $
Orville.convertField
(Orville.tryConvertSqlType attributeNumberListToPgArrayText pgArrayTextToAttributeNumberList)
(Orville.unboundedTextField "conkey")
{- | The @confrelid@ column of the @pg_constraint@ table.
@since 1.0.0.0
-}
constraintForeignRelationOidField :: Orville.FieldDefinition Orville.NotNull LibPQ.Oid
constraintForeignRelationOidField =
oidTypeField "confrelid"
{- | The @confkey@ column of the @pg_constraint@ table.
@since 1.0.0.0
-}
constraintForeignKeyField :: Orville.FieldDefinition Orville.Nullable (Maybe [AttributeNumber])
constraintForeignKeyField =
Orville.nullableField $
Orville.convertField
(Orville.tryConvertSqlType attributeNumberListToPgArrayText pgArrayTextToAttributeNumberList)
(Orville.unboundedTextField "confkey")
constraintForeignKeyOnUpdateTypeField :: Orville.FieldDefinition Orville.NotNull (Maybe Orville.ForeignKeyAction)
constraintForeignKeyOnUpdateTypeField =
Orville.convertField
(Orville.tryConvertSqlType foreignKeyActionToPgText pgTextToForeignKeyAction)
(Orville.unboundedTextField "confupdtype")
constraintForeignKeyOnDeleteTypeField :: Orville.FieldDefinition Orville.NotNull (Maybe Orville.ForeignKeyAction)
constraintForeignKeyOnDeleteTypeField =
Orville.convertField
(Orville.tryConvertSqlType foreignKeyActionToPgText pgTextToForeignKeyAction)
(Orville.unboundedTextField "confdeltype")
pgArrayTextToAttributeNumberList :: T.Text -> Either String [AttributeNumber]
pgArrayTextToAttributeNumberList text =
let
parser = do
_ <- AttoText.char '{'
attNums <- AttoText.sepBy attributeNumberParser (AttoText.char ',')
_ <- AttoText.char '}'
AttoText.endOfInput
pure attNums
in
case AttoText.parseOnly parser text of
Left err -> Left ("Unable to decode PostgreSQL Array as AttributeNumber list: " <> err)
Right nums -> Right nums
attributeNumberListToPgArrayText :: [AttributeNumber] -> T.Text
attributeNumberListToPgArrayText attNums =
let
commaDelimitedAttributeNumbers =
mconcat $
List.intersperse (LTB.singleton ',') (map attributeNumberTextBuilder attNums)
in
LT.toStrict . LTB.toLazyText $
LTB.singleton '{' <> commaDelimitedAttributeNumbers <> LTB.singleton '}'