{-# LANGUAGE OverloadedStrings #-}

-- | Utility function for describing a table in the database.
module Database.PostgreSQL.Describe (
  ColumnInfo(..), describeTable
  ) where

import Control.Monad
import qualified Data.ByteString as S
import Data.Int
import qualified Data.Vector as V
import qualified Database.PostgreSQL.Simple.TypeInfo.Static as PG
import Database.PostgreSQL.Simple
import Database.PostgreSQL.Simple.TypeInfo
import Database.PostgreSQL.Simple.Types

data ColumnInfo = ColumnInfo {
    colNum :: !Int16
    -- ^ Internal column number used by PostgreSQL.  Generally these
    -- will be consecutive starting from 1, but this may not be the
    -- case if you have altered a table to delete columns.
  , colName :: S.ByteString
    -- ^ Name of the column
  , colType :: !TypeInfo
    -- ^ Type of the column
  , colNotNull :: !Bool
    -- ^ If 'True', the database cannot contain null.  (This
    -- constraint should always be accurate.)
  , colPrimary :: !Bool
    -- ^ 'True' if this column (and only this column) constitutes the
    -- primary key of the table.  Always 'False' if the primary key
    -- comprises multiple columns (even if this is one of those
    -- columns).
  , colUnique :: !Bool
    -- ^ 'True' if there is a uniqueness constraint on this column.
    -- Not 'True' if this column is part of a uniqueness constraint
    -- involving multiple columns.  (Such multi-column uniqueness
    -- constraints are not reported by this interface.)
  , colReferences :: !(Maybe S.ByteString)
    -- ^ If this there is a foreign key constraint on this column (and
    -- the constraint does not span multiple columns), report the
    -- table referenced by this column.
  } deriving (Show)

defColInfo :: ColumnInfo
defColInfo = ColumnInfo {
    colNum = 0
  , colName = S.empty
  , colType = PG.void
  , colNotNull = False
  , colPrimary = False
  , colUnique = False
  , colReferences = Nothing
  }

-- | Returns a list of 'ColumnInfo' structures for a particular table.
-- Not all information about a table is returned.  In particular,
-- constraints that span columns are ignored.
describeTable :: Connection -> S.ByteString -> IO [ColumnInfo]
describeTable cn t = do
  [(Only tbloid)] <- query cn "select oid from pg_class where relname = ?"
                     (Only t)
  cs0 <- query cn "select attnum, attname, atttypid, attnotnull\
                  \ from pg_attribute\
                  \ where attrelid = ? and attisdropped = 'f' and attnum > 0\
                  \ order by attnum"
                  (Only (tbloid :: Oid))
  cs1 <- forM cs0 $ \ (num, name, typ, notnull) -> do
    ti <- getTypeInfo cn typ
    return defColInfo {
      colNum = num, colName = name, colType = ti, colNotNull = notnull
    }
  constraints <- query cn "select contype, conkey, relname\
                          \ from pg_constraint left join pg_class\
                          \ on confrelid = pg_class.oid\
                          \ where conrelid = ?"
                          (Only tbloid)
  let _ = constraints :: [(String, V.Vector Int16, Maybe S.ByteString)]
  return $ map (\c -> foldl appConstr c constraints) cs1
    where appConstr ci (ct, ck, mn)
            | V.length ck == 1, colNum ci == ck V.! 0 = appConstr1 ci ct mn
            | otherwise                           = ci
          appConstr1 ci "p" _ = ci { colPrimary = True }
          appConstr1 ci "u" _ = ci { colUnique = True }
          appConstr1 ci "f" n@(Just _) = ci { colReferences = n }
          appConstr1 ci _ _ = ci