packages feed

psql-0.0.0: lib/PostgreSQL/Result/Column.hs

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

-- | Exports of this module are concerned with columns in a Postgres query result. This includes
-- validation of type and format. Parsing of the actual cell values in a column is delegated to
-- "PostgreSQL.Result.Cell".
module PostgreSQL.Result.Column
  ( -- * Column
    Column (..)

    -- ** Basics
  , ignored
  , raw
  , text
  , readable

    -- ** Helpful combinators
  , unchecked
  , validate
  , onlyTextual
  , onlyBinary

    -- * Class
  , AutoColumn (..)

    -- * Helpers
  , Readable (..)
  , RawValue (..)
  )
where

import           Data.ByteString (ByteString)
import           Data.Coerce (coerce)
import           Data.Functor.Alt (Alt (..))
import           Data.Text (Text)
import           Numeric.Natural (Natural)
import qualified PostgreSQL.Result.Cell as Cell
import           PostgreSQL.Types (Format (..), Oid (..), ParserError (..), ParserErrors, Value)

-- | Result column parser
--
-- @since 0.0.0
newtype Column a = Column
  { parseColumn
      :: Oid -- OID of the column type
      -> Format -- Format in which the cells of this column will appear
      -> Either ParserErrors (Cell.Cell a)
  }
  deriving stock Functor -- ^ @since 0.0.0

-- | @since 0.0.0
instance Alt Column where
  Column lhs <!> Column rhs = Column $ \typ format ->
    case (lhs typ format, rhs typ format) of
      (Right lhsParser, Right rhsParser) ->
        -- Both parsers at the column level succeeded. This means we must pass the alternation down
        -- to the cell-level parser.
        Right (lhsParser <!> rhsParser)

      (Left lhsErrors, Left rhsErrors) ->
        -- Both have failed, therefore we must combine the errors.
        Left (lhsErrors <> rhsErrors)

      (lhs, rhs) ->
        -- At this point we know that exactly one parser at the column level has failed.
        lhs <!> rhs

  {-# INLINE (<!>) #-}

-- | Lift a cell parser. This does perform any validation on column type or format.
--
-- @since 0.0.0
unchecked :: Cell.Cell a -> Column a
unchecked parser = Column $ \_ _ -> Right parser

{-# INLINE unchecked #-}

-- | Only allow textual format.
--
-- @since 0.0.0
onlyTextual :: Column a -> Column a
onlyTextual (Column run) = Column $ \oid format ->
  case format of
    Binary -> Left [UnsupportedFormat format]
    Text -> run oid format

{-# INLINE onlyTextual #-}

-- | Only allow binary format.
--
-- @since 0.0.0
onlyBinary :: Column a -> Column a
onlyBinary (Column run) = Column $ \oid format ->
  case format of
    Text -> Left [UnsupportedFormat format]
    Binary -> run oid format

{-# INLINE onlyBinary #-}

-- | Validate the result of a cell parser.
--
-- @since 0.0.0
validate :: Column a -> (a -> Either Text b) -> Column b
validate (Column run) f = Column $ \oid fmt -> do
  parser <- run oid fmt
  pure (Cell.validate parser f)

{-# INLINE validate #-}

-- | Don't parse the column.
--
-- @since 0.0.0
ignored :: Column ()
ignored = unchecked Cell.ignored

{-# INLINE ignored #-}

-- | Raw value. Rejects @NULL@.
--
-- @since 0.0.0
raw :: Column ByteString
raw = unchecked Cell.raw

{-# INLINE raw #-}

-- | Parse as UTF-8 'Text'. See 'Cell.text'.
--
-- @since 0.0.0
text :: Column Text
text = onlyTextual (unchecked Cell.text)

{-# INLINE text #-}

-- | Parse something using its 'Read' instance. Only supports textual format. See 'Cell.readable'.
--
-- @since 0.0.0
readable :: Read a => Column a
readable = onlyTextual (unchecked Cell.readable)

{-# INLINE readable #-}

-- | Default column parser for a type
--
-- @since 0.0.0
class AutoColumn a where
  -- | Default column parser for @a@
  --
  -- @since 0.0.0
  autoColumn :: Column a

-- | @since 0.0.0
instance AutoColumn () where
  autoColumn = ignored

  {-# INLINE autoColumn #-}

-- | @since 0.0.0
instance AutoColumn Int where
  autoColumn = readable

  {-# INLINE autoColumn #-}

-- | @since 0.0.0
instance AutoColumn Word where
  autoColumn = readable

  {-# INLINE autoColumn #-}

-- | @since 0.0.0
instance AutoColumn Integer where
  autoColumn = readable

  {-# INLINE autoColumn #-}

-- | @since 0.0.0
instance AutoColumn Natural where
  autoColumn = readable

  {-# INLINE autoColumn #-}

-- | @since 0.0.0
instance AutoColumn Float where
  autoColumn = readable

  {-# INLINE autoColumn #-}

-- | @since 0.0.0
instance AutoColumn Double where
  autoColumn = readable

  {-# INLINE autoColumn #-}

-- | @since 0.0.0
instance AutoColumn Oid where
  autoColumn = Oid <$> readable

  {-# INLINE autoColumn #-}

-- | @since 0.0.0
instance AutoColumn Text where
  autoColumn = text

  {-# INLINE autoColumn #-}

-- | @since 0.0.0
instance (AutoColumn a, AutoColumn b) => AutoColumn (Either a b) where
  autoColumn = fmap Left autoColumn <!> fmap Right autoColumn

  {-# INLINE autoColumn #-}

-- | Provides a 'AutoColumn' instance using the 'Read' for @a@
--
-- @since 0.0.0
newtype Readable a = Readable a

-- | @since 0.0.0
instance Read a => AutoColumn (Readable a) where
  autoColumn = coerce (readable @a)

  {-# INLINE autoColumn #-}

-- | The raw cell value
--
-- @since 0.0.0
data RawValue = RawValue
  { rawValue_type :: Oid
  , rawValue_format :: Format
  , rawValue_value :: Value
  }
  deriving stock (Show, Eq, Ord)

-- | @since 0.0.0
instance AutoColumn RawValue where
  autoColumn = Column $ \oid format ->
    Right $ Cell.Cell $ Right . RawValue oid format

  {-# INLINE autoColumn #-}