duckdb-simple-0.1.0.0: src/Database/DuckDB/Simple/FromRow.hs
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : Database.DuckDB.Simple.FromRow
Description : Convert rows of 'Field's into Haskell values using a parser-style interface.
-}
module Database.DuckDB.Simple.FromRow (
-- * Row parsing
RowParser (..),
field,
fieldWith,
numFieldsRemaining,
parseRow,
-- * Generic derivation
GFromRow (..),
FromRow (..),
-- * Error conversion
resultErrorToSqlError,
rowErrorsToSqlError,
) where
import Control.Applicative (Alternative (..))
import Control.Exception (Exception, SomeException, fromException, toException)
import Control.Monad (MonadPlus, replicateM)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT)
import Control.Monad.Trans.State.Strict (StateT, get, put, runStateT)
import Data.Maybe (listToMaybe, mapMaybe)
import Data.Text (Text)
import qualified Data.Text as Text
import GHC.Generics
import Database.DuckDB.Simple.FromField
import Database.DuckDB.Simple.Internal (SQLError (..))
import Database.DuckDB.Simple.Ok (Ok (..))
import Database.DuckDB.Simple.Types (Only (..), Query, (:.) (..))
-- | Row parsing environment (read-only data available to the parser).
newtype RowParseRO = RowParseRO
{ rowParseColumnCount :: Int
}
-- | Column-out-of-bounds sentinel used internally to map parser failures.
newtype ColumnOutOfBounds = ColumnOutOfBounds {columnOutOfBoundsIndex :: Int}
deriving stock (Eq, Show)
instance Exception ColumnOutOfBounds
-- | Parser used by 'FromRow' implementations.
newtype RowParser a = RowParser
{ runRowParser :: ReaderT RowParseRO (StateT (Int, [Field]) Ok) a
}
deriving stock (Functor)
deriving newtype (Applicative, Alternative, Monad, MonadPlus)
-- | Generic derivation helper mirroring @sqlite-simple@.
class GFromRow f where
gFromRow :: RowParser (f p)
instance GFromRow U1 where
gFromRow = pure U1
instance (GFromRow a) => GFromRow (M1 i c a) where
gFromRow = M1 <$> gFromRow
instance (FromField a) => GFromRow (K1 i a) where
gFromRow = K1 <$> field
instance (GFromRow a, GFromRow b) => GFromRow (a :*: b) where
gFromRow = (:*:) <$> gFromRow <*> gFromRow
-- | Types that can be constructed from database rows.
class FromRow a where
fromRow :: RowParser a
default fromRow :: (Generic a, GFromRow (Rep a)) => RowParser a
fromRow = to <$> gFromRow
-- | Pull the next field using the provided 'FieldParser'.
fieldWith :: FieldParser a -> RowParser a
fieldWith fieldParser = RowParser $ do
RowParseRO{rowParseColumnCount} <- ask
(columnIndex, remaining) <- lift get
case remaining of
[] -> do
lift (put (columnIndex + 1, []))
lift (lift (Errors [toException (ColumnOutOfBounds (columnIndex + 1))]))
(f : rest) -> do
lift (put (columnIndex + 1, rest))
if columnIndex >= rowParseColumnCount
then lift (lift (Errors [toException (ColumnOutOfBounds (columnIndex + 1))]))
else case fieldParser f of
Errors err -> lift $ lift $ Errors err
Ok value -> pure value
-- | Pull the next field and parse it using its 'FromField' instance.
field :: (FromField a) => RowParser a
field = fieldWith fromField
-- | Report how many columns remain unread in the current row.
numFieldsRemaining :: RowParser Int
numFieldsRemaining = RowParser $ do
RowParseRO{rowParseColumnCount} <- ask
(columnIndex, _) <- lift get
pure (rowParseColumnCount - columnIndex)
-- | Execute a 'RowParser' against the provided row.
parseRow :: RowParser a -> [Field] -> Ok a
parseRow parser fields =
let context = RowParseRO (length fields)
initialState = (0, fields)
in case runStateT (runReaderT (runRowParser parser) context) initialState of
Ok (value, (columnCount, _))
| columnCount == length fields -> Ok value
| otherwise -> left (ColumnOutOfBounds (columnCount + 1))
Errors errs -> Errors errs
instance FromRow () where
fromRow = pure ()
instance (FromField a) => FromRow (Only a) where
fromRow = Only <$> field
instance (FromRow a, FromRow b) => FromRow (a :. b) where
fromRow = (:.) <$> fromRow <*> fromRow
instance (FromField a, FromField b) => FromRow (a, b) where
fromRow = (,) <$> field <*> field
instance (FromField a, FromField b, FromField c) => FromRow (a, b, c) where
fromRow = (,,) <$> field <*> field <*> field
instance (FromField a, FromField b, FromField c, FromField d) => FromRow (a, b, c, d) where
fromRow = (,,,) <$> field <*> field <*> field <*> field
instance (FromField a, FromField b, FromField c, FromField d, FromField e) => FromRow (a, b, c, d, e) where
fromRow = (,,,,) <$> field <*> field <*> field <*> field <*> field
instance (FromField a, FromField b, FromField c, FromField d, FromField e, FromField f) => FromRow (a, b, c, d, e, f) where
fromRow = (,,,,,) <$> field <*> field <*> field <*> field <*> field <*> field
instance
(FromField a, FromField b, FromField c, FromField d, FromField e, FromField f, FromField g) =>
FromRow (a, b, c, d, e, f, g)
where
fromRow = (,,,,,,) <$> field <*> field <*> field <*> field <*> field <*> field <*> field
instance
(FromField a, FromField b, FromField c, FromField d, FromField e, FromField f, FromField g, FromField h) =>
FromRow (a, b, c, d, e, f, g, h)
where
fromRow = (,,,,,,,) <$> field <*> field <*> field <*> field <*> field <*> field <*> field <*> field
instance
( FromField a
, FromField b
, FromField c
, FromField d
, FromField e
, FromField f
, FromField g
, FromField h
, FromField i
) =>
FromRow (a, b, c, d, e, f, g, h, i)
where
fromRow =
(,,,,,,,,)
<$> field
<*> field
<*> field
<*> field
<*> field
<*> field
<*> field
<*> field
<*> field
instance
( FromField a
, FromField b
, FromField c
, FromField d
, FromField e
, FromField f
, FromField g
, FromField h
, FromField i
, FromField j
) =>
FromRow (a, b, c, d, e, f, g, h, i, j)
where
fromRow =
(,,,,,,,,,)
<$> field
<*> field
<*> field
<*> field
<*> field
<*> field
<*> field
<*> field
<*> field
<*> field
instance (FromField a) => FromRow [a] where
fromRow = do
remaining <- numFieldsRemaining
replicateM remaining field
-- | Convert a 'ResultError' into a user-facing 'SQLError'.
resultErrorToSqlError :: Query -> ResultError -> SQLError
resultErrorToSqlError query err =
SQLError
{ sqlErrorMessage = renderError err
, sqlErrorType = Nothing
, sqlErrorQuery = Just query
}
-- | Collapse parser failure diagnostics into an 'SQLError' while preserving the query.
rowErrorsToSqlError :: Query -> [SomeException] -> SQLError
rowErrorsToSqlError query errs =
case listToMaybe (mapMaybe (fromException :: SomeException -> Maybe ResultError) errs) of
Just resultErr -> resultErrorToSqlError query resultErr
Nothing ->
case listToMaybe (mapMaybe (fromException :: SomeException -> Maybe ColumnOutOfBounds) errs) of
Just (ColumnOutOfBounds idx) ->
SQLError
{ sqlErrorMessage =
Text.concat
[ "duckdb-simple: column index "
, Text.pack (show idx)
, " out of bounds"
]
, sqlErrorType = Nothing
, sqlErrorQuery = Just query
}
Nothing ->
SQLError
{ sqlErrorMessage =
Text.pack $ "duckdb-simple: row-parsing failed:" <> show errs
, sqlErrorType = Nothing
, sqlErrorQuery = Just query
}
renderError :: ResultError -> Text
renderError = \case
Incompatible{errSQLType, errHaskellType} ->
Text.concat
[ "duckdb-simple: column "
, " has type "
, errSQLType
, " but expected "
, errHaskellType
]
UnexpectedNull{errSQLType} ->
Text.concat
[ "duckdb-simple: column "
, " is NULL but expected "
, errSQLType
]
ConversionFailed{errMessage} ->
errMessage