Compare commits
19
Commits
ab9fac6a55
...
1.1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b39da29d13 | ||
|
|
3379fa9df7 | ||
|
|
2fddc958b9 | ||
|
|
9a2dbbd3cc | ||
|
|
d17865265a | ||
|
|
3879c2603f | ||
|
|
79085906a5 | ||
|
|
8ddeb8a61a | ||
|
|
aa1bb459be | ||
|
|
d6703f9646 | ||
|
|
6d3b1e660a | ||
|
|
97e5d9e61c | ||
|
|
56585dd5f1 | ||
|
|
2b2a048197 | ||
|
|
ef52bd9235 | ||
|
|
5a36ec328f | ||
|
|
19a1aa26e4 | ||
|
|
2caa00c06a | ||
|
|
e8ccecc8c7 |
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -83,3 +83,4 @@ getScoreByAge conn = do
|
|||||||
- [ ] `FromRow`
|
- [ ] `FromRow`
|
||||||
- [ ] Custom `FromField` impls
|
- [ ] Custom `FromField` impls
|
||||||
- [ ] Improve type errors when trying to `instance` a type that isn't a record (e.g. sum type)
|
- [ ] Improve type errors when trying to `instance` a type that isn't a record (e.g. sum type)
|
||||||
|
- [ ] Improve documentation for `fromRow` module
|
||||||
|
|||||||
Generated
+44
-8
@@ -1,23 +1,59 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1681753173,
|
"lastModified": 1731533236,
|
||||||
"narHash": "sha256-MrGmzZWLUqh2VstoikKLFFIELXm/lsf/G9U9zR96VD4=",
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
"owner": "NixOS",
|
"owner": "numtide",
|
||||||
"repo": "nixpkgs",
|
"repo": "flake-utils",
|
||||||
"rev": "0a4206a51b386e5cda731e8ac78d76ad924c7125",
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"id": "nixpkgs",
|
"owner": "numtide",
|
||||||
"type": "indirect"
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1752536923,
|
||||||
|
"narHash": "sha256-fdgPZR7VFSSRIQKOJLcs3qCJBWM64Uak0gAGtWTYAd8=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "c665e4d918eda5d78a175ed8d300809c44932160",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "release-25.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
@@ -1,35 +1,29 @@
|
|||||||
{
|
{
|
||||||
description = "An opionated Postgres library";
|
description = "An opionated Postgres library";
|
||||||
|
|
||||||
outputs = { self, nixpkgs }:
|
inputs.nixpkgs.url = "github:nixos/nixpkgs/release-25.05";
|
||||||
let
|
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
|
||||||
in {
|
|
||||||
apps.x86_64-linux.cabal = {
|
|
||||||
type = "app";
|
|
||||||
program = "${nixpkgs.legacyPackages.x86_64-linux.cabal-install}/bin/cabal";
|
|
||||||
};
|
|
||||||
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
|
outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system:
|
||||||
];
|
let
|
||||||
shellHook = ''
|
pkgs = import nixpkgs { inherit system; };
|
||||||
PS1="<opium> ''${PS1}"
|
opium = pkgs.haskellPackages.developPackage {
|
||||||
'';
|
root = ./.;
|
||||||
|
modifier = drv:
|
||||||
|
pkgs.haskell.lib.addBuildTools drv [
|
||||||
|
pkgs.cabal-install
|
||||||
|
pkgs.haskellPackages.implicit-hie
|
||||||
|
pkgs.haskell-language-server
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
in {
|
||||||
|
packages.opium = pkgs.haskell.lib.overrideCabal opium {
|
||||||
|
# Currently the tests require a running Postgres instance.
|
||||||
|
# This is not automated yet, so don't export the tests.
|
||||||
|
doCheck = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
devShells.default = opium.env;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
{-# LANGUAGE DataKinds #-}
|
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
|
||||||
{-# LANGUAGE LambdaCase #-}
|
|
||||||
{-# LANGUAGE KindSignatures #-}
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
|
||||||
module Database.PostgreSQL.Opium
|
module Database.PostgreSQL.Opium
|
||||||
|
-- * Connection Management
|
||||||
|
( Connection
|
||||||
|
, ConnectionError
|
||||||
|
, connect
|
||||||
|
, close
|
||||||
-- * Queries
|
-- * 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.
|
-- | TODO: Add @newtype Query = Query Text@ with @IsString@ instance to make constructing query strings at run time harder.
|
||||||
( fetch
|
, fetch
|
||||||
, fetch_
|
, fetch_
|
||||||
, execute
|
, execute
|
||||||
, execute_
|
, execute_
|
||||||
@@ -27,51 +30,68 @@ module Database.PostgreSQL.Opium
|
|||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Control.Monad (void)
|
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 (..), runExceptT, throwE)
|
||||||
|
import Data.Functor.Identity (Identity (..))
|
||||||
import Data.Proxy (Proxy (..))
|
import Data.Proxy (Proxy (..))
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Database.PostgreSQL.LibPQ
|
import Database.PostgreSQL.LibPQ (Result)
|
||||||
( Connection
|
|
||||||
, Result
|
|
||||||
)
|
|
||||||
|
|
||||||
import qualified Data.Text.Encoding as Encoding
|
import qualified Data.Text.Encoding as Encoding
|
||||||
import qualified Database.PostgreSQL.LibPQ as LibPQ
|
import qualified Database.PostgreSQL.LibPQ as LibPQ
|
||||||
|
|
||||||
|
import Database.PostgreSQL.Opium.Connection (Connection, ConnectionError, connect, close, withRawConnection)
|
||||||
import Database.PostgreSQL.Opium.Error (Error (..), ErrorPosition (..))
|
import Database.PostgreSQL.Opium.Error (Error (..), ErrorPosition (..))
|
||||||
import Database.PostgreSQL.Opium.FromField (FromField (..), RawField (..))
|
import Database.PostgreSQL.Opium.FromField (FromField (..), RawField (..))
|
||||||
import Database.PostgreSQL.Opium.FromRow (FromRow (..))
|
import Database.PostgreSQL.Opium.FromRow (FromRow (..), ColumnTable)
|
||||||
import Database.PostgreSQL.Opium.ToField (ToField (..))
|
import Database.PostgreSQL.Opium.ToField (ToField (..))
|
||||||
import Database.PostgreSQL.Opium.ToParamList (ToParamList (..))
|
import Database.PostgreSQL.Opium.ToParamList (ToParamList (..))
|
||||||
|
|
||||||
-- The order of the type parameters is important, because it is more common to use type applications for providing the row type.
|
class RowContainer c where
|
||||||
fetch
|
extract :: FromRow a => Result -> LibPQ.Row -> ColumnTable -> ExceptT Error IO (c a)
|
||||||
:: forall a b. (ToParamList b, FromRow a)
|
|
||||||
=> Connection
|
|
||||||
-> Text
|
|
||||||
-> b
|
|
||||||
-> IO (Either Error [a])
|
|
||||||
fetch conn query params = runExceptT $ do
|
|
||||||
result <- execParams conn query params
|
|
||||||
columnTable <- ExceptT $ getColumnTable @a 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])
|
instance RowContainer [] where
|
||||||
fetch_ conn query = fetch conn query ()
|
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
|
execute
|
||||||
:: forall a. ToParamList a
|
:: forall a. ToParamList a
|
||||||
=> Connection
|
=> Text
|
||||||
-> Text
|
|
||||||
-> a
|
-> a
|
||||||
|
-> Connection
|
||||||
-> IO (Either Error ())
|
-> IO (Either Error ())
|
||||||
execute conn query params = runExceptT $ void $ execParams conn query params
|
execute query params conn = runExceptT $ void $ execParams conn query params
|
||||||
|
|
||||||
execute_ :: Connection -> Text -> IO (Either Error ())
|
execute_ :: Text -> Connection -> IO (Either Error ())
|
||||||
execute_ conn query = execute conn query ()
|
execute_ query = execute query ()
|
||||||
|
|
||||||
execParams
|
execParams
|
||||||
:: ToParamList a
|
:: ToParamList a
|
||||||
@@ -80,14 +100,23 @@ execParams
|
|||||||
-> a
|
-> a
|
||||||
-> ExceptT Error IO Result
|
-> ExceptT Error IO Result
|
||||||
execParams conn query params = do
|
execParams conn query params = do
|
||||||
let queryBytes = Encoding.encodeUtf8 query
|
-- Actually run the query while locking the connection.
|
||||||
liftIO (LibPQ.execParams conn queryBytes (toParamList params) LibPQ.Binary) >>= \case
|
mbResult <- ExceptT runQuery
|
||||||
Nothing ->
|
-- Check whether the result is valid or nah.
|
||||||
except $ Left ErrorNoResult
|
result <- mbResult `orThrow` ErrorNoResult
|
||||||
Just result -> do
|
status <- liftIO $ LibPQ.resultStatus result
|
||||||
status <- liftIO $ LibPQ.resultStatus result
|
mbMessage <- liftIO $ LibPQ.resultErrorMessage result
|
||||||
mbMessage <- liftIO $ LibPQ.resultErrorMessage result
|
case mbMessage of
|
||||||
case mbMessage of
|
Just "" -> pure result
|
||||||
Just "" -> pure result
|
Nothing -> pure result
|
||||||
Nothing -> pure result
|
Just message -> throwE $ ErrorInvalidResult status $ Encoding.decodeUtf8 message
|
||||||
Just message -> except $ Left $ ErrorInvalidResult status $ Encoding.decodeUtf8 message
|
|
||||||
|
where
|
||||||
|
orThrow (Just x) _ = pure x
|
||||||
|
orThrow Nothing e = throwE e
|
||||||
|
|
||||||
|
queryBytes = Encoding.encodeUtf8 query
|
||||||
|
runQuery =
|
||||||
|
flip withRawConnection conn $ \mbRawConn -> runExceptT $ do
|
||||||
|
rawConn <- mbRawConn `orThrow` ErrorConnectionClosed
|
||||||
|
liftIO $ LibPQ.execParams rawConn queryBytes (toParamList params) LibPQ.Binary
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE OverloadedRecordDot #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
|
||||||
|
module Database.PostgreSQL.Opium.Connection
|
||||||
|
( Connection
|
||||||
|
, ConnectionError
|
||||||
|
, unsafeWithRawConnection
|
||||||
|
, withRawConnection
|
||||||
|
, connect
|
||||||
|
, close
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Concurrent.MVar (MVar, newMVar, modifyMVar_, withMVar)
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Database.PostgreSQL.LibPQ (ConnStatus (..))
|
||||||
|
import GHC.Stack (HasCallStack)
|
||||||
|
|
||||||
|
import qualified Data.Text.Encoding as Encoding
|
||||||
|
import qualified Database.PostgreSQL.LibPQ as LibPQ
|
||||||
|
|
||||||
|
newtype Connection = Connection
|
||||||
|
{ rawConnection :: MVar (Maybe LibPQ.Connection)
|
||||||
|
}
|
||||||
|
|
||||||
|
newtype ConnectionError = ConnectionError Text
|
||||||
|
deriving (Show)
|
||||||
|
|
||||||
|
withRawConnection
|
||||||
|
:: HasCallStack
|
||||||
|
=> (Maybe LibPQ.Connection -> IO a)
|
||||||
|
-> Connection
|
||||||
|
-> IO a
|
||||||
|
withRawConnection f conn = withMVar conn.rawConnection f
|
||||||
|
|
||||||
|
unsafeWithRawConnection
|
||||||
|
:: HasCallStack
|
||||||
|
=> (LibPQ.Connection -> IO a)
|
||||||
|
-> Connection
|
||||||
|
-> IO a
|
||||||
|
unsafeWithRawConnection f = withRawConnection $ \case
|
||||||
|
Nothing -> error "raw connection is missing! perhaps the connection was already closed."
|
||||||
|
Just rawConn -> f rawConn
|
||||||
|
|
||||||
|
connect :: Text -> IO (Either ConnectionError Connection)
|
||||||
|
connect connectionString = do
|
||||||
|
-- Appending the client_encoding setting overrides any previous setting in the connection string.
|
||||||
|
-- We set the client encoding here to make sure we can use it below for decoding connection
|
||||||
|
-- error messages.
|
||||||
|
rawConn <- LibPQ.connectdb $ Encoding.encodeUtf8 $ connectionString <> " client_encoding=UTF8"
|
||||||
|
status <- LibPQ.status rawConn
|
||||||
|
if status == ConnectionOk then
|
||||||
|
Right . Connection <$> newMVar (Just rawConn)
|
||||||
|
else do
|
||||||
|
rawError <- fromMaybe "" <$> LibPQ.errorMessage rawConn
|
||||||
|
pure $ Left $ ConnectionError $ Encoding.decodeUtf8Lenient rawError
|
||||||
|
|
||||||
|
close :: Connection -> IO ()
|
||||||
|
close conn = modifyMVar_ conn.rawConnection $ \case
|
||||||
|
Just rawConn -> do
|
||||||
|
LibPQ.finish rawConn
|
||||||
|
pure Nothing
|
||||||
|
Nothing ->
|
||||||
|
pure Nothing
|
||||||
@@ -17,6 +17,9 @@ data Error
|
|||||||
| ErrorInvalidOid Text Oid
|
| ErrorInvalidOid Text Oid
|
||||||
| ErrorUnexpectedNull ErrorPosition
|
| ErrorUnexpectedNull ErrorPosition
|
||||||
| ErrorInvalidField ErrorPosition Oid ByteString String
|
| ErrorInvalidField ErrorPosition Oid ByteString String
|
||||||
|
| ErrorNotExactlyOneRow
|
||||||
|
| ErrorMoreThanOneRow
|
||||||
|
| ErrorConnectionClosed
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
instance Exception Error where
|
instance Exception Error where
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ instance FromField ByteString where
|
|||||||
-- | See https://www.postgresql.org/docs/current/datatype-character.html.
|
-- | See https://www.postgresql.org/docs/current/datatype-character.html.
|
||||||
-- Accepts @text@, @character@ and @character varying@.
|
-- Accepts @text@, @character@ and @character varying@.
|
||||||
instance FromField Text where
|
instance FromField Text where
|
||||||
validOid Proxy = eq Oid.text \/ eq Oid.character \/ eq Oid.characterVarying
|
validOid Proxy = eq Oid.name \/ eq Oid.text \/ eq Oid.character \/ eq Oid.characterVarying
|
||||||
parseField = Encoding.decodeUtf8 <$> AP.takeByteString
|
parseField = Encoding.decodeUtf8 <$> AP.takeByteString
|
||||||
|
|
||||||
-- Accepts @text@, @character@ and @character varying@.
|
-- Accepts @text@, @character@ and @character varying@.
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ module Database.PostgreSQL.Opium.FromRow
|
|||||||
-- * FromRow
|
-- * FromRow
|
||||||
( FromRow (..)
|
( FromRow (..)
|
||||||
-- * Internal
|
-- * Internal
|
||||||
|
, ColumnTable
|
||||||
, toListColumnTable
|
, toListColumnTable
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@@ -52,12 +53,34 @@ class FromRow a where
|
|||||||
fromRow result columnTable row =
|
fromRow result columnTable row =
|
||||||
runExceptT $ to <$> fromRow' @0 FRProxy (FromRowCtx 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))
|
newtype ColumnTable = ColumnTable (Vector (Column, Oid))
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
newColumnTable :: [(Column, Oid)] -> ColumnTable
|
newColumnTable :: [(Column, Oid)] -> ColumnTable
|
||||||
newColumnTable = ColumnTable . Vector.fromList
|
newColumnTable = ColumnTable . Vector.fromList
|
||||||
|
|
||||||
|
concatColumnTables :: ColumnTable -> ColumnTable -> ColumnTable
|
||||||
|
concatColumnTables (ColumnTable a) (ColumnTable b) =
|
||||||
|
ColumnTable $ a <> b
|
||||||
|
|
||||||
indexColumnTable :: ColumnTable -> Int -> (Column, Oid)
|
indexColumnTable :: ColumnTable -> Int -> (Column, Oid)
|
||||||
indexColumnTable (ColumnTable v) i = v `Vector.unsafeIndex` i
|
indexColumnTable (ColumnTable v) i = v `Vector.unsafeIndex` i
|
||||||
|
|
||||||
@@ -83,36 +106,41 @@ instance {-# OVERLAPPABLE #-} (FromField t, KnownSymbol nameSym) => GetColumnTab
|
|||||||
instance {-# OVERLAPPING #-} (KnownSymbol nameSym, FromField t) => GetColumnTable' (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 (Maybe t))) where
|
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
|
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'.
|
-- | State kept for a call to 'fromRow'.
|
||||||
data FromRowCtx = FromRowCtx
|
data FromRowCtx = FromRowCtx
|
||||||
Result -- ^ Obtained from 'LibPQ.execParams'.
|
Result -- ^ Obtained from 'LibPQ.execParams'.
|
||||||
ColumnTable -- ^ 'Vector' of expected columns indices and OIDs.
|
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
|
data FRProxy (n :: Nat) (f :: Type -> Type) = FRProxy
|
||||||
|
|
||||||
class FromRow' (n :: Nat) (f :: Type -> Type) where
|
class FromRow' (n :: Nat) (f :: Type -> Type) where
|
||||||
type Members f :: Nat
|
|
||||||
|
|
||||||
fromRow' :: FRProxy n f -> FromRowCtx -> Row -> ExceptT Error IO (f p)
|
fromRow' :: FRProxy n f -> FromRowCtx -> Row -> ExceptT Error IO (f p)
|
||||||
|
|
||||||
instance FromRow' n f => FromRow' n (M1 D c f) where
|
instance FromRow' n f => FromRow' n (M1 D c f) where
|
||||||
type Members (M1 D c f) = Members f
|
fromRow' FRProxy ctx row =
|
||||||
|
M1 <$> fromRow' @n FRProxy ctx row
|
||||||
fromRow' FRProxy ctx row = M1 <$> fromRow' @n FRProxy ctx row
|
|
||||||
|
|
||||||
instance FromRow' n f => FromRow' n (M1 C c f) where
|
instance FromRow' n f => FromRow' n (M1 C c f) where
|
||||||
type Members (M1 C c f) = Members f
|
fromRow' FRProxy ctx row =
|
||||||
|
M1 <$> fromRow' @n FRProxy ctx row
|
||||||
|
|
||||||
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 =
|
||||||
instance (FromRow' n f, FromRow' (n + Members f) g) => FromRow' n (f :*: g) where
|
(:*:) <$> fromRow' @n FRProxy ctx row <*> fromRow' @(n + NumberOfMembers f) FRProxy ctx row
|
||||||
type Members (f :*: g) = Members f + Members g
|
|
||||||
|
|
||||||
fromRow' FRProxy ctx row = (:*:) <$> fromRow' @n FRProxy ctx row <*> fromRow' @(n + Members 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
|
instance {-# OVERLAPPABLE #-} (KnownNat n, KnownSymbol nameSym, FromField t) => FromRow' n (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 t)) where
|
||||||
type Members (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 t)) = 1
|
|
||||||
|
|
||||||
fromRow' FRProxy = decodeField memberIndex nameText $ \row ->
|
fromRow' FRProxy = decodeField memberIndex nameText $ \row ->
|
||||||
maybe (Left $ ErrorUnexpectedNull $ ErrorPosition row nameText) Right
|
maybe (Left $ ErrorUnexpectedNull $ ErrorPosition row nameText) Right
|
||||||
where
|
where
|
||||||
@@ -120,8 +148,6 @@ instance {-# OVERLAPPABLE #-} (KnownNat n, KnownSymbol nameSym, FromField t) =>
|
|||||||
nameText = Text.pack $ symbolVal @nameSym 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
|
instance {-# OVERLAPPING #-} (KnownNat n, KnownSymbol nameSym, FromField t) => FromRow' n (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 (Maybe t))) where
|
||||||
type Members (M1 S ('MetaSel ('Just nameSym) nu ns dl) (Rec0 (Maybe t))) = 1
|
|
||||||
|
|
||||||
fromRow' FRProxy = decodeField memberIndex nameText $ const pure
|
fromRow' FRProxy = decodeField memberIndex nameText $ const pure
|
||||||
where
|
where
|
||||||
memberIndex = fromIntegral $ natVal @n Proxy
|
memberIndex = fromIntegral $ natVal @n Proxy
|
||||||
@@ -159,4 +185,3 @@ decodeField memberIndex nameText g (FromRowCtx result columnTable) row = do
|
|||||||
first
|
first
|
||||||
(ErrorInvalidField (ErrorPosition row nameText) oid field)
|
(ErrorInvalidField (ErrorPosition row nameText) oid field)
|
||||||
(Just <$> fromField field)
|
(Just <$> fromField field)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ bytea = Oid 17
|
|||||||
|
|
||||||
-- string types
|
-- string types
|
||||||
|
|
||||||
|
name :: Oid
|
||||||
|
name = Oid 19
|
||||||
|
|
||||||
text :: Oid
|
text :: Oid
|
||||||
text = Oid 25
|
text = Oid 25
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -20,7 +20,7 @@ name: opium
|
|||||||
-- PVP summary: +-+------- breaking API changes
|
-- PVP summary: +-+------- breaking API changes
|
||||||
-- | | +----- non-breaking API additions
|
-- | | +----- non-breaking API additions
|
||||||
-- | | | +--- code changes with no API change
|
-- | | | +--- code changes with no API change
|
||||||
version: 0.1.0.0
|
version: 1.1.0.0
|
||||||
|
|
||||||
-- A short (one-line) description of the package.
|
-- A short (one-line) description of the package.
|
||||||
-- synopsis:
|
-- synopsis:
|
||||||
@@ -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.
|
||||||
@@ -63,6 +63,7 @@ library
|
|||||||
Database.PostgreSQL.Opium,
|
Database.PostgreSQL.Opium,
|
||||||
Database.PostgreSQL.Opium.FromField,
|
Database.PostgreSQL.Opium.FromField,
|
||||||
Database.PostgreSQL.Opium.FromRow,
|
Database.PostgreSQL.Opium.FromRow,
|
||||||
|
Database.PostgreSQL.Opium.Connection,
|
||||||
Database.PostgreSQL.Opium.ToField
|
Database.PostgreSQL.Opium.ToField
|
||||||
|
|
||||||
-- Modules included in this library but not exported.
|
-- Modules included in this library but not exported.
|
||||||
@@ -117,13 +118,15 @@ test-suite opium-test
|
|||||||
other-modules:
|
other-modules:
|
||||||
SpecHook,
|
SpecHook,
|
||||||
Database.PostgreSQL.OpiumSpec,
|
Database.PostgreSQL.OpiumSpec,
|
||||||
Database.PostgreSQL.Opium.FromFieldSpec
|
Database.PostgreSQL.Opium.FromFieldSpec,
|
||||||
|
Database.PostgreSQL.Opium.FromRowSpec
|
||||||
|
|
||||||
-- Test dependencies.
|
-- Test dependencies.
|
||||||
build-depends:
|
build-depends:
|
||||||
base,
|
base,
|
||||||
opium,
|
opium,
|
||||||
bytestring,
|
bytestring,
|
||||||
|
containers,
|
||||||
hspec,
|
hspec,
|
||||||
postgresql-libpq,
|
postgresql-libpq,
|
||||||
time,
|
time,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import Data.Time
|
|||||||
, timeOfDayToTime
|
, timeOfDayToTime
|
||||||
)
|
)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Database.PostgreSQL.LibPQ (Connection)
|
|
||||||
import Database.PostgreSQL.Opium (FromRow)
|
import Database.PostgreSQL.Opium (FromRow)
|
||||||
import GHC.Generics (Generic)
|
import GHC.Generics (Generic)
|
||||||
import Test.Hspec (SpecWith, describe, it, shouldBe, shouldSatisfy)
|
import Test.Hspec (SpecWith, describe, it, shouldBe, shouldSatisfy)
|
||||||
@@ -113,15 +112,15 @@ newtype ARawField = ARawField
|
|||||||
|
|
||||||
instance FromRow ARawField where
|
instance FromRow ARawField where
|
||||||
|
|
||||||
shouldFetch :: (Eq a, FromRow a, Show a) => Connection -> Text -> [a] -> IO ()
|
shouldFetch :: (Eq a, FromRow a, Show a) => Opium.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
|
||||||
p /\ q = \x -> p x && q x
|
p /\ q = \x -> p x && q x
|
||||||
|
|
||||||
spec :: SpecWith Connection
|
spec :: SpecWith Opium.Connection
|
||||||
spec = do
|
spec = do
|
||||||
describe "FromField Int" $ do
|
describe "FromField Int" $ do
|
||||||
it "Decodes smallint" $ \conn -> do
|
it "Decodes smallint" $ \conn -> do
|
||||||
@@ -213,15 +212,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 +228,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
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
|
||||||
|
module Database.PostgreSQL.Opium.FromRowSpec (spec) where
|
||||||
|
|
||||||
|
import Data.ByteString (ByteString)
|
||||||
|
import Data.Proxy (Proxy (..))
|
||||||
|
import Test.Hspec (SpecWith, aroundWith, describe, it, shouldBe)
|
||||||
|
|
||||||
|
import qualified Database.PostgreSQL.LibPQ as LibPQ
|
||||||
|
|
||||||
|
import Database.PostgreSQL.Opium.Connection (unsafeWithRawConnection)
|
||||||
|
import Database.PostgreSQL.OpiumSpec (ManyFields (..), MaybeTest (..), Person (..), Only (..))
|
||||||
|
|
||||||
|
import qualified Database.PostgreSQL.Opium as Opium
|
||||||
|
import qualified Database.PostgreSQL.Opium.FromRow as Opium.FromRow
|
||||||
|
|
||||||
|
shouldHaveColumns
|
||||||
|
:: Opium.FromRow a
|
||||||
|
=> Proxy a
|
||||||
|
-> LibPQ.Connection
|
||||||
|
-> ByteString
|
||||||
|
-> [LibPQ.Column]
|
||||||
|
-> IO ()
|
||||||
|
shouldHaveColumns proxy conn query expectedColumns = do
|
||||||
|
Just result <- LibPQ.execParams conn query [] LibPQ.Binary
|
||||||
|
columnTable <- Opium.getColumnTable proxy result
|
||||||
|
let actualColumns = fmap (map fst . Opium.FromRow.toListColumnTable) columnTable
|
||||||
|
actualColumns `shouldBe` Right expectedColumns
|
||||||
|
|
||||||
|
-- These test the mapping from Result to ColumnTable/FromRow instances.
|
||||||
|
-- They use the raw LibPQ connection for retrieving the Results.
|
||||||
|
spec :: SpecWith Opium.Connection
|
||||||
|
spec = aroundWith unsafeWithRawConnection $ do
|
||||||
|
describe "getColumnTable" $ do
|
||||||
|
it "Gets the column table for a result" $ \conn -> do
|
||||||
|
shouldHaveColumns @Person Proxy conn
|
||||||
|
"SELECT name, age FROM person"
|
||||||
|
[0, 1]
|
||||||
|
|
||||||
|
it "Gets the numbers right for funky configurations" $ \conn -> do
|
||||||
|
shouldHaveColumns @Person Proxy conn
|
||||||
|
"SELECT age, name FROM person"
|
||||||
|
[1, 0]
|
||||||
|
|
||||||
|
shouldHaveColumns @Person Proxy conn
|
||||||
|
"SELECT 0 AS a, 1 AS b, 2 AS c, age, 4 AS d, name FROM person"
|
||||||
|
[5, 3]
|
||||||
|
|
||||||
|
it "Fails for missing columns" $ \conn -> do
|
||||||
|
Just result <- LibPQ.execParams conn "SELECT 0 AS a FROM person" [] LibPQ.Binary
|
||||||
|
columnTable <- Opium.getColumnTable @Person Proxy result
|
||||||
|
columnTable `shouldBe` Left (Opium.ErrorMissingColumn "name")
|
||||||
|
|
||||||
|
describe "fromRow" $ do
|
||||||
|
it "Decodes rows in a Result" $ \conn -> do
|
||||||
|
Just result <- LibPQ.execParams conn "SELECT * FROM person" [] LibPQ.Binary
|
||||||
|
Right columnTable <- Opium.getColumnTable @Person Proxy result
|
||||||
|
|
||||||
|
row0 <- Opium.fromRow @Person result columnTable 0
|
||||||
|
row0 `shouldBe` Right (Person "paul" 25)
|
||||||
|
|
||||||
|
row1 <- Opium.fromRow @Person result columnTable 1
|
||||||
|
row1 `shouldBe` Right (Person "albus" 103)
|
||||||
|
|
||||||
|
it "Decodes NULL into Nothing for Maybes" $ \conn -> do
|
||||||
|
Just result <- LibPQ.execParams conn "SELECT NULL AS a" [] LibPQ.Binary
|
||||||
|
Right columnTable <- Opium.getColumnTable @MaybeTest Proxy result
|
||||||
|
|
||||||
|
row <- Opium.fromRow result columnTable 0
|
||||||
|
row `shouldBe` Right (MaybeTest Nothing)
|
||||||
|
|
||||||
|
it "Decodes values into Just for Maybes" $ \conn -> do
|
||||||
|
Just result <- LibPQ.execParams conn "SELECT 'abc' AS a" [] LibPQ.Binary
|
||||||
|
Right columnTable <- Opium.getColumnTable @MaybeTest Proxy result
|
||||||
|
|
||||||
|
row <- Opium.fromRow result columnTable 0
|
||||||
|
row `shouldBe` Right (MaybeTest $ Just "abc")
|
||||||
|
|
||||||
|
it "Works for many fields" $ \conn -> do
|
||||||
|
Just result <- LibPQ.execParams conn "SELECT 'abc' AS a, 42 AS b, 1.0::double precision AS c, 'test' AS d, true AS e" [] LibPQ.Binary
|
||||||
|
Right columnTable <- Opium.getColumnTable @ManyFields Proxy result
|
||||||
|
|
||||||
|
row <- Opium.fromRow result columnTable 0
|
||||||
|
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)
|
||||||
@@ -4,20 +4,16 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
|
|
||||||
module Database.PostgreSQL.OpiumSpec (spec) where
|
module Database.PostgreSQL.OpiumSpec (ManyFields (..), MaybeTest (..), Person (..), Only (..), spec) where
|
||||||
|
|
||||||
import Data.ByteString (ByteString)
|
|
||||||
import Data.Either (isLeft)
|
import Data.Either (isLeft)
|
||||||
import Data.Functor.Identity (Identity (..))
|
import Data.Functor.Identity (Identity (..))
|
||||||
import Data.Proxy (Proxy (..))
|
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Database.PostgreSQL.LibPQ (Connection)
|
|
||||||
import GHC.Generics (Generic)
|
import GHC.Generics (Generic)
|
||||||
import Test.Hspec (SpecWith, describe, it, shouldBe, shouldSatisfy)
|
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
|
||||||
@@ -55,99 +51,63 @@ newtype Only a = Only
|
|||||||
|
|
||||||
instance Opium.FromField a => Opium.FromRow (Only a) where
|
instance Opium.FromField a => Opium.FromRow (Only a) where
|
||||||
|
|
||||||
shouldHaveColumns
|
spec :: SpecWith Opium.Connection
|
||||||
:: Opium.FromRow a
|
|
||||||
=> Proxy a
|
|
||||||
-> Connection
|
|
||||||
-> ByteString
|
|
||||||
-> [LibPQ.Column]
|
|
||||||
-> IO ()
|
|
||||||
shouldHaveColumns proxy conn query expectedColumns = do
|
|
||||||
Just result <- LibPQ.execParams conn query [] LibPQ.Binary
|
|
||||||
columnTable <- Opium.getColumnTable proxy result
|
|
||||||
let actualColumns = fmap (map fst . Opium.FromRow.toListColumnTable) columnTable
|
|
||||||
actualColumns `shouldBe` Right expectedColumns
|
|
||||||
|
|
||||||
spec :: SpecWith Connection
|
|
||||||
spec = do
|
spec = do
|
||||||
describe "getColumnTable" $ do
|
|
||||||
it "Gets the column table for a result" $ \conn -> do
|
|
||||||
shouldHaveColumns @Person Proxy conn
|
|
||||||
"SELECT name, age FROM person"
|
|
||||||
[0, 1]
|
|
||||||
|
|
||||||
it "Gets the numbers right for funky configurations" $ \conn -> do
|
|
||||||
shouldHaveColumns @Person Proxy conn
|
|
||||||
"SELECT age, name FROM person"
|
|
||||||
[1, 0]
|
|
||||||
|
|
||||||
shouldHaveColumns @Person Proxy conn
|
|
||||||
"SELECT 0 AS a, 1 AS b, 2 AS c, age, 4 AS d, name FROM person"
|
|
||||||
[5, 3]
|
|
||||||
|
|
||||||
it "Fails for missing columns" $ \conn -> do
|
|
||||||
Just result <- LibPQ.execParams conn "SELECT 0 AS a FROM person" [] LibPQ.Binary
|
|
||||||
columnTable <- Opium.getColumnTable @Person Proxy result
|
|
||||||
columnTable `shouldBe` Left (Opium.ErrorMissingColumn "name")
|
|
||||||
|
|
||||||
describe "fromRow" $ do
|
|
||||||
it "Decodes rows in a Result" $ \conn -> do
|
|
||||||
Just result <- LibPQ.execParams conn "SELECT * FROM person" [] LibPQ.Binary
|
|
||||||
Right columnTable <- Opium.getColumnTable @Person Proxy result
|
|
||||||
|
|
||||||
row0 <- Opium.fromRow @Person result columnTable 0
|
|
||||||
row0 `shouldBe` Right (Person "paul" 25)
|
|
||||||
|
|
||||||
row1 <- Opium.fromRow @Person result columnTable 1
|
|
||||||
row1 `shouldBe` Right (Person "albus" 103)
|
|
||||||
|
|
||||||
it "Decodes NULL into Nothing for Maybes" $ \conn -> do
|
|
||||||
Just result <- LibPQ.execParams conn "SELECT NULL AS a" [] LibPQ.Binary
|
|
||||||
Right columnTable <- Opium.getColumnTable @MaybeTest Proxy result
|
|
||||||
|
|
||||||
row <- Opium.fromRow result columnTable 0
|
|
||||||
row `shouldBe` Right (MaybeTest Nothing)
|
|
||||||
|
|
||||||
it "Decodes values into Just for Maybes" $ \conn -> do
|
|
||||||
Just result <- LibPQ.execParams conn "SELECT 'abc' AS a" [] LibPQ.Binary
|
|
||||||
Right columnTable <- Opium.getColumnTable @MaybeTest Proxy result
|
|
||||||
|
|
||||||
row <- Opium.fromRow result columnTable 0
|
|
||||||
row `shouldBe` Right (MaybeTest $ Just "abc")
|
|
||||||
|
|
||||||
it "Works for many fields" $ \conn -> do
|
|
||||||
Just result <- LibPQ.execParams conn "SELECT 'abc' AS a, 42 AS b, 1.0::double precision AS c, 'test' AS d, true AS e" [] LibPQ.Binary
|
|
||||||
Right columnTable <- Opium.getColumnTable @ManyFields Proxy result
|
|
||||||
|
|
||||||
row <- Opium.fromRow result columnTable 0
|
|
||||||
row `shouldBe` Right (ManyFields "abc" 42 1.0 "test" True)
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
describe "close" $ do
|
||||||
|
it "Does not crash when called twice on the same connection" $ \conn -> do
|
||||||
|
Opium.close conn
|
||||||
|
Opium.close conn
|
||||||
|
|||||||
+11
-19
@@ -3,16 +3,15 @@
|
|||||||
module SpecHook (hook) where
|
module SpecHook (hook) where
|
||||||
|
|
||||||
import Control.Exception (bracket)
|
import Control.Exception (bracket)
|
||||||
import Database.PostgreSQL.LibPQ (Connection)
|
|
||||||
import System.Environment (lookupEnv)
|
import System.Environment (lookupEnv)
|
||||||
import Test.Hspec (Spec, SpecWith, around)
|
import Test.Hspec (Spec, SpecWith, around)
|
||||||
import Text.Printf (printf)
|
import Text.Printf (printf)
|
||||||
|
|
||||||
import qualified Data.Text as Text
|
import qualified Data.Text as Text
|
||||||
import qualified Data.Text.Encoding as Encoding
|
|
||||||
import qualified Database.PostgreSQL.LibPQ as LibPQ
|
|
||||||
|
|
||||||
setupConnection :: IO Connection
|
import qualified Database.PostgreSQL.Opium as Opium
|
||||||
|
|
||||||
|
setupConnection :: IO Opium.Connection
|
||||||
setupConnection = do
|
setupConnection = do
|
||||||
Just dbUser <- lookupEnv "DB_USER"
|
Just dbUser <- lookupEnv "DB_USER"
|
||||||
Just dbPass <- lookupEnv "DB_PASS"
|
Just dbPass <- lookupEnv "DB_PASS"
|
||||||
@@ -20,24 +19,17 @@ setupConnection = do
|
|||||||
Just dbPort <- lookupEnv "DB_PORT"
|
Just dbPort <- lookupEnv "DB_PORT"
|
||||||
|
|
||||||
let dsn = printf "host=localhost user=%s password=%s dbname=%s port=%s" dbUser dbPass dbName dbPort
|
let dsn = printf "host=localhost user=%s password=%s dbname=%s port=%s" dbUser dbPass dbName dbPort
|
||||||
conn <- LibPQ.connectdb $ Encoding.encodeUtf8 $ Text.pack dsn
|
Right conn <- Opium.connect $ Text.pack dsn
|
||||||
_ <- LibPQ.setClientEncoding conn "UTF8"
|
|
||||||
|
|
||||||
_ <- LibPQ.exec conn "CREATE TABLE person (name TEXT NOT NULL, age INT NOT NULL, score DOUBLE PRECISION NOT NULL, motto TEXT)"
|
Right _ <- Opium.execute_ "DROP TABLE IF EXISTS person" conn
|
||||||
_ <- LibPQ.exec conn "INSERT INTO person VALUES ('paul', 25, 30), ('albus', 103, 50.42)"
|
Right _ <- Opium.execute_ "CREATE TABLE person (name TEXT NOT NULL, age INT NOT NULL, score DOUBLE PRECISION NOT NULL, motto TEXT)" conn
|
||||||
|
Right _ <- Opium.execute_ "INSERT INTO person VALUES ('paul', 25, 30), ('albus', 103, 50.42)" conn
|
||||||
|
|
||||||
pure conn
|
pure conn
|
||||||
|
|
||||||
teardownConnection :: Connection -> IO ()
|
teardownConnection :: Opium.Connection -> IO ()
|
||||||
teardownConnection conn = do
|
teardownConnection conn = do
|
||||||
_ <- LibPQ.exec conn "DROP TABLE person"
|
Opium.close conn
|
||||||
LibPQ.finish conn
|
|
||||||
|
|
||||||
class SpecInput a where
|
hook :: SpecWith Opium.Connection -> Spec
|
||||||
hook :: SpecWith a -> Spec
|
hook = around $ bracket setupConnection teardownConnection
|
||||||
|
|
||||||
instance SpecInput Connection where
|
|
||||||
hook = around $ bracket setupConnection teardownConnection
|
|
||||||
|
|
||||||
instance SpecInput () where
|
|
||||||
hook = id
|
|
||||||
|
|||||||
Reference in New Issue
Block a user