packages feed

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

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UndecidableInstances #-}

-- | Things in this module are used for processing Postgres query result rows.
module PostgreSQL.Result.Row
  ( Row
  , runRow
  , runRowPq

  , ColumnRequest (..)
  , ColumnPosition (..)

    -- * Combinators
  , column
  , columnWith
  , fixedColumn
  , fixedColumnWith
  , namedColumn
  , namedColumnWith

    -- * Class
  , AutoRow (..)
  , genericRow
  , AutoColumnDelegate

    -- * Helpers
  , Fixed (..)
  , Named (..)
  )
where

import           Control.Applicative (liftA2)
import           Control.Monad (when)
import qualified Control.Monad.Except as Except
import           Control.Monad.IO.Class (MonadIO (liftIO))
import qualified Control.Monad.Reader as Reader
import qualified Control.Monad.State.Strict as State
import           Data.Bifunctor (first)
import           Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as Char8
import           Data.Data (Proxy (..))
import           Data.Functor.Apply (Apply (..))
import           Data.Functor.Identity (Identity (..))
import           Data.Void (Void)
import qualified Database.PostgreSQL.LibPQ as PQ
import qualified GHC.Generics as Generics
import           GHC.TypeLits (KnownSymbol, Symbol, symbolVal)
import           GHC.TypeNats (KnownNat, Nat, natVal)
import qualified PostgreSQL.Result.Cell as Cell
import qualified PostgreSQL.Result.Column as Column
import qualified PostgreSQL.Types as Types

-- | Position of a column
--
-- @since 0.0.0
data ColumnPosition
  = FixedColumn Types.ColumnNum
  -- ^ Column is at a fixed index.
  --
  -- @since 0.0.0
  | NamedColumn ByteString
  -- ^ Column has a fixed name.
  --
  -- @since 0.0.0
  deriving stock (Show, Read, Eq, Ord)

-- | Request a column
--
-- @since 0.0.0
data ColumnRequest a = ColumnReqest -- ^ @since 0.0.0
  { columnRequest_position :: ColumnPosition
  -- ^ Location of the column
  --
  -- @since 0.0.0
  , columnRequest_parser :: Column.Column a
  -- ^ Parser for the column
  --
  -- @since 0.0.0
  }
  deriving stock Functor

-- | Result row parser
--
-- @since 0.0.0
newtype Row a = Row
  { _unRow
      :: forall m row
      .  (Monad m, Applicative row)
      => (forall x. ColumnRequest x -> m (row x))
      -> State.StateT Types.ColumnNum m (row a)
  }

-- | @since 0.0.0
instance Functor Row where
  fmap f (Row run) = Row (\liftRequest -> fmap f <$> run liftRequest)

  {-# INLINE fmap #-}

-- | @since 0.0.0
instance Applicative Row where
  pure x = Row $ \_liftRequest -> pure $ pure x

  {-# INLINE pure #-}

  Row f <*> Row x = Row $ \liftRequest -> liftA2 (<*>) (f liftRequest) (x liftRequest)

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

-- | @since 0.0.0
instance Apply Row where
  (<.>) = (<*>)

-- | Translate a 'Row' expression. Validate things in @m@ and parse each row in @row@.
--
-- @since 0.0.0
runRow
  :: (Monad m, Applicative row)
  => Row a
  -> (forall x. ColumnRequest x -> m (row x))
  -> m (row a)
runRow (Row run) liftRequest =
  State.evalStateT (run liftRequest) 0

{-# INLINE runRow #-}

-- | Generate a row runner for libpq\'s 'PQ.Result'.
--
-- @since 0.0.0
runRowPq
  :: (Except.MonadError Types.ProcessorErrors m, MonadIO m)
  => PQ.Result
  -> Row a
  -> m (Types.RowNum -> m a)
runRowPq result row = Reader.runReaderT <$> do
  numCols <- liftIO (PQ.nfields result)

  runRow row $ \req -> do
    col <-
      case columnRequest_position req of
        FixedColumn origCol@(Types.ColumnNum col) -> do
          when (col >= numCols) $
            Except.throwError [Types.NotEnoughColumns origCol (Types.ColumnNum numCols)]

          pure col

        NamedColumn name -> do
          mbCol <- liftIO (PQ.fnumber result name)
          maybe (Except.throwError [Types.MissingNamedColumn name]) pure mbCol

    oid <- liftIO (PQ.ftype result col)
    format <- liftIO (PQ.fformat result col)

    cell <-
      Except.liftEither $ first (fmap (Types.ColumnParserError (Types.ColumnNum col) oid format)) $
        Column.parseColumn (columnRequest_parser req) oid format

    pure $ Reader.ReaderT $ \(Types.RowNum row) -> do
      valueBare <- liftIO (PQ.getvalue' result row col)
      let value = maybe Types.Null Types.Value valueBare
      Except.liftEither
        $ first
            (fmap
              (Types.CellParserError
                (Types.ColumnNum col)
                oid
                format
                (Types.RowNum row)
                value))
        $ Cell.parseCell cell value

{-# INLINE runRowPq #-}

-- | Floating column using the default 'Column.Column' for @a@
--
-- The position of this column is depenend on other floating columns left of it.
--
-- For example:
--
-- > foo = baz <$> column <*> column <*> column
-- > --            ^ A        ^ B        ^ C
--
-- Here, @A@ would be at index 0, @B@ at 1 and @C@ at 2.
-- Other non-floating columns do not impact the column indices.
--
-- @since 0.0.0
column :: Column.AutoColumn a => Row a
column = columnWith Column.autoColumn

{-# INLINE column #-}

-- | Same as 'column' but lets you specify the 'Column.Column'.
--
-- @since 0.0.0
columnWith :: Column.Column a -> Row a
columnWith column = Row $ \liftRequest -> do
  col <- State.state (\col -> (col, col + 1))
  State.lift $ liftRequest ColumnReqest
    { columnRequest_position = FixedColumn col
    , columnRequest_parser = column
    }

{-# INLINE columnWith #-}

-- | Fixed-position column using the default 'Column.Column' for @a@
--
-- @since 0.0.0
fixedColumn :: Column.AutoColumn a => Types.ColumnNum -> Row a
fixedColumn num = fixedColumnWith num Column.autoColumn

{-# INLINE fixedColumn #-}

-- | Same as 'fixedColumn' but lets you specify the 'Column.Column'.
--
-- @since 0.0.0
fixedColumnWith :: Types.ColumnNum -> Column.Column a -> Row a
fixedColumnWith number column = Row $ \liftRequest -> State.lift $
  liftRequest ColumnReqest
    { columnRequest_position = FixedColumn number
    , columnRequest_parser = column
    }

{-# INLINE fixedColumnWith #-}

-- | Named column using the default 'Column.Column' for @a@
--
-- @since 0.0.0
namedColumn :: Column.AutoColumn a => ByteString -> Row a
namedColumn name = namedColumnWith name Column.autoColumn

{-# INLINE namedColumn #-}

-- | Same as 'namedColumn' but lets you specify the 'Column.Column'.
--
-- @since 0.0.0
namedColumnWith :: ByteString -> Column.Column a -> Row a
namedColumnWith name column = Row $ \liftRequest -> State.lift $
  liftRequest ColumnReqest
    { columnRequest_position = NamedColumn name
    , columnRequest_parser = column
    }

{-# INLINE namedColumnWith #-}

-- | Generic row parser
--
-- You can use this with your 'Generics.Generic'-implementing data types.
--
-- > data Foo = Foo
-- >   { bar :: Integer
-- >   , baz :: Text
-- >   }
-- >   deriving Generic
-- >
-- > fooRow :: Row Foo
-- > fooRow = genericRow
--
-- @since 0.0.0
genericRow :: (Generics.Generic a, AutoRow (Generics.Rep a Void)) => Row a
genericRow = Generics.to @_ @Void <$> autoRow

{-# INLINE genericRow #-}

-- | Value for a column at a fixed location
--
-- @since 0.0.0
newtype Fixed (index :: Nat) a = Fixed
  { fromFixed :: a }

-- | Value for a named column
--
-- @since 0.0.0
newtype Named (name :: Symbol) a = Named
  { fromNamed :: a }

-- | This class is used to intercept instance heads like 'Fixed' and 'Named' that have special
-- additional meaning. For most cases it will delegate to 'Column.AutoColumn'.
--
-- Use this class instead of 'Column.AutoColumn' when implementing 'AutoRow' instances.
--
-- @since 0.0.0
class AutoColumnDelegate a where
  autoColumnDelegate :: Row a

-- | Uses 'fixedColumn' with @index@ to construct the 'Row'
--
-- @since 0.0.0
instance (KnownNat index, Column.AutoColumn a) => AutoColumnDelegate (Fixed index a) where
  autoColumnDelegate = Fixed <$> fixedColumn (fromIntegral (natVal @index Proxy))

-- | Uses 'namedColumn' with @name@ to construct the 'Row'
--
-- @since 0.0.0
instance (KnownSymbol name, Column.AutoColumn a) => AutoColumnDelegate (Named name a) where
  autoColumnDelegate = Named <$> namedColumn (Char8.pack (symbolVal @name Proxy))

-- | Passthrough to 'Column.AutoColumn'
--
-- @since 0.0.0
instance {-# OVERLAPPABLE #-} Column.AutoColumn a => AutoColumnDelegate a where
  autoColumnDelegate = column

-- | Default row parser for a type
--
-- @since 0.0.0
class AutoRow a where
  -- | Default row parser for @a@
  --
  -- You may omit a definition for 'autoRow' if @a@ implements 'Generics.Generic'.
  --
  -- @since 0.0.0
  autoRow :: Row a

  default autoRow :: (Generics.Generic a, AutoRow (Generics.Rep a Void)) => Row a
  autoRow = genericRow

  {-# INLINE autoRow #-}

-- | @since 0.0.0
instance AutoColumnDelegate a => AutoRow (Generics.K1 tag a x) where
  autoRow = Generics.K1 <$> autoColumnDelegate

  {-# INLINE autoRow #-}

-- | @since 0.0.0
instance AutoRow (f x) => AutoRow (Generics.M1 tag meta f x) where
  autoRow = Generics.M1 <$> autoRow

  {-# INLINE autoRow #-}

-- | @since 0.0.0
instance (AutoRow (lhs x), AutoRow (rhs x)) => AutoRow ((Generics.:*:) lhs rhs x) where
  autoRow = (Generics.:*:) <$> autoRow <*> autoRow

  {-# INLINE autoRow #-}

-- | @since 0.0.0
instance AutoColumnDelegate a => AutoRow (Identity a)

-- | @since 0.0.0
instance
  ( AutoColumnDelegate a
  , AutoColumnDelegate b
  )
  => AutoRow (a, b)

-- | @since 0.0.0
instance
  ( AutoColumnDelegate a
  , AutoColumnDelegate b
  , AutoColumnDelegate c
  )
  => AutoRow (a, b, c)

-- | @since 0.0.0
instance
  ( AutoColumnDelegate a
  , AutoColumnDelegate b
  , AutoColumnDelegate c
  , AutoColumnDelegate d
  )
  => AutoRow (a, b, c, d)

-- | @since 0.0.0
instance
  ( AutoColumnDelegate a
  , AutoColumnDelegate b
  , AutoColumnDelegate c
  , AutoColumnDelegate d
  , AutoColumnDelegate e
  )
  => AutoRow (a, b, c, d, e)

-- | @since 0.0.0
instance
  ( AutoColumnDelegate a
  , AutoColumnDelegate b
  , AutoColumnDelegate c
  , AutoColumnDelegate d
  , AutoColumnDelegate e
  , AutoColumnDelegate f
  )
  => AutoRow (a, b, c, d, e, f)

-- | @since 0.0.0
instance
  ( AutoColumnDelegate a
  , AutoColumnDelegate b
  , AutoColumnDelegate c
  , AutoColumnDelegate d
  , AutoColumnDelegate e
  , AutoColumnDelegate f
  , AutoColumnDelegate g
  )
  => AutoRow (a, b, c, d, e, f, g)