17 Commits
9 changed files with 345 additions and 204 deletions
+12 -3
View File
@@ -33,8 +33,8 @@ data User = User
instance Opium.FromRow User where instance Opium.FromRow User where
getUsers :: Connection -> IO (Either Opium.Error [Users]) getUsers :: Connection -> IO (Either Opium.Error [User])
getUsers conn = Opium.fetch_ conn "SELECT * FROM user" getUsers = Opium.fetch_ "SELECT * FROM user"
``` ```
The `Opium.FromRow` instance is implemented generically for all product types ("records"). It looks up the field name in the query result and decodes the column value using `Opium.FromField`. The `Opium.FromRow` instance is implemented generically for all product types ("records"). It looks up the field name in the query result and decodes the column value using `Opium.FromField`.
@@ -51,7 +51,7 @@ instance Opium.FromRow ScoreByAge where
getScoreByAge :: Connection -> IO ScoreByAge getScoreByAge :: Connection -> IO ScoreByAge
getScoreByAge conn = do getScoreByAge conn = do
let query = "SELECT regr_intercept(score, age) AS t, regr_slope(score, age) AS m FROM user" let query = "SELECT regr_intercept(score, age) AS t, regr_slope(score, age) AS m FROM user"
Right [x] <- Opium.fetch_ conn query Right (Identity x) <- Opium.fetch_ query conn
pure x pure x
``` ```
@@ -74,4 +74,13 @@ getScoreByAge conn = do
- [ ] Implement JSON decoding - [ ] Implement JSON decoding
- [ ] Implement (anonymous) composite types - [ ] Implement (anonymous) composite types
- [ ] Catch [UnicodeException](https://hackage.haskell.org/package/text-2.1/docs/Data-Text-Encoding-Error.html#t:UnicodeException) when decoding text - [ ] Catch [UnicodeException](https://hackage.haskell.org/package/text-2.1/docs/Data-Text-Encoding-Error.html#t:UnicodeException) when decoding text
- This might not be necessary if Postgres guarantees us that having a textual OID on a field means that the field is encoded correctly.
- [ ] Implement array decoding - [ ] Implement array decoding
- [ ] Better docs and structure for `FromRow` module
- [ ] Lexer for PostgreSQL that replaces $name by $1, $2, etc.
- [ ] Tutorial
- [ ] Rationale
- [ ] `FromRow`
- [ ] Custom `FromField` impls
- [ ] Improve type errors when trying to `instance` a type that isn't a record (e.g. sum type)
- [ ] Improve documentation for `fromRow` module
Generated
+3 -3
View File
@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1681753173, "lastModified": 1719285171,
"narHash": "sha256-MrGmzZWLUqh2VstoikKLFFIELXm/lsf/G9U9zR96VD4=", "narHash": "sha256-kOUKtKfYEh8h8goL/P6lKF4Jb0sXnEkFyEganzdTGvo=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "0a4206a51b386e5cda731e8ac78d76ad924c7125", "rev": "cfb89a95f19bea461fc37228dc4d07b22fe617c2",
"type": "github" "type": "github"
}, },
"original": { "original": {
+16 -25
View File
@@ -3,33 +3,24 @@
outputs = { self, nixpkgs }: outputs = { self, nixpkgs }:
let let
pkgs = nixpkgs.legacyPackages.x86_64-linux; system = "aarch64-darwin";
pkgs = nixpkgs.legacyPackages.${system};
opium = pkgs.haskellPackages.developPackage {
root = ./.;
modifier = drv:
pkgs.haskell.lib.addBuildTools drv [
pkgs.cabal-install
pkgs.haskellPackages.implicit-hie
pkgs.haskell-language-server
];
};
in { in {
apps.x86_64-linux.cabal = { packages.${system}.opium = pkgs.haskell.lib.overrideCabal opium {
type = "app"; # Currently the tests require a running Postgres instance.
program = "${nixpkgs.legacyPackages.x86_64-linux.cabal-install}/bin/cabal"; # This is not automated yet, so don't export the tests.
doCheck = false;
}; };
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [
pkgs.cabal-install
pkgs.haskellPackages.implicit-hie
(pkgs.ghc.withPackages (hp: with hp; [
attoparsec
containers
bytestring
hspec
postgresql-libpq
text
time
transformers
vector
]))
pkgs.haskell-language-server devShells.${system}.default = opium.env;
];
shellHook = ''
PS1="<opium> ''${PS1}"
'';
};
}; };
} }
+69 -149
View File
@@ -1,53 +1,96 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
module Database.PostgreSQL.Opium module Database.PostgreSQL.Opium
( ColumnTable -- * Queries
--
-- Functions for performing queries. @fetch@ retrieves rows, @execute@ doesn't.
-- The 'Connection' parameter comes last to facilitate currying for implicitly passing in the connection, e.g. from some framework's connection pool.
--
-- | TODO: Add @newtype Query = Query Text@ with @IsString@ instance to make constructing query strings at run time harder.
( fetch
, fetch_
, execute
, execute_
-- * Classes to Implement
, FromRow (..)
, FromField (..)
, ToParamList (..)
, ToField (..)
-- * Utility Stuff
, Error (..) , Error (..)
, ErrorPosition (..) , ErrorPosition (..)
, FromField (..)
, FromRow (..)
, RawField (..) , RawField (..)
, fetch
, fetch_
, toListColumnTable
) )
where where
import Control.Monad (unless, void)
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Except (ExceptT (..), except, runExceptT) import Control.Monad.Trans.Except (ExceptT (..), except, runExceptT, throwE)
import Data.ByteString (ByteString) import Data.Functor.Identity (Identity (..))
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
import Data.Proxy (Proxy (..)) import Data.Proxy (Proxy (..))
import Data.Text (Text) import Data.Text (Text)
import Data.Vector (Vector)
import Database.PostgreSQL.LibPQ import Database.PostgreSQL.LibPQ
( Column ( Connection
, Connection
, Oid
, Result , Result
, Row
) )
import GHC.Generics (C, D, Generic, K1 (..), M1 (..), Meta (..), Rec0, Rep, S, to, (:*:) (..))
import GHC.TypeLits (KnownSymbol, symbolVal)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Encoding import qualified Data.Text.Encoding as Encoding
import qualified Data.Vector as Vector
import qualified Database.PostgreSQL.LibPQ as LibPQ import qualified Database.PostgreSQL.LibPQ as LibPQ
import Database.PostgreSQL.Opium.Error (Error (..), ErrorPosition (..)) import Database.PostgreSQL.Opium.Error (Error (..), ErrorPosition (..))
import Database.PostgreSQL.Opium.FromField (FromField (..), fromField, RawField (..)) import Database.PostgreSQL.Opium.FromField (FromField (..), RawField (..))
import Database.PostgreSQL.Opium.FromRow (FromRow (..), ColumnTable)
import Database.PostgreSQL.Opium.ToField (ToField (..))
import Database.PostgreSQL.Opium.ToParamList (ToParamList (..)) import Database.PostgreSQL.Opium.ToParamList (ToParamList (..))
class RowContainer c where
extract :: FromRow a => Result -> LibPQ.Row -> ColumnTable -> ExceptT Error IO (c a)
instance RowContainer [] where
extract result nRows columnTable = do
mapM (ExceptT . fromRow result columnTable) [0..nRows - 1]
instance RowContainer Maybe where
extract result nRows columnTable
| nRows == 0 = pure Nothing
| nRows == 1 = Just <$> ExceptT (fromRow result columnTable 0)
| otherwise = throwE ErrorMoreThanOneRow
instance RowContainer Identity where
extract result nRows columnTable = do
unless (nRows == 1) $ throwE ErrorNotExactlyOneRow
Identity <$> ExceptT (fromRow result columnTable 0)
-- The order of the type parameters is important, because it is more common to use type applications for providing the row type and row container type.
fetch
:: forall a b c. (ToParamList c, FromRow a, RowContainer b)
=> Text
-> c
-> Connection
-> IO (Either Error (b a))
fetch query params conn = runExceptT $ do
result <- execParams conn query params
nRows <- liftIO $ LibPQ.ntuples result
columnTable <- ExceptT $ getColumnTable @a Proxy result
extract result nRows columnTable
fetch_ :: forall a c. (FromRow a, RowContainer c) => Text -> Connection -> IO (Either Error (c a))
fetch_ query = fetch query ()
execute
:: forall a. ToParamList a
=> Text
-> a
-> Connection
-> IO (Either Error ())
execute query params conn = runExceptT $ void $ execParams conn query params
execute_ :: Text -> Connection -> IO (Either Error ())
execute_ query = execute query ()
execParams execParams
:: ToParamList a :: ToParamList a
=> Connection => Connection
@@ -66,126 +109,3 @@ execParams conn query params = do
Just "" -> pure result Just "" -> pure result
Nothing -> pure result Nothing -> pure result
Just message -> except $ Left $ ErrorInvalidResult status $ Encoding.decodeUtf8 message Just message -> except $ Left $ ErrorInvalidResult status $ Encoding.decodeUtf8 message
fetch
:: forall a b. (ToParamList a, FromRow b)
=> Connection
-> Text
-> a
-> IO (Either Error [b])
fetch conn query params = runExceptT $ do
result <- execParams conn query params
columnTable <- ExceptT $ getColumnTable @b Proxy result
nRows <- liftIO $ LibPQ.ntuples result
mapM (ExceptT . fromRow result columnTable) [0..nRows - 1]
fetch_ :: forall a. FromRow a => Connection -> Text -> IO (Either Error [a])
fetch_ conn query = fetch conn query ()
newtype ColumnTable = ColumnTable (Vector (Column, Oid))
deriving (Eq, Show)
newColumnTable :: [(Column, Oid)] -> ColumnTable
newColumnTable = ColumnTable . Vector.fromList
indexColumnTable :: ColumnTable -> Int -> (Column, Oid)
indexColumnTable (ColumnTable v) i = v `Vector.unsafeIndex` i
toListColumnTable :: ColumnTable -> [(Column, Oid)]
toListColumnTable (ColumnTable v) = Vector.toList v
class FromRow a where
getColumnTable :: Proxy a -> Result -> IO (Either Error ColumnTable)
default getColumnTable :: (Generic a, GetColumnTable' (Rep a)) => Proxy a -> Result -> IO (Either Error ColumnTable)
getColumnTable Proxy = runExceptT . fmap newColumnTable . getColumnTable' @(Rep a) Proxy
fromRow :: Result -> ColumnTable -> Row -> IO (Either Error a)
default fromRow :: (Generic a, FromRow' (Rep a)) => Result -> ColumnTable -> Row -> IO (Either Error a)
fromRow result columnTable row = do
iRef <- newIORef 0
runExceptT $ to <$> fromRow' (FromRowCtx result columnTable iRef) row
class GetColumnTable' f where
getColumnTable' :: Proxy (f p) -> Result -> ExceptT Error IO [(Column, Oid)]
instance GetColumnTable' f => GetColumnTable' (M1 D c f) where
getColumnTable' Proxy = getColumnTable' @f Proxy
instance GetColumnTable' f => GetColumnTable' (M1 C c f) where
getColumnTable' Proxy = getColumnTable' @f Proxy
instance (GetColumnTable' f, GetColumnTable' g) => GetColumnTable' (f :*: g) where
getColumnTable' Proxy result =
(++) <$> getColumnTable' @f Proxy result <*> getColumnTable' @g Proxy result
checkColumn :: forall a. FromField a => Proxy a -> String -> Result -> ExceptT Error IO [(Column, Oid)]
checkColumn Proxy nameStr result = do
column <- ExceptT $ maybe (Left $ ErrorMissingColumn nameText) Right <$> LibPQ.fnumber result name
oid <- liftIO $ LibPQ.ftype result column
if validOid @a Proxy oid then
pure [(column, oid)]
else
except $ Left $ ErrorInvalidOid nameText oid
where
nameText = Text.pack nameStr
name = Encoding.encodeUtf8 nameText
instance {-# OVERLAPPABLE #-} (FromField t, KnownSymbol nameSym) => GetColumnTable' (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 t)) where
getColumnTable' Proxy = checkColumn @t Proxy $ symbolVal @nameSym Proxy
instance {-# OVERLAPPING #-} (KnownSymbol nameSym, FromField t) => GetColumnTable' (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 (Maybe t))) where
getColumnTable' Proxy = checkColumn @t Proxy $ symbolVal @nameSym Proxy
data FromRowCtx = FromRowCtx Result ColumnTable (IORef Int)
class FromRow' f where
fromRow' :: FromRowCtx -> Row -> ExceptT Error IO (f p)
instance FromRow' f => FromRow' (M1 D c f) where
fromRow' ctx row = M1 <$> fromRow' ctx row
instance FromRow' f => FromRow' (M1 C c f) where
fromRow' ctx row = M1 <$> fromRow' ctx row
instance (FromRow' f, FromRow' g) => FromRow' (f :*: g) where
fromRow' ctx row = do
y <- fromRow' ctx row
z <- fromRow' ctx row
pure $ y :*: z
decodeField
:: FromField t
=> Text
-> (Row -> Maybe t -> Either Error t')
-> FromRowCtx
-> Row
-> ExceptT Error IO (M1 S m (Rec0 t') p)
decodeField nameText g (FromRowCtx result columnTable iRef) row = do
i <- liftIO $ readIORef iRef
liftIO $ modifyIORef' iRef (+1)
let (column, oid) = columnTable `indexColumnTable` i
mbField <- liftIO $ LibPQ.getvalue result row column
mbValue <- except $ getValue oid mbField
value <- except $ g row mbValue
pure $ M1 $ K1 value
where
getValue :: FromField u => LibPQ.Oid -> Maybe ByteString -> Either Error (Maybe u)
getValue oid = maybe (Right Nothing) $ \field ->
mapLeft
(ErrorInvalidField (ErrorPosition row nameText) oid field)
(Just <$> fromField field)
mapLeft :: (b -> c) -> Either b a -> Either c a
mapLeft f (Left l) = Left $ f l
mapLeft _ (Right r) = Right r
instance {-# OVERLAPPABLE #-} (FromField t, KnownSymbol nameSym) => FromRow' (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 t)) where
fromRow' = decodeField nameText $ \row ->
maybe (Left $ ErrorUnexpectedNull $ ErrorPosition row nameText) Right
where
nameText = Text.pack $ symbolVal @nameSym Proxy
instance {-# OVERLAPPING #-} (KnownSymbol nameSym, FromField t) => FromRow' (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 (Maybe t))) where
fromRow' = decodeField nameText $ const pure
where
nameText = Text.pack $ symbolVal @nameSym Proxy
+2
View File
@@ -17,6 +17,8 @@ data Error
| ErrorInvalidOid Text Oid | ErrorInvalidOid Text Oid
| ErrorUnexpectedNull ErrorPosition | ErrorUnexpectedNull ErrorPosition
| ErrorInvalidField ErrorPosition Oid ByteString String | ErrorInvalidField ErrorPosition Oid ByteString String
| ErrorNotExactlyOneRow
| ErrorMoreThanOneRow
deriving (Eq, Show) deriving (Eq, Show)
instance Exception Error where instance Exception Error where
+187
View File
@@ -0,0 +1,187 @@
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE UndecidableInstances #-}
module Database.PostgreSQL.Opium.FromRow
-- * FromRow
( FromRow (..)
-- * Internal
, ColumnTable
, toListColumnTable
) where
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Except (ExceptT (..), except, runExceptT)
import Data.ByteString (ByteString)
import Data.Bifunctor (first)
import Data.Kind (Type)
import Data.Proxy (Proxy (..))
import Data.Text (Text)
import Data.Vector (Vector)
import Database.PostgreSQL.LibPQ
( Column
, Oid
, Result
, Row
)
import GHC.Generics (Generic, C, D, K1 (..), M1 (..), Meta (..), Rec0, Rep, S, to, (:*:) (..))
import GHC.TypeLits (KnownNat, KnownSymbol, Nat, natVal, symbolVal, type (+))
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Encoding
import qualified Data.Vector as Vector
import qualified Database.PostgreSQL.LibPQ as LibPQ
import Database.PostgreSQL.Opium.Error (Error (..), ErrorPosition (..))
import Database.PostgreSQL.Opium.FromField (FromField (..), fromField)
class FromRow a where
getColumnTable :: Proxy a -> Result -> IO (Either Error ColumnTable)
default getColumnTable :: (Generic a, GetColumnTable' (Rep a)) => Proxy a -> Result -> IO (Either Error ColumnTable)
getColumnTable Proxy = runExceptT . fmap newColumnTable . getColumnTable' @(Rep a) Proxy
fromRow :: Result -> ColumnTable -> Row -> IO (Either Error a)
default fromRow :: (Generic a, FromRow' 0 (Rep a)) => Result -> ColumnTable -> Row -> IO (Either Error a)
fromRow result columnTable row =
runExceptT $ to <$> fromRow' @0 FRProxy (FromRowCtx result columnTable) row
instance
( Generic a
, GetColumnTable' (Rep a)
, FromRow' 0 (Rep a)
, Generic b
, GetColumnTable' (Rep b)
, FromRow' (NumberOfMembers (Rep a)) (Rep b)
) => FromRow (a, b) where
getColumnTable Proxy result = runExceptT $ do
ctA <- newColumnTable <$> getColumnTable' @(Rep a) Proxy result
ctB <- newColumnTable <$> getColumnTable' @(Rep b) Proxy result
pure $ ctA `concatColumnTables` ctB
fromRow result ct row = runExceptT $ do
x <- to <$> fromRow' @0 FRProxy (FromRowCtx result ct) row
y <- to <$> fromRow' @(NumberOfMembers (Rep a)) FRProxy (FromRowCtx result ct) row
pure (x, y)
newtype ColumnTable = ColumnTable (Vector (Column, Oid))
deriving (Eq, Show)
newColumnTable :: [(Column, Oid)] -> ColumnTable
newColumnTable = ColumnTable . Vector.fromList
concatColumnTables :: ColumnTable -> ColumnTable -> ColumnTable
concatColumnTables (ColumnTable a) (ColumnTable b) =
ColumnTable $ a <> b
indexColumnTable :: ColumnTable -> Int -> (Column, Oid)
indexColumnTable (ColumnTable v) i = v `Vector.unsafeIndex` i
toListColumnTable :: ColumnTable -> [(Column, Oid)]
toListColumnTable (ColumnTable v) = Vector.toList v
class GetColumnTable' f where
getColumnTable' :: Proxy (f p) -> Result -> ExceptT Error IO [(Column, Oid)]
instance GetColumnTable' f => GetColumnTable' (M1 D c f) where
getColumnTable' Proxy = getColumnTable' @f Proxy
instance GetColumnTable' f => GetColumnTable' (M1 C c f) where
getColumnTable' Proxy = getColumnTable' @f Proxy
instance (GetColumnTable' f, GetColumnTable' g) => GetColumnTable' (f :*: g) where
getColumnTable' Proxy result =
(++) <$> getColumnTable' @f Proxy result <*> getColumnTable' @g Proxy result
instance {-# OVERLAPPABLE #-} (FromField t, KnownSymbol nameSym) => GetColumnTable' (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 t)) where
getColumnTable' Proxy = checkColumn @t Proxy $ symbolVal @nameSym Proxy
instance {-# OVERLAPPING #-} (KnownSymbol nameSym, FromField t) => GetColumnTable' (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 (Maybe t))) where
getColumnTable' Proxy = checkColumn @t Proxy $ symbolVal @nameSym Proxy
-- | Number of members in the generic representation of a record type (doesn't support sum types).
type family NumberOfMembers f where
-- The data type itself has as many members as the type that it defines.
NumberOfMembers (M1 D _ f) = NumberOfMembers f
-- The constructor has as many members as the type that it contains.
NumberOfMembers (M1 C _ f) = NumberOfMembers f
-- A product type has as many members as its subtypes have together.
NumberOfMembers (f :*: g) = NumberOfMembers f + NumberOfMembers g
-- A selector has/is exactly one member.
NumberOfMembers (M1 S _ f) = 1
-- | State kept for a call to 'fromRow'.
data FromRowCtx = FromRowCtx
Result -- ^ Obtained from 'LibPQ.execParams'.
ColumnTable -- ^ 'Vector' of expected columns indices and OIDs.
-- Specialized proxy type to be used instead of `Proxy (n, f)`
data FRProxy (n :: Nat) (f :: Type -> Type) = FRProxy
class FromRow' (n :: Nat) (f :: Type -> Type) where
fromRow' :: FRProxy n f -> FromRowCtx -> Row -> ExceptT Error IO (f p)
instance FromRow' n f => FromRow' n (M1 D c f) where
fromRow' FRProxy ctx row =
M1 <$> fromRow' @n FRProxy ctx row
instance FromRow' n f => FromRow' n (M1 C c f) where
fromRow' FRProxy ctx row =
M1 <$> fromRow' @n FRProxy ctx row
instance (FromRow' n f, FromRow' (n + NumberOfMembers f) g) => FromRow' n (f :*: g) where
fromRow' FRProxy ctx row =
(:*:) <$> fromRow' @n FRProxy ctx row <*> fromRow' @(n + NumberOfMembers f) FRProxy ctx row
instance {-# OVERLAPPABLE #-} (KnownNat n, KnownSymbol nameSym, FromField t) => FromRow' n (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 t)) where
fromRow' FRProxy = decodeField memberIndex nameText $ \row ->
maybe (Left $ ErrorUnexpectedNull $ ErrorPosition row nameText) Right
where
memberIndex = fromIntegral $ natVal @n Proxy
nameText = Text.pack $ symbolVal @nameSym Proxy
instance {-# OVERLAPPING #-} (KnownNat n, KnownSymbol nameSym, FromField t) => FromRow' n (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 (Maybe t))) where
fromRow' FRProxy = decodeField memberIndex nameText $ const pure
where
memberIndex = fromIntegral $ natVal @n Proxy
nameText = Text.pack $ symbolVal @nameSym Proxy
checkColumn :: forall a. FromField a => Proxy a -> String -> Result -> ExceptT Error IO [(Column, Oid)]
checkColumn Proxy nameStr result = do
column <- ExceptT $ maybe (Left $ ErrorMissingColumn nameText) Right <$> LibPQ.fnumber result name
oid <- liftIO $ LibPQ.ftype result column
if validOid @a Proxy oid then
pure [(column, oid)]
else
except $ Left $ ErrorInvalidOid nameText oid
where
nameText = Text.pack nameStr
name = Encoding.encodeUtf8 nameText
decodeField
:: FromField t
=> Int
-> Text
-> (Row -> Maybe t -> Either Error t')
-> FromRowCtx
-> Row
-> ExceptT Error IO (M1 S m (Rec0 t') p)
decodeField memberIndex nameText g (FromRowCtx result columnTable) row = do
let (column, oid) = columnTable `indexColumnTable` memberIndex
mbField <- liftIO $ LibPQ.getvalue result row column
mbValue <- except $ fromFieldIfPresent oid mbField
value <- except $ g row mbValue
pure $ M1 $ K1 value
where
fromFieldIfPresent :: FromField u => LibPQ.Oid -> Maybe ByteString -> Either Error (Maybe u)
fromFieldIfPresent oid = maybe (Right Nothing) $ \field ->
first
(ErrorInvalidField (ErrorPosition row nameText) oid field)
(Just <$> fromField field)
+5 -2
View File
@@ -52,7 +52,7 @@ build-type: Simple
-- extra-source-files: -- extra-source-files:
common warnings common warnings
ghc-options: -Wall ghc-options: -Wall -Wextra
library library
-- Import common warning flags. -- Import common warning flags.
@@ -62,11 +62,14 @@ library
exposed-modules: exposed-modules:
Database.PostgreSQL.Opium, Database.PostgreSQL.Opium,
Database.PostgreSQL.Opium.FromField, Database.PostgreSQL.Opium.FromField,
Database.PostgreSQL.Opium.FromRow,
Database.PostgreSQL.Opium.ToField
-- Modules included in this library but not exported. -- Modules included in this library but not exported.
other-modules: other-modules:
Database.PostgreSQL.Opium.Error, Database.PostgreSQL.Opium.Error,
Database.PostgreSQL.Opium.Oid Database.PostgreSQL.Opium.Oid,
Database.PostgreSQL.Opium.ToParamList
-- LANGUAGE extensions used by modules in this package. -- LANGUAGE extensions used by modules in this package.
-- other-extensions: -- other-extensions:
@@ -115,7 +115,7 @@ instance FromRow ARawField where
shouldFetch :: (Eq a, FromRow a, Show a) => Connection -> Text -> [a] -> IO () shouldFetch :: (Eq a, FromRow a, Show a) => Connection -> Text -> [a] -> IO ()
shouldFetch conn query expectedRows = do shouldFetch conn query expectedRows = do
actualRows <- Opium.fetch_ conn query actualRows <- Opium.fetch_ query conn
actualRows `shouldBe` Right expectedRows actualRows `shouldBe` Right expectedRows
(/\) :: (a -> Bool) -> (a -> Bool) -> a -> Bool (/\) :: (a -> Bool) -> (a -> Bool) -> a -> Bool
@@ -213,15 +213,15 @@ spec = do
shouldFetch conn "SELECT 4.2::real AS float" [AFloat 4.2] shouldFetch conn "SELECT 4.2::real AS float" [AFloat 4.2]
it "Decodes NaN::real" $ \conn -> do it "Decodes NaN::real" $ \conn -> do
Right [AFloat value] <- Opium.fetch_ conn "SELECT 'NaN'::real AS float" Right [AFloat value] <- Opium.fetch_ "SELECT 'NaN'::real AS float" conn
value `shouldSatisfy` isNaN value `shouldSatisfy` isNaN
it "Decodes Infinity::real" $ \conn -> do it "Decodes Infinity::real" $ \conn -> do
Right [AFloat value] <- Opium.fetch_ conn "SELECT 'Infinity'::real AS float" Right [AFloat value] <- Opium.fetch_ "SELECT 'Infinity'::real AS float" conn
value `shouldSatisfy` (isInfinite /\ (> 0)) value `shouldSatisfy` (isInfinite /\ (> 0))
it "Decodes -Infinity::real" $ \conn -> do it "Decodes -Infinity::real" $ \conn -> do
Right [AFloat value] <- Opium.fetch_ conn "SELECT '-Infinity'::real AS float" Right [AFloat value] <- Opium.fetch_ "SELECT '-Infinity'::real AS float" conn
value `shouldSatisfy` (isInfinite /\ (< 0)) value `shouldSatisfy` (isInfinite /\ (< 0))
describe "FromField Double" $ do describe "FromField Double" $ do
@@ -229,22 +229,22 @@ spec = do
shouldFetch conn "SELECT 4.2::double precision AS double" [ADouble 4.2] shouldFetch conn "SELECT 4.2::double precision AS double" [ADouble 4.2]
it "Decodes NaN::double precision" $ \conn -> do it "Decodes NaN::double precision" $ \conn -> do
Right [ADouble value] <- Opium.fetch_ conn "SELECT 'NaN'::double precision AS double" Right [ADouble value] <- Opium.fetch_ "SELECT 'NaN'::double precision AS double" conn
value `shouldSatisfy` isNaN value `shouldSatisfy` isNaN
it "Decodes Infinity::double precision" $ \conn -> do it "Decodes Infinity::double precision" $ \conn -> do
Right [ADouble value] <- Opium.fetch_ conn "SELECT 'Infinity'::double precision AS double" Right [ADouble value] <- Opium.fetch_ "SELECT 'Infinity'::double precision AS double" conn
value `shouldSatisfy` (isInfinite /\ (> 0)) value `shouldSatisfy` (isInfinite /\ (> 0))
it "Decodes -Infinity::double precision" $ \conn -> do it "Decodes -Infinity::double precision" $ \conn -> do
Right [ADouble value] <- Opium.fetch_ conn "SELECT '-Infinity'::double precision AS double" Right [ADouble value] <- Opium.fetch_ "SELECT '-Infinity'::double precision AS double" conn
value `shouldSatisfy` (isInfinite /\ (< 0)) value `shouldSatisfy` (isInfinite /\ (< 0))
it "Decodes {inf,-inf}::double precision" $ \conn -> do it "Decodes {inf,-inf}::double precision" $ \conn -> do
Right [ADouble value0] <- Opium.fetch_ conn "SELECT 'inf'::double precision AS double" Right [ADouble value0] <- Opium.fetch_ "SELECT 'inf'::double precision AS double" conn
value0 `shouldSatisfy` (isInfinite /\ (> 0)) value0 `shouldSatisfy` (isInfinite /\ (> 0))
Right [ADouble value1] <- Opium.fetch_ conn "SELECT '-inf'::double precision AS double" Right [ADouble value1] <- Opium.fetch_ "SELECT '-inf'::double precision AS double" conn
value1 `shouldSatisfy` (isInfinite /\ (< 0)) value1 `shouldSatisfy` (isInfinite /\ (< 0))
describe "FromField Bool" $ do describe "FromField Bool" $ do
+42 -13
View File
@@ -7,6 +7,7 @@
module Database.PostgreSQL.OpiumSpec (spec) where module Database.PostgreSQL.OpiumSpec (spec) where
import Data.ByteString (ByteString) import Data.ByteString (ByteString)
import Data.Either (isLeft)
import Data.Functor.Identity (Identity (..)) import Data.Functor.Identity (Identity (..))
import Data.Proxy (Proxy (..)) import Data.Proxy (Proxy (..))
import Data.Text (Text) import Data.Text (Text)
@@ -16,6 +17,7 @@ import Test.Hspec (SpecWith, describe, it, shouldBe, shouldSatisfy)
import qualified Database.PostgreSQL.LibPQ as LibPQ import qualified Database.PostgreSQL.LibPQ as LibPQ
import qualified Database.PostgreSQL.Opium as Opium import qualified Database.PostgreSQL.Opium as Opium
import qualified Database.PostgreSQL.Opium.FromRow as Opium.FromRow
data Person = Person data Person = Person
{ name :: Text { name :: Text
@@ -47,16 +49,12 @@ data ScoreByAge = ScoreByAge
instance Opium.FromRow ScoreByAge where instance Opium.FromRow ScoreByAge where
data Only a = Only newtype Only a = Only
{ only :: a { only :: a
} deriving (Eq, Generic, Show) } deriving (Eq, Generic, Show)
instance Opium.FromField a => Opium.FromRow (Only a) where instance Opium.FromField a => Opium.FromRow (Only a) where
isLeft :: Either a b -> Bool
isLeft (Left _) = True
isLeft _ = False
shouldHaveColumns shouldHaveColumns
:: Opium.FromRow a :: Opium.FromRow a
=> Proxy a => Proxy a
@@ -67,7 +65,7 @@ shouldHaveColumns
shouldHaveColumns proxy conn query expectedColumns = do shouldHaveColumns proxy conn query expectedColumns = do
Just result <- LibPQ.execParams conn query [] LibPQ.Binary Just result <- LibPQ.execParams conn query [] LibPQ.Binary
columnTable <- Opium.getColumnTable proxy result columnTable <- Opium.getColumnTable proxy result
let actualColumns = fmap (map fst . Opium.toListColumnTable) columnTable let actualColumns = fmap (map fst . Opium.FromRow.toListColumnTable) columnTable
actualColumns `shouldBe` Right expectedColumns actualColumns `shouldBe` Right expectedColumns
spec :: SpecWith Connection spec :: SpecWith Connection
@@ -124,32 +122,63 @@ spec = do
row <- Opium.fromRow result columnTable 0 row <- Opium.fromRow result columnTable 0
row `shouldBe` Right (ManyFields "abc" 42 1.0 "test" True) row `shouldBe` Right (ManyFields "abc" 42 1.0 "test" True)
it "Decodes multiple records into a tuple" $ \conn -> do
Just result <- LibPQ.execParams conn "SELECT 'albus' AS name, 123 AS age, 42 AS only" [] LibPQ.Binary
Right columnTable <- Opium.getColumnTable @(Person, Only Int) Proxy result
row <- Opium.fromRow @(Person, Only Int) result columnTable 0
row `shouldBe` Right (Person "albus" 123, Only 42)
describe "fetch" $ do describe "fetch" $ do
it "Passes numbered parameters and retrieves a list of rows" $ \conn -> do it "Passes numbered parameters and retrieves a list of rows" $ \conn -> do
rows <- Opium.fetch conn "SELECT ($1 + $2) AS only" (17 :: Int, 25 :: Int) rows <- Opium.fetch "SELECT ($1 + $2) AS only" (17 :: Int, 25 :: Int) conn
rows `shouldBe` Right [Only (42 :: Int)] rows `shouldBe` Right [Only (42 :: Int)]
it "Uses Identity to pass single parameters" $ \conn -> do it "Uses Identity to pass single parameters" $ \conn -> do
rows <- Opium.fetch conn "SELECT count(*) AS only FROM person WHERE name = $1" $ Identity ("paul" :: Text) rows <- Opium.fetch "SELECT count(*) AS only FROM person WHERE name = $1" (Identity ("paul" :: Text)) conn
rows `shouldBe` Right [Only (1 :: Int)] rows `shouldBe` Right [Only (1 :: Int)]
describe "fetch_" $ do describe "fetch_" $ do
it "Retrieves a list of rows" $ \conn -> do it "Retrieves a list of rows" $ \conn -> do
rows <- Opium.fetch_ conn "SELECT * FROM person" rows <- Opium.fetch_ "SELECT * FROM person" conn
rows `shouldBe` Right [Person "paul" 25, Person "albus" 103] rows `shouldBe` Right [Person "paul" 25, Person "albus" 103]
it "Fails for invalid queries" $ \conn -> do it "Fails for invalid queries" $ \conn -> do
rows <- Opium.fetch_ @Person conn "MRTLBRNFT" rows <- Opium.fetch_ @Person @[] "MRTLBRNFT" conn
rows `shouldSatisfy` isLeft rows `shouldSatisfy` isLeft
it "Fails for unexpected NULLs" $ \conn -> do it "Fails for unexpected NULLs" $ \conn -> do
rows <- Opium.fetch_ @Person conn "SELECT NULL AS name, 0 AS age" rows <- Opium.fetch_ @Person @[] "SELECT NULL AS name, 0 AS age" conn
rows `shouldBe` Left (Opium.ErrorUnexpectedNull (Opium.ErrorPosition 0 "name")) rows `shouldBe` Left (Opium.ErrorUnexpectedNull (Opium.ErrorPosition 0 "name"))
it "Fails for the wrong column type" $ \conn -> do it "Fails for the wrong column type" $ \conn -> do
rows <- Opium.fetch_ @Person conn "SELECT 'quby' AS name, 'indeterminate' AS age" rows <- Opium.fetch_ @Person @[] "SELECT 'quby' AS name, 'indeterminate' AS age" conn
rows `shouldBe` Left (Opium.ErrorInvalidOid "age" $ LibPQ.Oid 25) rows `shouldBe` Left (Opium.ErrorInvalidOid "age" $ LibPQ.Oid 25)
it "Works for the readme regression example" $ \conn -> do it "Works for the readme regression example" $ \conn -> do
rows <- Opium.fetch_ @ScoreByAge conn "SELECT regr_intercept(score, age) AS t, regr_slope(score, age) AS m FROM person" rows <- Opium.fetch_ @ScoreByAge @[] "SELECT regr_intercept(score, age) AS t, regr_slope(score, age) AS m FROM person" conn
rows `shouldSatisfy` \case { (Right [ScoreByAge _ _]) -> True; _ -> False } rows `shouldSatisfy` \case { (Right [ScoreByAge _ _]) -> True; _ -> False }
it "Accepts exactly one row when Identity is the row container type" $ \conn -> do
row <- Opium.fetch_ "SELECT 42 AS only" conn
row `shouldBe` Right (Identity (Only (42 :: Int)))
it "Does not accept zero rows when Identity is the row container type" $ \conn -> do
row <- Opium.fetch_ @(Only Int) @Identity "SELECT 42 AS only WHERE false" conn
row `shouldSatisfy` isLeft
it "Does not accept two rows when Identity is the row container type" $ \conn -> do
row <- Opium.fetch_ @(Only Int) @Identity "SELECT 17 AS only UNION ALL SELECT 25 AS only" conn
row `shouldSatisfy` isLeft
it "Accepts zero rows when Maybe is the row container type" $ \conn -> do
row <- Opium.fetch_ @(Only Int) @Maybe "SELECT 17 AS only WHERE false" conn
row `shouldBe` Right Nothing
it "Accepts one row when Maybe is the row container type" $ \conn -> do
row <- Opium.fetch_ @(Only Int) @Maybe "SELECT 42 AS only" conn
row `shouldBe` Right (Just (Only 42))
it "Does not accept two rows when Maybe is the row container type" $ \conn -> do
row <- Opium.fetch_ @(Only Int) @Maybe "SELECT 17 AS only UNION ALL SELECT 25 AS only" conn
row `shouldSatisfy` isLeft