{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ViewPatterns #-}
module Database.PostgreSQL.Simple.FromRow.Named
(
gFromRow
, fieldByName
, fieldByNameWith
, NoSuchColumn(..)
, TooManyColumns(..)
) where
import Control.Exception
import Control.Monad.Extra
import Control.Monad.Reader
import Control.Monad.State.Strict
import Data.ByteString (ByteString)
import qualified Data.ByteString.UTF8 as BS
import Data.Typeable
import qualified Database.PostgreSQL.LibPQ as PQ
import Database.PostgreSQL.Simple.FromField hiding (name)
import Database.PostgreSQL.Simple.FromRow
import Database.PostgreSQL.Simple.Internal
import GHC.TypeLits
import Generics.SOP
import qualified Generics.SOP.Type.Metadata as T
npLength :: NP f xs -> Word
npLength xs = go 0 xs
where
go :: Word -> NP f xs -> Word
go !i Nil = i
go !i (_ :* xs') = go (i + 1) xs'
gFromRow :: forall a modName tyName constrName fields xs.
( Generic a
, HasDatatypeInfo a
, All2 FromField (Code a)
, KnownSymbol modName
, KnownSymbol tyName
, DatatypeInfoOf a ~ 'T.ADT modName tyName '[ 'T.Record constrName fields]
, Code a ~ '[xs]
, T.DemoteFieldInfos fields xs
) => RowParser a
gFromRow = do
let f :: forall f. FromField f => FieldInfo f -> RowParser f
f (FieldInfo name) = fieldByName (BS.fromString name)
fieldInfos :: NP FieldInfo xs
fieldInfos = T.demoteFieldInfos (Proxy @fields)
guardMatchingColumnNumber (npLength fieldInfos)
res <-
fmap (to . SOP . Z) $
hsequence
(hcliftA
(Proxy :: Proxy FromField)
f
(T.demoteFieldInfos (Proxy :: Proxy fields)))
setToLastCol
pure res
guardMatchingColumnNumber :: Word -> RowParser ()
guardMatchingColumnNumber numFields =
RP $ do
Row {rowresult} <- ask
PQ.Col (fromIntegral -> numCols) <- liftIO' (PQ.nfields rowresult)
when
(numCols /= numFields)
((lift . lift . conversionError) (TooManyColumns numFields numCols))
liftIO' :: IO a -> ReaderT Row (StateT PQ.Column Conversion) a
liftIO' = lift . lift . liftConversion
-- | Thrown when there is no column of the given name.
data NoSuchColumn =
NoSuchColumn ByteString
deriving (Show, Eq, Ord, Typeable)
instance Exception NoSuchColumn
-- | Thrown by 'gFromRow' when trying to deserialize to a record that
-- has less fields than the current row has columns (counting both
-- named and unnamed columns).
data TooManyColumns = TooManyColumns
{ numRecordFields :: !Word -- ^ The expected number of record fields.
, numColumns :: !Word -- ^ The number of columns in the row that should have been deserialized.
} deriving (Show, Eq, Ord, Typeable)
instance Exception TooManyColumns
-- | This is similar to 'fieldWith' but instead of trying to
-- deserialize the field at the current position it goes through all
-- fields in the current row (starting at the beginning not the
-- current position) and tries to deserialize the first field with a
-- matching column name.
fieldByNameWith :: FieldParser a -> ByteString {- ^ column name to look for -} -> RowParser a
fieldByNameWith fieldP name =
RP $ do
Row {rowresult, row} <- ask
ncols <- liftIO' (PQ.nfields rowresult)
matchingCol <-
liftIO' $
findM
(\col -> (Just name ==) <$> PQ.fname rowresult col)
[PQ.Col 0 .. ncols - 1]
case matchingCol of
Nothing -> (lift . lift . conversionError) (NoSuchColumn name)
Just col ->
(lift . lift) $ do
oid <- liftConversion (PQ.ftype rowresult col)
val <- liftConversion (PQ.getvalue rowresult row col)
fieldP (Field rowresult col oid) val
-- | This is a wrapper around 'fieldByNameWith' that gets the
-- 'FieldParser' via the typeclass instance. Take a look at the docs
-- for 'fieldByNameWith' for the details of this function.
fieldByName :: FromField a => ByteString {- ^ column name to look for -} -> RowParser a
fieldByName = fieldByNameWith fromField
setToLastCol :: RowParser ()
setToLastCol =
RP $ do
Row {rowresult} <- ask
ncols <- liftIO' (PQ.nfields rowresult)
put ncols