packages feed

valiant-cli-0.1.0.0: src/Valiant/CLI/Nullability.hs

module Valiant.CLI.Nullability
  ( resolveNullability
  ) where

import Control.Exception (try)
import Data.ByteString.Char8 qualified as BS8
import PgWire.Connection (Connection, simpleQuery)
import PgWire.Error (PgWireError)
import PgWire.Protocol.Oid (Oid (..))
import Valiant.CLI.Describe (ColumnMeta (..))

-- | For each column, determine whether it is nullable by querying
-- @pg_attribute@. Columns without a source table (computed columns)
-- are assumed nullable.
resolveNullability :: Connection -> [ColumnMeta] -> IO [Bool]
resolveNullability conn = mapM (isNullable conn)

-- | Returns @True@ if the column is nullable.
isNullable :: Connection -> ColumnMeta -> IO Bool
isNullable conn col
  | cmTableOid col == Oid 0 = pure True -- computed / expression column
  | cmColumnNumber col == 0 = pure True -- no source column info
  | otherwise = do
      let Oid tableOid = cmTableOid col
          sql =
            "SELECT NOT attnotnull FROM pg_attribute WHERE attrelid = "
              <> BS8.pack (show tableOid)
              <> " AND attnum = "
              <> BS8.pack (show (cmColumnNumber col))
      result <- try @PgWireError (simpleQuery conn sql)
      pure $ case result of
        Left _ -> True -- assume nullable on error
        Right (rows, _) -> case rows of
          ((Just "t" : _) : _) -> True
          ((Just "f" : _) : _) -> False
          _ -> True -- no info or unexpected value: default to nullable