packages feed

orville-postgresql-1.1.0.0: src/Orville/PostgreSQL/PgCatalog/DatabaseDescription.hs

{-# LANGUAGE CPP #-}

{- |
Copyright : Flipstone Technology Partners 2023-2024
License   : MIT
Stability : Stable

@since 1.0.0.0
-}
module Orville.PostgreSQL.PgCatalog.DatabaseDescription
  ( DatabaseDescription (..)
  , RelationDescription (..)
  , ConstraintDescription (..)
  , ForeignRelationDescription (..)
  , IndexDescription (..)
  , IndexMember (..)
  , lookupRelation
  , lookupRelationOfKind
  , lookupAttribute
  , lookupAttributeDefault
  , lookupAttributeComment
  , lookupProcedure
  , lookupExtension
  , describeDatabase
  )
where

#if MIN_VERSION_base(4,18,0)
#else
import Control.Applicative (liftA2)
#endif
import qualified Data.Map.Strict as Map
import qualified Data.Text as T
import qualified Database.PostgreSQL.LibPQ as LibPQ

import qualified Orville.PostgreSQL as Orville
import Orville.PostgreSQL.PgCatalog.OidField (oidField)
import Orville.PostgreSQL.PgCatalog.PgAttribute (AttributeName, AttributeNumber, PgAttribute (pgAttributeName, pgAttributeNumber), attributeIsDroppedField, attributeNumberToInt16, attributeRelationOidField, pgAttributeTable)
import Orville.PostgreSQL.PgCatalog.PgAttributeDefault (PgAttributeDefault (pgAttributeDefaultAttributeNumber), attributeDefaultRelationOidField, pgAttributeDefaultTable)
import Orville.PostgreSQL.PgCatalog.PgClass (PgClass (pgClassNamespaceOid, pgClassOid, pgClassRelationName), RelationKind, RelationName, namespaceOidField, pgClassRelationKind, pgClassTable, relationNameField, relationNameToString)
import Orville.PostgreSQL.PgCatalog.PgConstraint (PgConstraint (pgConstraintForeignKey, pgConstraintForeignRelationOid, pgConstraintKey), constraintRelationOidField, pgConstraintTable)
import Orville.PostgreSQL.PgCatalog.PgDescription (ObjectSubId, PgDescription (pgDescriptionObjSubId), objOidField, objectSubIdFromAttributeNumber, objectSubIdZero, pgDescriptionDescription, pgDescriptionTable)
import Orville.PostgreSQL.PgCatalog.PgExtension (ExtensionName, PgExtension, extensionNameField, pgExtensionTable)
import Orville.PostgreSQL.PgCatalog.PgIndex (PgIndex (pgIndexAttributeNumbers, pgIndexPgClassOid), indexIsLiveField, indexRelationOidField, pgIndexTable)
import Orville.PostgreSQL.PgCatalog.PgNamespace (NamespaceName, PgNamespace (pgNamespaceOid), namespaceNameField, pgNamespaceTable)
import Orville.PostgreSQL.PgCatalog.PgProc (PgProc, ProcName, pgProcTable, procNameField, procNamespaceOidField)
import Orville.PostgreSQL.PgCatalog.PgSequence (PgSequence, pgSequenceTable, sequencePgClassOidField)
import Orville.PostgreSQL.PgCatalog.PgTrigger (PgTrigger, pgTriggerTable, triggerRelationOidField)
import qualified Orville.PostgreSQL.Plan as Plan
import qualified Orville.PostgreSQL.Plan.Many as Many
import qualified Orville.PostgreSQL.Plan.Operation as Op

{- | A description of selected items from a single PostgreSQL database.
  'describeDatabase' can be used to load the descriptions of request
  items.

@since 1.0.0.0
-}
data DatabaseDescription = DatabaseDescription
  { databaseRelations :: Map.Map (NamespaceName, RelationName) RelationDescription
  -- ^ @since 1.0.0.0
  , databaseProcedures :: Map.Map (NamespaceName, ProcName) PgProc
  -- ^ @since 1.1.0.0
  , databaseExtensions :: Map.Map ExtensionName PgExtension
  -- ^ @since 1.1.0.0
  }

{- | Lookup a relation by its qualified name in the @pg_catalog@ schema.

@since 1.0.0.0
-}
lookupRelation ::
  (NamespaceName, RelationName) ->
  DatabaseDescription ->
  Maybe RelationDescription
lookupRelation key =
  Map.lookup key . databaseRelations

{- | Lookup a procedure by its qualified name in the @pg_catalog@ schema.

@since 1.1.0.0
-}
lookupProcedure ::
  (NamespaceName, ProcName) ->
  DatabaseDescription ->
  Maybe PgProc
lookupProcedure key =
  Map.lookup key . databaseProcedures

{- | Lookup an extension by its name in the @pg_catalog@ schema.

@since 1.1.0.0
-}
lookupExtension ::
  ExtensionName ->
  DatabaseDescription ->
  Maybe PgExtension
lookupExtension key =
  Map.lookup key . databaseExtensions

{- | Lookup a relation by its qualified name in the @pg_catalog@ schema. If the
  relation is not of the expected kind, 'Nothing' is returned.

@since 1.0.0.0
-}
lookupRelationOfKind ::
  RelationKind ->
  (NamespaceName, RelationName) ->
  DatabaseDescription ->
  Maybe RelationDescription
lookupRelationOfKind kind key dbDesc =
  case Map.lookup key (databaseRelations dbDesc) of
    Just relation ->
      if pgClassRelationKind (relationRecord relation) == kind
        then Just relation
        else Nothing
    Nothing ->
      Nothing

{- | A description of a particular relation in the PostgreSQL database, including
  the attributes of the relation.

@since 1.0.0.0
-}
data RelationDescription = RelationDescription
  { relationRecord :: PgClass
  -- ^ @since 1.0.0.0
  , relationAttributes :: Map.Map AttributeName PgAttribute
  -- ^ @since 1.0.0.0
  , relationAttributeDefaults :: Map.Map AttributeNumber PgAttributeDefault
  -- ^ @since 1.0.0.0
  , relationConstraints :: [ConstraintDescription]
  -- ^ @since 1.0.0.0
  , relationIndexes :: [IndexDescription]
  -- ^ @since 1.0.0.0
  , relationTriggers :: [PgTrigger]
  -- ^ @since 1.1.0.0
  , relationSequence :: Maybe PgSequence
  -- ^ @since 1.0.0.0
  , relationComment :: Maybe T.Text
  -- ^ @since 1.1.0.0
  , relationAttributeComments :: Map.Map AttributeName T.Text
  -- ^ @since 1.1.0.0
  }

{- | Find an attribute by name from the 'RelationDescription'.

@since 1.0.0.0
-}
lookupAttribute ::
  AttributeName ->
  RelationDescription ->
  Maybe PgAttribute
lookupAttribute key =
  Map.lookup key . relationAttributes

{- | Find an attribute default from the 'RelationDescription'.

@since 1.0.0.0
-}
lookupAttributeDefault ::
  PgAttribute ->
  RelationDescription ->
  Maybe PgAttributeDefault
lookupAttributeDefault attr =
  Map.lookup (pgAttributeNumber attr) . relationAttributeDefaults

{- | Find an attribute comment by name from the 'RelationDescription'

@since 1.1.0.0
-}
lookupAttributeComment ::
  AttributeName ->
  RelationDescription ->
  Maybe T.Text
lookupAttributeComment key =
  Map.lookup key . relationAttributeComments

{- | A description of a particular constraint in the PostgreSQL database, including
  the attributes and relations that it references.

@since 1.0.0.0
-}
data ConstraintDescription = ConstraintDescription
  { constraintRecord :: PgConstraint
  , constraintKey :: Maybe [PgAttribute]
  , constraintForeignRelation :: Maybe ForeignRelationDescription
  , constraintForeignKey :: Maybe [PgAttribute]
  }

{- | A description of a relation in the PostgreSQL database that is referenced by
  a foreign key constraint, including the namespace that the relation belongs to.

@since 1.0.0.0
-}
data ForeignRelationDescription = ForeignRelationDescription
  { foreignRelationClass :: PgClass
  , foreignRelationNamespace :: PgNamespace
  }

{- | A description of an index in the PostgreSQL database, including the names of
  the attributes included in the index and the 'PgClass' record of the index
  itself (NOT the 'PgClass' of the table that the index is for).

@since 1.0.0.0
-}
data IndexDescription = IndexDescription
  { indexRecord :: PgIndex
  , indexPgClass :: PgClass
  , indexMembers :: [IndexMember]
  }

{- | A description of an index member in the PostgreSQL database. If the member
  is a simple attribute, the 'PgAttribute' for that is provided. If it is an
  index over an expression, no further description is currently provided.

@since 1.0.0.0
-}
data IndexMember
  = IndexAttribute PgAttribute
  | IndexExpression

{- | Describes the requested relations in the current database. If any of the
  relations do not exist, they will not have an entry in the returned
  description.

  Each 'RelationDescription' will contain all the attributes that currently
  exist for that relation, according to the @pg_catalog@ tables.

@since 1.1.0.0
-}
describeDatabase ::
  Orville.MonadOrville m =>
  [(NamespaceName, RelationName)] ->
  [(NamespaceName, ProcName)] ->
  [ExtensionName] ->
  m DatabaseDescription
describeDatabase relations procedures extensions = do
  manyRelations <-
    Plan.execute
      (Plan.planMany describeRelationByName)
      relations

  manyProcedures <-
    Plan.execute
      (Plan.planMany describeProcedureByName)
      procedures

  manyExtensions <-
    Plan.execute
      (Plan.planMany describeExtensionByName)
      extensions

  let
    mkMap :: Ord k => Many.Many k (Maybe v) -> Map.Map k v
    mkMap =
      Map.mapMaybe id . Many.toMap

  pure $
    DatabaseDescription
      { databaseRelations = mkMap manyRelations
      , databaseProcedures = mkMap manyProcedures
      , databaseExtensions = mkMap manyExtensions
      }

describeRelationByName :: Plan.Plan scope (NamespaceName, RelationName) (Maybe RelationDescription)
describeRelationByName =
  Plan.bind (fst <$> Plan.askParam) $ \namespaceName ->
    Plan.bind (snd <$> Plan.askParam) $ \relationName ->
      Plan.bind (Plan.using namespaceName findNamespace) $ \namespace ->
        let
          namespaceAndRelName =
            (,) <$> Plan.use namespace <*> Plan.use relationName
        in
          Plan.bind (Plan.chain namespaceAndRelName findRelation) $ \maybePgClass ->
            Plan.using maybePgClass (Plan.planMaybe describeRelationByClass)

describeRelationByClass :: Plan.Plan scope PgClass RelationDescription
describeRelationByClass =
  Plan.bind Plan.askParam $ \pgClass ->
    Plan.bind findClassAttributes $ \attributes ->
      Plan.bind findClassDescriptions $ \descriptions ->
        let
          classAndAttributes =
            mkPgClassAndAttributes
              <$> Plan.use pgClass
              <*> Plan.use attributes
        in
          RelationDescription
            <$> Plan.use pgClass
            <*> Plan.use (fmap (indexBy pgAttributeName) attributes)
            <*> fmap (indexBy pgAttributeDefaultAttributeNumber) findClassAttributeDefaults
            <*> Plan.chain classAndAttributes findClassConstraints
            <*> Plan.chain classAndAttributes findClassIndexes
            <*> Plan.using pgClass findClassTriggers
            <*> Plan.using pgClass findClassSequence
            <*> fmap (Map.lookup objectSubIdZero) (Plan.use descriptions)
            <*> liftA2 mkAttributeCommentMap (Plan.use descriptions) (Plan.use attributes)

findRelation :: Plan.Plan scope (PgNamespace, RelationName) (Maybe PgClass)
findRelation =
  Plan.focusParam (\(ns, relname) -> (pgNamespaceOid ns, relname))
    . Plan.planOperation
    $ Op.findOne pgClassTable byNamespaceOidAndRelationName

byNamespaceOidAndRelationName :: Op.WherePlanner (LibPQ.Oid, RelationName)
byNamespaceOidAndRelationName =
  Op.byFieldTuple namespaceOidField relationNameField

findNamespace :: Plan.Plan scope NamespaceName PgNamespace
findNamespace =
  Plan.findOne pgNamespaceTable namespaceNameField

findClassAttributes :: Plan.Plan scope PgClass [PgAttribute]
findClassAttributes =
  Plan.focusParam pgClassOid $
    Plan.findAllWhere
      pgAttributeTable
      attributeRelationOidField
      (Orville.fieldEquals attributeIsDroppedField False)

findClassAttributeDefaults :: Plan.Plan scope PgClass [PgAttributeDefault]
findClassAttributeDefaults =
  Plan.focusParam pgClassOid $
    Plan.findAll
      pgAttributeDefaultTable
      attributeDefaultRelationOidField

findClassConstraints :: Plan.Plan scope PgClassAndAttributes [ConstraintDescription]
findClassConstraints =
  let
    relationOid =
      pgClassOid . pgClassRecord
  in
    Plan.bind (Plan.focusParam relationOid $ Plan.findAll pgConstraintTable constraintRelationOidField) $ \constraints ->
      Plan.bind Plan.askParam $ \pgClassAndAttrs ->
        Plan.chain
          (zip <$> Plan.use (fmap repeat pgClassAndAttrs) <*> Plan.use constraints)
          (Plan.planList describeConstraint)

describeConstraint :: Plan.Plan scope (PgClassAndAttributes, PgConstraint) ConstraintDescription
describeConstraint =
  let
    prepareAttributeLookups :: (PgClassAndAttributes, PgConstraint) -> Maybe [(PgClassAndAttributes, AttributeNumber)]
    prepareAttributeLookups (pgClassAndAttrs, pgConstraint) =
      case pgConstraintKey pgConstraint of
        Nothing -> Nothing
        Just key -> Just (fmap (\attNum -> (pgClassAndAttrs, attNum)) key)
  in
    Plan.bind (snd <$> Plan.askParam) $ \constraint ->
      Plan.bind (Plan.using constraint findConstraintForeignRelationClass) $ \maybeForeignPgClass ->
        let
          maybeForeignClassAndAttrNums =
            liftA2
              (liftA2 (,))
              (Plan.use maybeForeignPgClass)
              (fmap (pgConstraintForeignKey . snd) Plan.askParam)
        in
          ConstraintDescription
            <$> Plan.use constraint
            <*> Plan.focusParam prepareAttributeLookups (Plan.planMaybe $ Plan.planList findAttributeByNumber)
            <*> Plan.using maybeForeignPgClass (Plan.planMaybe describeForeignRelation)
            <*> Plan.chain maybeForeignClassAndAttrNums (Plan.planMaybe findForeignKeyAttributes)

describeForeignRelation :: Plan.Plan scope PgClass ForeignRelationDescription
describeForeignRelation =
  ForeignRelationDescription
    <$> Plan.askParam
    <*> Plan.focusParam pgClassNamespaceOid (Plan.findOne pgNamespaceTable oidField)

findForeignKeyAttributes :: Plan.Plan scope (PgClass, [AttributeNumber]) [PgAttribute]
findForeignKeyAttributes =
  Plan.bind (fst <$> Plan.askParam) $ \pgClass ->
    Plan.bind (snd <$> Plan.askParam) $ \attrNums ->
      Plan.bind (Plan.focusParam fst findClassAttributes) $ \attributes ->
        let
          attrSource =
            mkPgClassAndAttributes
              <$> Plan.use pgClass
              <*> Plan.use attributes
        in
          Plan.chain
            (zip <$> fmap repeat attrSource <*> Plan.use attrNums)
            (Plan.planList findAttributeByNumber)

findConstraintForeignRelationClass :: Plan.Plan scope PgConstraint (Maybe PgClass)
findConstraintForeignRelationClass =
  let
    relationId constraint =
      case pgConstraintForeignRelationOid constraint of
        LibPQ.Oid 0 -> Nothing
        nonZero -> Just nonZero
  in
    Plan.focusParam relationId
      . Plan.planMaybe
      $ Plan.findOne pgClassTable oidField

findClassIndexes :: Plan.Plan scope PgClassAndAttributes [IndexDescription]
findClassIndexes =
  let
    findIndexes :: Plan.Plan scope PgClassAndAttributes [PgIndex]
    findIndexes =
      Plan.focusParam (pgClassOid . pgClassRecord) $
        Plan.findAllWhere
          pgIndexTable
          indexRelationOidField
          (Orville.fieldEquals indexIsLiveField True)

    indexesWithClassAndAttrs :: Plan.Plan scope PgClassAndAttributes [(PgClassAndAttributes, PgIndex)]
    indexesWithClassAndAttrs =
      zip
        <$> fmap repeat Plan.askParam
        <*> findIndexes
  in
    Plan.chain indexesWithClassAndAttrs (Plan.planList describeIndex)

describeIndex :: Plan.Plan scope (PgClassAndAttributes, PgIndex) IndexDescription
describeIndex =
  let
    expressionsOrAttributeLookups ::
      PgClassAndAttributes ->
      [AttributeNumber] ->
      [Either IndexMember (PgClassAndAttributes, AttributeNumber)]
    expressionsOrAttributeLookups pgClassAndAttrs attNumList = do
      attNum <- attNumList
      pure $
        if attNum == 0
          then Left IndexExpression
          else Right (pgClassAndAttrs, attNum)

    indexMemberLookups ::
      Plan.Plan
        scope
        (PgClassAndAttributes, PgIndex)
        [Either IndexMember (PgClassAndAttributes, AttributeNumber)]
    indexMemberLookups =
      Plan.bind (fst <$> Plan.askParam) $ \pgClassAndAttrs ->
        Plan.bind (pgIndexAttributeNumbers . snd <$> Plan.askParam) $ \attNums ->
          expressionsOrAttributeLookups
            <$> Plan.use pgClassAndAttrs
            <*> Plan.use attNums

    resolveIndexMemberLookup ::
      Plan.Plan
        scope
        (Either IndexMember (PgClassAndAttributes, AttributeNumber))
        IndexMember
    resolveIndexMemberLookup =
      either id IndexAttribute
        <$> Plan.planEither Plan.askParam findAttributeByNumber
  in
    IndexDescription
      <$> fmap snd Plan.askParam
      <*> Plan.focusParam (pgIndexPgClassOid . snd) (Plan.findOne pgClassTable oidField)
      <*> Plan.chain indexMemberLookups (Plan.planList resolveIndexMemberLookup)

data PgClassAndAttributes = PgClassAndAttributes
  { pgClassRecord :: PgClass
  , pgClassAttributes :: Map.Map AttributeNumber PgAttribute
  }

mkPgClassAndAttributes :: PgClass -> [PgAttribute] -> PgClassAndAttributes
mkPgClassAndAttributes pgClass attributes =
  PgClassAndAttributes
    { pgClassRecord = pgClass
    , pgClassAttributes = indexBy pgAttributeNumber attributes
    }

findAttributeByNumber :: Plan.Plan scope (PgClassAndAttributes, AttributeNumber) PgAttribute
findAttributeByNumber =
  let
    lookupAttr (pgClassAndAttrs, attrNum) =
      Map.lookup attrNum (pgClassAttributes pgClassAndAttrs)

    assertFound ::
      (PgClassAndAttributes, AttributeNumber) ->
      Maybe PgAttribute ->
      Either String PgAttribute
    assertFound (pgClassAndAttrs, attrNum) maybeAttr =
      case maybeAttr of
        Nothing ->
          Left $
            "Unable to find attribute number "
              <> show (attributeNumberToInt16 attrNum)
              <> " of relation "
              <> (relationNameToString . pgClassRelationName . pgClassRecord $ pgClassAndAttrs)
        Just attr ->
          Right attr
  in
    Plan.assert assertFound $ fmap lookupAttr Plan.askParam

findClassSequence :: Plan.Plan scope PgClass (Maybe PgSequence)
findClassSequence =
  Plan.focusParam pgClassOid $
    Plan.findMaybeOne pgSequenceTable sequencePgClassOidField

findClassDescriptions :: Plan.Plan scope PgClass (Map.Map ObjectSubId T.Text)
findClassDescriptions =
  let
    mkObjectSubIdMap =
      Map.fromList
        . fmap
          ( \pgDescription ->
              (pgDescriptionObjSubId pgDescription, pgDescriptionDescription pgDescription)
          )
  in
    fmap mkObjectSubIdMap . Plan.focusParam pgClassOid $ Plan.findAll pgDescriptionTable objOidField

mkAttributeCommentMap :: Map.Map ObjectSubId T.Text -> [PgAttribute] -> Map.Map AttributeName T.Text
mkAttributeCommentMap descriptionMap =
  foldMap
    ( \attribute ->
        maybe
          mempty
          (Map.singleton (pgAttributeName attribute))
          ( Map.lookup
              (objectSubIdFromAttributeNumber $ pgAttributeNumber attribute)
              descriptionMap
          )
    )

findClassTriggers :: Plan.Plan scope PgClass [PgTrigger]
findClassTriggers =
  Plan.focusParam pgClassOid $
    Plan.findAll pgTriggerTable triggerRelationOidField

describeProcedureByName :: Plan.Plan scope (NamespaceName, ProcName) (Maybe PgProc)
describeProcedureByName =
  Plan.bind (fst <$> Plan.askParam) $ \namespaceName ->
    Plan.bind (snd <$> Plan.askParam) $ \procName ->
      Plan.bind (Plan.using namespaceName findNamespace) $ \namespace ->
        let
          namespaceAndProcName =
            (,) <$> Plan.use namespace <*> Plan.use procName
        in
          Plan.chain namespaceAndProcName findProc

findProc :: Plan.Plan scope (PgNamespace, ProcName) (Maybe PgProc)
findProc =
  Plan.focusParam (\(ns, procName) -> (pgNamespaceOid ns, procName))
    . Plan.planOperation
    $ Op.findOne pgProcTable byNamespaceOidAndProcName

byNamespaceOidAndProcName :: Op.WherePlanner (LibPQ.Oid, ProcName)
byNamespaceOidAndProcName =
  Op.byFieldTuple procNamespaceOidField procNameField

describeExtensionByName :: Plan.Plan scope ExtensionName (Maybe PgExtension)
describeExtensionByName =
  Plan.findMaybeOne pgExtensionTable extensionNameField

indexBy :: Ord key => (row -> key) -> [row] -> Map.Map key row
indexBy rowKey =
  Map.fromList . fmap (\row -> (rowKey row, row))