packages feed

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

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

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

@since 1.0.0.0
-}
module Orville.PostgreSQL.PgCatalog.PgAttribute
  ( PgAttribute (..)
  , pgAttributeMaxLength
  , AttributeName
  , attributeNameToString
  , AttributeNumber
  , attributeNumberToInt16
  , attributeNumberFromInt16
  , attributeNumberTextBuilder
  , attributeNumberParser
  , isOrdinaryColumn
  , pgAttributeTable
  , attributeRelationOidField
  , attributeNameField
  , attributeTypeOidField
  , attributeLengthField
  , attributeIsDroppedField
  , attributeNumberTypeField
  )
where

import qualified Data.Attoparsec.Text as AttoText
import Data.Int (Int16, Int32)
import qualified Data.String as String
import qualified Data.Text as T
import qualified Data.Text.Lazy.Builder as LTB
import qualified Data.Text.Lazy.Builder.Int as LTBI
import qualified Database.PostgreSQL.LibPQ as LibPQ

import qualified Orville.PostgreSQL as Orville
import qualified Orville.PostgreSQL.Marshall as Marshall
import Orville.PostgreSQL.PgCatalog.OidField (oidTypeField)

{- | The Haskell representation of data read from the @pg_catalog.pg_attribute@
  table. Rows in this table correspond to table columns, but also to attributes
  of other items from the @pg_class@ table.

  See also 'Orville.PostgreSQL.PgCatalog.PgClass'.

@since 1.0.0.0
-}
data PgAttribute = PgAttribute
  { pgAttributeRelationOid :: LibPQ.Oid
  -- ^ The PostgreSQL @oid@ for the relation that this
  -- attribute belongs to. References @pg_class.oid@.
  , pgAttributeName :: AttributeName
  -- ^ The name of the attribute.
  , pgAttributeNumber :: AttributeNumber
  -- ^ The PostgreSQL number of the attribute.
  , pgAttributeTypeOid :: LibPQ.Oid
  -- ^ The PostgreSQL @oid@ for the type of this attribute. References
  -- @pg_type.oid@.
  , pgAttributeLength :: Int16
  -- ^ The length of this attribute\'s type (a copy of @pg_type.typlen@). Note
  -- that this is _NOT_ the maximum length of a @varchar@ column!
  , pgAttributeTypeModifier :: Int32
  -- ^ Type-specific data supplied at creation time, such as the maximum length
  -- of a @varchar@ column.
  , pgAttributeIdentity :: Maybe Marshall.FieldIdentityGeneration
  -- ^ Indicates whether the column is an identity and how it is generated if at all.
  , pgAttributeIsDropped :: Bool
  -- ^ Indicates whether the column has been dropped and is not longer valid.
  , pgAttributeIsNotNull :: Bool
  -- ^ Indicates whether the column has a not-null constraint.
  }

{- | Returns the maximum length for an attribute with a variable length type,
  or 'Nothing' if the length of the type is not variable.

@since 1.0.0.0
-}
pgAttributeMaxLength :: PgAttribute -> Maybe Int32
pgAttributeMaxLength attr =
  -- This function is a port of the following function from _pg_char_max_length
  -- function postgresql:
  --
  -- Note that it does not handle DOMAIN (user created) types correctly at the
  -- moment. Handling domain types would require loading the pg_type record and
  -- checking whether to use the typid and typmod from the attribute or the
  -- base type and typemod from the domain type.
  --
  let
    charTypes =
      [LibPQ.Oid 1042, LibPQ.Oid 1043] -- char, varchar
    bitTypes =
      [LibPQ.Oid 1560, LibPQ.Oid 1562] -- bit, varbit
    typeOid =
      pgAttributeTypeOid attr

    typeMod =
      pgAttributeTypeModifier attr
  in
    if typeMod == -1
      then Nothing
      else
        if typeOid `elem` charTypes
          then Just (typeMod - 4)
          else
            if typeOid `elem` bitTypes
              then Just typeMod
              else Nothing

{- | Determines whether the attribute represents a system column by inspecting
  the attribute\'s 'AttributeNumber'. Ordinary columns have attribute numbers
  starting at 1.

@since 1.0.0.0
-}
isOrdinaryColumn :: PgAttribute -> Bool
isOrdinaryColumn attr =
  pgAttributeNumber attr > AttributeNumber 0

{- | A Haskell type for the name of the attribute represented by a 'PgAttribute'.

@since 1.0.0.0
-}
newtype AttributeName
  = AttributeName 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 an 'AttributeName' to a plain 'String'.

@since 1.0.0.0
-}
attributeNameToString :: AttributeName -> String
attributeNameToString (AttributeName txt) =
  T.unpack txt

{- | A Haskell type for the number of the attribute represented by a 'PgAttribute'.

@since 1.0.0.0
-}
newtype AttributeNumber
  = AttributeNumber Int16
  deriving
    ( -- | @since 1.0.0.0
      Show
    , -- | @since 1.0.0.0
      Eq
    , -- | @since 1.0.0.0
      Ord
    , -- | @since 1.0.0.0
      Enum
    , -- | @since 1.0.0.0
      Num
    , -- | @since 1.0.0.0
      Integral
    , -- | @since 1.0.0.0
      Real
    )

{- | Converts an 'AttributeNumber' to an integer.

@since 1.0.0.0
-}
attributeNumberToInt16 :: AttributeNumber -> Int16
attributeNumberToInt16 (AttributeNumber int) = int

{- | Converts an integer to an 'AttributeNumber'.

@since 1.0.0.0
-}
attributeNumberFromInt16 :: Int16 -> AttributeNumber
attributeNumberFromInt16 = AttributeNumber

{- | Attoparsec parser for 'AttributeNumber'.

@since 1.0.0.0
-}
attributeNumberParser :: AttoText.Parser AttributeNumber
attributeNumberParser =
  AttoText.signed AttoText.decimal

{- | Encodes an 'AttributeNumber' to lazy text as a builder.

@since 1.0.0.0
-}
attributeNumberTextBuilder :: AttributeNumber -> LTB.Builder
attributeNumberTextBuilder =
  LTBI.decimal . attributeNumberToInt16

{- | An Orville 'Orville.TableDefinition' for querying the
  @pg_catalog.pg_attribute@ table.

@since 1.0.0.0
-}
pgAttributeTable :: Orville.TableDefinition Orville.NoKey PgAttribute PgAttribute
pgAttributeTable =
  Orville.setTableSchema "pg_catalog" $
    Orville.mkTableDefinitionWithoutKey
      "pg_attribute"
      pgAttributeMarshaller

pgAttributeMarshaller :: Orville.SqlMarshaller PgAttribute PgAttribute
pgAttributeMarshaller =
  PgAttribute
    <$> Orville.marshallField pgAttributeRelationOid attributeRelationOidField
    <*> Orville.marshallField pgAttributeName attributeNameField
    <*> Orville.marshallField pgAttributeNumber attributeNumberField
    <*> Orville.marshallField pgAttributeTypeOid attributeTypeOidField
    <*> Orville.marshallField pgAttributeLength attributeLengthField
    <*> Orville.marshallField pgAttributeTypeModifier attributeTypeModifierField
    <*> Orville.marshallField pgAttributeIdentity attributeIdentityField
    <*> Orville.marshallField pgAttributeIsDropped attributeIsDroppedField
    <*> Orville.marshallField pgAttributeIsNotNull attributeIsNotNullField

{- | The @attrelid@ column of the @pg_catalog.pg_attribute@ table.

@since 1.0.0.0
-}
attributeRelationOidField :: Orville.FieldDefinition Orville.NotNull LibPQ.Oid
attributeRelationOidField =
  oidTypeField "attrelid"

{- | The @attname@ column of the @pg_catalog.pg_attribute@ table.

@since 1.0.0.0
-}
attributeNameField :: Orville.FieldDefinition Orville.NotNull AttributeName
attributeNameField =
  Orville.coerceField $
    Orville.unboundedTextField "attname"

{- | The @attnum@ column of the @pg_catalog.pg_attribute@ table.

@since 1.0.0.0
-}
attributeNumberField :: Orville.FieldDefinition Orville.NotNull AttributeNumber
attributeNumberField =
  attributeNumberTypeField "attnum"

{- | Builds a 'Orville.FieldDefinition' for a field with type 'AttributeNumber'.

@since 1.0.0.0
-}
attributeNumberTypeField :: String -> Orville.FieldDefinition Orville.NotNull AttributeNumber
attributeNumberTypeField =
  Orville.coerceField . Orville.smallIntegerField

{- | The @atttypid@ column of the @pg_catalog.pg_attribute@ table.

@since 1.0.0.0
-}
attributeTypeOidField :: Orville.FieldDefinition Orville.NotNull LibPQ.Oid
attributeTypeOidField =
  oidTypeField "atttypid"

{- | The @attlen@ column of the @pg_catalog.pg_attribute@ table.

@since 1.0.0.0
-}
attributeLengthField :: Orville.FieldDefinition Orville.NotNull Int16
attributeLengthField =
  Orville.smallIntegerField "attlen"

{- | The @atttypmod@ column of the @pg_catalog.pg_attribute@ table.

@since 1.0.0.0
-}
attributeTypeModifierField :: Orville.FieldDefinition Orville.NotNull Int32
attributeTypeModifierField =
  Orville.integerField "atttypmod"

{- | The @attidentity@ column of the @pg_catalog.pg_attribute@ table.

@since 1.1.0.0
-}
attributeIdentityField ::
  Orville.FieldDefinition
    Orville.NotNull
    (Maybe Marshall.FieldIdentityGeneration)
attributeIdentityField =
  Orville.convertField
    (Orville.tryConvertSqlType columnIdentityToPgText pgTextToColumnIdentity)
    (Orville.fixedTextField "attidentity" 1)

{- | The @attisdropped@ column of the @pg_catalog.pg_attribute@ table.

@since 1.0.0.0
-}
attributeIsDroppedField :: Orville.FieldDefinition Orville.NotNull Bool
attributeIsDroppedField =
  Orville.booleanField "attisdropped"

{- | The @attnotnull@ column of the @pg_catalog.pg_attribute@ table.

@since 1.0.0.0
-}
attributeIsNotNullField :: Orville.FieldDefinition Orville.NotNull Bool
attributeIsNotNullField =
  Orville.booleanField "attnotnull"

{- | Internal, convert a 'T.Text' to a 'Maybe Marshall.FieldIdentityGeneration', allowing for the empty text case
   to be Nothing.

@since 1.1.0.0
-}
pgTextToColumnIdentity :: T.Text -> Either String (Maybe Marshall.FieldIdentityGeneration)
pgTextToColumnIdentity text =
  case T.unpack text of
    "" -> Right Nothing
    "a" -> Right (Just Marshall.GeneratedAlways)
    "d" -> Right (Just Marshall.GeneratedByDefault)
    attid -> Left ("Unrecognized PostgreSQL attribute identity: " <> attid)

{- | Internal, convert a 'Maybe Expr.ColumnIdentity' to a 'T.Text', allowing for the Nothing case to
   be the empty text.

@since 1.1.0.0
-}
columnIdentityToPgText :: Maybe Marshall.FieldIdentityGeneration -> T.Text
columnIdentityToPgText attid =
  T.pack $ case attid of
    Nothing -> ""
    (Just Marshall.GeneratedAlways) -> "a"
    (Just Marshall.GeneratedByDefault) -> "d"