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)