A complete overhaul

This commit is contained in:
Nikita Volkov 2014-12-27 03:29:36 +03:00
parent 1dfed9be76
commit 39970138fa
8 changed files with 330 additions and 356 deletions

View File

@ -1,3 +1,15 @@
# 0.6.0 - Major API overhaul
* Removed the `Session` monad, opening a direct access to the connection pool, thus providing for a simpler compatibility with other libraries.
* The connection timeout is now set using `Int` for simplicity.
* There are no exceptions any more. All the error-reporting is typed and done explicitly, using `Either`.
* The error types are now mostly backend-specific.
* The transaction mode is now extended to support uncommitable transactions with the `TxWriteMode` type.
* `Tx` now has a `MonadError` instance, which allows to handle errors while remaining in the transaction.
* All `Tx` functions now have a "Tx" suffix.
* There is no more `list` transaction. Instead there is `vectorTx`.
* The `Statement` type is renamed to `Stmt` and is now exported from the main API.
* `RowParser` is now uninstantiable. This enforces the idiomatic usage of the library.
# 0.5.0
* Update the "list-t" and "monad-control" deps

View File

@ -1,7 +1,7 @@
name:
hasql
version:
0.5.0
0.6.0
synopsis:
A minimalistic general high level API for relational databases
description:
@ -10,7 +10,7 @@ description:
.
Features:
.
* Concise and crisp API. Just a few functions and two monads doing all the
* Concise and crisp API. Just a few functions and one monad doing all the
boilerplate job for you.
.
* A powerful transaction abstraction, which provides
@ -31,7 +31,7 @@ description:
* Automated management of resources related to connections, transactions and
cursors.
.
* A built-in connections pool.
* A built-in connection pool.
.
* Compile-time generation of templates. You just can't write a statement with an
incorrect number of placeholders.
@ -94,27 +94,21 @@ library
exposed-modules:
Hasql
build-depends:
hasql-backend == 0.2.*,
-- template-haskell:
--
resource-pool == 0.2.*,
hasql-backend == 0.3.*,
--
template-haskell >= 2.8 && < 2.10,
-- parsing:
--
attoparsec >= 0.10 && < 0.13,
-- database:
ex-pool == 0.2.*,
-- data:
--
vector < 0.11,
time >= 1.4 && < 1.6,
bytestring == 0.10.*,
text >= 1.0 && < 1.3,
-- errors:
loch-th == 0.2.*,
placeholders == 0.1.*,
-- general:
safe == 0.3.*,
list-t >= 0.4 && < 0.5,
--
either >= 4.3 && < 4.4,
list-t >= 0.3.1 && < 0.5,
mmorph == 1.0.*,
monad-control == 1.0.*,
transformers-base == 0.4.*,
mtl >= 2.1 && < 2.3,
transformers >= 0.3 && < 0.5,
base-prelude >= 0.1.3 && < 0.2,
base >= 4.5 && < 4.8
@ -138,10 +132,12 @@ test-suite hspec
build-depends:
--
hasql,
hasql-backend == 0.2.*,
hasql-backend,
--
hspec == 2.1.*,
--
vector,
--
mtl-prelude < 3,
base-prelude,
base
@ -165,7 +161,7 @@ test-suite hspec-postgres
build-depends:
--
hasql,
hasql-postgres == 0.9.*,
hasql-postgres == 0.10.*,
--
slave-thread == 0.1.*,
--
@ -173,6 +169,8 @@ test-suite hspec-postgres
--
text,
--
monad-control,
either,
mtl-prelude < 3,
base-prelude,
base
@ -197,7 +195,7 @@ benchmark demo
Haskell2010
build-depends:
hasql,
hasql-postgres == 0.9.*,
hasql-postgres == 0.10.*,
transformers >= 0.3 && < 0.5,
base >= 4.5 && < 4.8

View File

@ -1,5 +1,7 @@
import BasePrelude
import MTLPrelude
import Control.Monad.Trans.Either
import Control.Monad.Trans.Control
import Test.Hspec
import Data.Text (Text)
import qualified Hasql as H
@ -15,17 +17,17 @@ main =
context "Unhandled transaction conflict" $ do
it "should not be" $ do
session $ H.tx Nothing $ do
H.unit [H.q|DROP TABLE IF EXISTS artist|]
H.unit [H.q|DROP TABLE IF EXISTS artist_union|]
H.unit $
session $ tx Nothing $ do
H.unitTx [H.q|DROP TABLE IF EXISTS artist|]
H.unitTx [H.q|DROP TABLE IF EXISTS artist_union|]
H.unitTx $
[H.q|
CREATE TABLE "artist_union" (
"id" BIGSERIAL,
PRIMARY KEY ("id")
)
|]
H.unit $
H.unitTx $
[H.q|
CREATE TABLE "artist" (
"id" BIGSERIAL,
@ -39,13 +41,13 @@ main =
let
insertArtistUnion :: H.Tx HP.Postgres s Int64
insertArtistUnion =
fmap (runIdentity . fromJust) $ H.single $
fmap (runIdentity . fromJust) $ H.maybeTx $
[H.q|
INSERT INTO artist_union DEFAULT VALUES RETURNING id
|]
insertArtist :: Int64 -> [Text] -> H.Tx HP.Postgres s Int64
insertArtist unionID artistNames =
fmap (runIdentity . fromJust) $ H.single $
fmap (runIdentity . fromJust) $ H.maybeTx $
[H.q|
INSERT INTO artist
(artist_union_id,
@ -58,7 +60,7 @@ main =
process =
SlaveThread.fork $ do
session $ replicateM_ 100 $ do
H.tx (Just (H.Serializable, True)) $ do
tx (Just (H.Serializable, Just True)) $ do
unionID <- insertArtistUnion
insertArtist unionID ["a", "b", "c"]
signal
@ -68,32 +70,25 @@ main =
context "RowParser" $ do
it "should fail on incorrect arity" $ do
flip shouldThrow (\case H.UnparsableRow _ -> True; _ -> False) $
flip shouldSatisfy (\case Left (H.UnparsableResult _) -> True; _ -> False) =<< do
session $ do
H.tx Nothing $ do
H.unit [H.q|DROP TABLE IF EXISTS data|]
H.unit [H.q|CREATE TABLE data (
tx Nothing $ do
H.unitTx [H.q|DROP TABLE IF EXISTS data|]
H.unitTx [H.q|CREATE TABLE data (
field1 DECIMAL NOT NULL,
field2 BIGINT NOT NULL,
PRIMARY KEY (field1)
)|]
H.unit [H.q|INSERT INTO data (field1, field2) VALUES (0, 0)|]
H.unitTx [H.q|INSERT INTO data (field1, field2) VALUES (0, 0)|]
mrow :: Maybe (Double, Int64, String) <-
H.tx Nothing $
H.single $ [H.q|SELECT * FROM data|]
tx Nothing $
H.maybeTx $ [H.q|SELECT * FROM data|]
return ()
-- * Helpers
-------------------------
session :: (forall s. H.Session HP.Postgres s IO r) -> IO r
session =
H.session backendSettings poolSettings
where
backendSettings = HP.ParamSettings "localhost" 5432 "postgres" "" "postgres"
poolSettings = fromJust $ H.sessionSettings 6 30
newBatchGate :: Int -> IO (IO (), IO ())
newBatchGate amount =
do
@ -102,3 +97,29 @@ newBatchGate amount =
let signal = atomically $ readTVar counter >>= writeTVar counter . pred
block = atomically $ readTVar counter >>= \x -> when (x > 0) retry
in (signal, block)
-- * Hasql utils
-------------------------
newtype HSession m r =
HSession (ReaderT (H.Pool HP.Postgres) (EitherT (H.TxError HP.Postgres) m) r)
deriving (Functor, Applicative, Monad, MonadIO)
instance MonadTrans HSession where
lift = HSession . lift . lift
tx :: MonadIO m => H.TxMode -> (forall s. H.Tx HP.Postgres s a) -> HSession m a
tx mode m =
HSession $ ReaderT $ \p -> EitherT $ liftIO $ H.tx p mode m
session :: MonadBaseControl IO m => HSession m r -> m (Either (H.TxError HP.Postgres) r)
session (HSession m) =
control $ \unlift -> do
p <- H.acquirePool backendSettings poolSettings
r <- unlift $ runEitherT $ runReaderT m p
H.releasePool p
return r
where
backendSettings = HP.ParamSettings "localhost" 5432 "postgres" "" "postgres"
poolSettings = fromJust $ H.poolSettings 6 30

View File

@ -2,21 +2,25 @@ import BasePrelude
import Test.Hspec
import qualified Hasql as H
import qualified Hasql.Backend as HB
import qualified Data.Vector as V
data X
instance HB.Backend X where
data StatementArgument X =
StatementArgument String
deriving (Eq, Show)
instance HB.Mapping X Char where
renderValue = StatementArgument . show
data instance HB.StmtParam X =
StmtParam String
deriving (Eq, Show)
deriving instance Show (HB.Stmt X)
deriving instance Eq (HB.Stmt X)
instance HB.CxValue X Char where
encodeValue = StmtParam . show
main =
hspec $ do
context "Quasi quoter" $ do
it "generates a proper statement" $ do
(flip shouldBe)
(" SELECT ? ", [HB.renderValue 'a'], True)
([H.q| SELECT ? |] 'a' :: HB.Statement X)
(HB.Stmt " SELECT (? + ?) " (V.fromList [HB.encodeValue 'a', HB.encodeValue 'b']) True)
([H.q| SELECT (? + ?) |] 'a' 'b' :: HB.Stmt X)

View File

@ -4,265 +4,236 @@
-- For an introduction to the package
-- and links to more documentation please refer to
-- <../ the package's index page>.
--
-- This API is completely disinfected from exceptions.
-- All error-reporting is explicit and
-- is presented using the 'Either' type.
module Hasql
(
-- * Session
Session,
session,
sessionUnlifter,
-- * Pool
Pool,
acquirePool,
releasePool,
-- ** Session Settings
SessionSettings,
sessionSettings,
-- ** Pool Settings
PoolSettings,
poolSettings,
-- * Statement
Bknd.Stmt,
q,
-- ** Statement Execution
unitTx,
countTx,
maybeTx,
vectorTx,
streamTx,
-- * Transaction
Tx,
tx,
-- ** Transaction Settings
Mode,
Backend.IsolationLevel(..),
Bknd.TxMode(..),
Bknd.TxIsolationLevel(..),
Bknd.TxWriteMode(..),
-- * Statement Quasi-Quoter
q,
-- ** Transaction Error
TxError(..),
-- * Statement Execution
unit,
count,
single,
list,
stream,
-- * Results Stream
-- ** Result Stream
TxListT,
-- * Row parser
RowParser.RowParser(..),
-- * Error
Error(..),
-- * Row Parser
RowParser.RowParser,
)
where
import Hasql.Prelude hiding (Error)
import Hasql.Backend (Backend)
import Hasql.RowParser (RowParser)
import qualified Hasql.Backend as Backend
import Hasql.Prelude
import qualified Hasql.Backend as Bknd
import qualified Hasql.RowParser as RowParser
import qualified Hasql.QParser as QParser
import qualified ListT
import qualified Data.Pool as Pool
import qualified Data.Vector as Vector
import qualified Data.Vector.Mutable as MVector
import qualified Language.Haskell.TH as TH
import qualified Language.Haskell.TH.Quote as TH
import qualified Hasql.TH as THUtil
-- * Session
-- * Resources
-------------------------
-- |
-- A monad transformer,
-- which executes transactions.
--
-- * @b@ is a backend.
--
-- * @s@ is an anonymous variable,
-- used to associate 'sessionUnlifter' with a specific session.
--
-- * @m@ is an inner (transformed) monad.
--
-- * @r@ is a result.
newtype Session b s m r =
Session (ReaderT (Pool.Pool (Backend.Connection b)) m r)
deriving (Functor, Applicative, Monad, MonadTrans, MonadIO)
instance MonadTransControl (Session b s) where
type StT (Session b s) a = a
liftWith onRunner =
Session $ ReaderT $ \e -> onRunner $ \(Session (ReaderT f)) -> f e
restoreT =
Session . ReaderT . const
instance (MonadBase IO m) => MonadBase IO (Session b s m) where
liftBase = Session . liftBase
instance (MonadBaseControl IO m) => MonadBaseControl IO (Session b s m) where
type StM (Session b s m) a = ComposeSt (Session b s) m a
liftBaseWith = defaultLiftBaseWith
restoreM = defaultRestoreM
-- A connection pool.
newtype Pool c =
Pool (Pool.Pool (Either (Bknd.CxError c) c))
-- |
-- Given backend settings, session settings, and a session monad transformer,
-- execute it in the inner monad.
--
-- It uses the same trick as 'ST' with the anonymous @s@ type argument
-- to prohibit the use of the result of
-- 'sessionUnlifter' outside of its creator session.
session ::
(Backend.Backend b, MonadBaseControl IO m) =>
b -> SessionSettings -> (forall s. Session b s m r) -> m r
session backend (SessionSettings size timeout) s =
control $ \runInIO ->
mask $ \unmask -> do
p <- Pool.createPool (Backend.connect backend) Backend.disconnect 1 timeout size
r <- try $ unmask $ runInIO $ runSession p s
Pool.purgePool p
either (throwIO :: SomeException -> IO r) return r
-- Given backend-specific connection settings and pool settings,
-- acquire a backend connection pool,
-- which can then be used to work with the DB.
acquirePool :: Bknd.Cx c => Bknd.CxSettings c -> PoolSettings -> IO (Pool c)
acquirePool cxSettings (PoolSettings size timeout) =
fmap Pool $
Pool.createPool (Bknd.acquireCx cxSettings)
(either (const $ return ()) Bknd.releaseCx)
(1)
(fromIntegral timeout)
(size)
-- |
-- Get a session unlifting function,
-- which allows to execute a session in the inner monad
-- using the resources of the current session.
--
-- Using this function in combination with 'lift'
-- you can interleave 'Session' with other monad transformers.
--
-- This function has the following property:
--
-- > (sessionUnlifter >>= \unlift -> lift (unlift m)) ≡ m
sessionUnlifter :: (MonadBaseControl IO m) => Session b s m (Session b s m r -> m r)
sessionUnlifter =
Session $ ReaderT $ return . runSession
runSession :: (MonadBaseControl IO m) => Pool.Pool (Backend.Connection b) -> Session b s m r -> m r
runSession e (Session r) =
control $ \runInIO ->
catch (runInIO (runReaderT r e)) $ \case
Backend.CantConnect t -> throwIO $ CantConnect t
Backend.ConnectionLost t -> throwIO $ ConnectionLost t
Backend.ErroneousResult t -> throwIO $ ErroneousResult t
Backend.UnexpectedResult t -> throwIO $ UnexpectedResult t
Backend.UnparsableTemplate t -> throwIO $ UnparsableTemplate t
Backend.TransactionConflict -> $bug "Unexpected TransactionConflict exception"
Backend.NotInTransaction -> throwIO $ NotInTransaction
-- Release all connections acquired by the pool.
releasePool :: Pool c -> IO ()
releasePool (Pool p) =
Pool.destroyAllResources p
-- ** Session Settings
-- ** Pool Settings
-------------------------
-- |
-- Settings of a session.
data SessionSettings =
SessionSettings !Word32 !NominalDiffTime
-- Settings of a pool.
data PoolSettings =
PoolSettings !Int !Int
-- |
-- A smart constructor for session settings.
sessionSettings ::
Word32
-- A smart constructor for pool settings.
poolSettings ::
Int
-- ^
-- The maximum number of connections to keep open.
-- The smallest acceptable value is 1.
-- Requests for connections will block if this limit is reached.
->
NominalDiffTime
Int
-- ^
-- The amount of time for which an unused connection is kept open.
-- The smallest acceptable value is 0.5 seconds.
-- The amount of seconds for which an unused connection is kept open.
-- The smallest acceptable value is 1.
->
Maybe SessionSettings
Maybe PoolSettings
-- ^
-- Maybe session settings, if they are correct.
sessionSettings size timeout =
if size > 0 && timeout >= 0.5
then Just $ SessionSettings size timeout
-- Maybe pool settings, if they are correct.
poolSettings size timeout =
if size > 0 && timeout >= 1
then Just $ PoolSettings size timeout
else Nothing
-- ** Error
-------------------------
-- |
-- The only exception type that this API can raise.
data Error =
-- |
-- Cannot connect to a server.
CantConnect Text |
-- |
-- The connection got interrupted.
ConnectionLost Text |
-- |
-- An error returned from the database.
ErroneousResult Text |
-- |
-- Unexpected result structure.
-- Indicates usage of inappropriate statement executor.
UnexpectedResult Text |
-- |
-- Incorrect statement template.
UnparsableTemplate Text |
-- |
-- An operation,
-- which requires a database transaction was executed without one.
NotInTransaction |
-- |
-- Attempt to parse a row into an incompatible type.
-- Indicates either a mismatching schema or an incorrect query.
UnparsableRow Text
deriving (Show, Typeable, Eq, Ord)
instance Exception Error
-- * Transaction
-------------------------
-- |
-- A transaction specialized for a backend @b@,
-- associated with its intermediate results using an anonymous type-argument @s@ (same as in 'ST')
-- A transaction specialized for a backend connection @c@,
-- associated with its intermediate results using an anonymous type-argument @s@ (same trick as in 'ST')
-- and producing a result @r@.
newtype Tx b s r =
Tx (ReaderT (Backend.Connection b) IO r)
deriving (Functor, Applicative, Monad)
--
-- Running `IO` in `Tx` is prohibited.
-- The motivation is identical to `STM`:
-- the `Tx` block may get executed multiple times if any transaction conflicts arise.
-- This will result in your effectful `IO` code being executed
-- an unpredictable amount of times as well,
-- which, chances are, is not what you want.
newtype Tx c s r =
Tx (EitherT (TxError c) (Bknd.Tx c) r)
deriving (Functor, Applicative, Monad, MonadError (TxError c))
data TxError c =
-- |
-- A backend-specific connection acquisition error.
-- E.g., a failure to establish a connection.
BackendCxError (Bknd.CxError c) |
-- |
-- A backend-specific transaction error.
-- It should cover all possible failures related to an established connection,
-- including the loss of connection, query errors and database failures.
BackendTxError (Bknd.TxError c) |
-- |
-- Attempt to parse a result into an incompatible type.
-- Indicates either a mismatching schema or an incorrect query.
UnparsableResult Text
deriving instance (Show (Bknd.CxError c), Show (Bknd.TxError c)) => Show (TxError c)
deriving instance (Eq (Bknd.CxError c), Eq (Bknd.TxError c)) => Eq (TxError c)
-- |
-- A transaction mode defining how a transaction should be executed.
--
-- * @Just (isolationLevel, write)@ indicates that a database transaction
-- should be established with a specified isolation level and a boolean,
-- defining, whether it would perform any modification operations.
--
-- * @Nothing@ indicates that there should be no database transaction established on
-- the backend and therefore it should be executed with no ACID guarantees,
-- but also without any induced overhead.
type Mode =
Maybe (Backend.IsolationLevel, Bool)
-- |
-- Execute a transaction in a session.
-- Execute a transaction on a connection pool.
--
-- This function ensures on the type level,
-- that it's impossible to return @'TxListT' s m r@ from it.
tx ::
(Backend.Backend b, MonadBase IO m) =>
Mode -> (forall s. Tx b s r) -> Session b s m r
tx m t =
Session $ ReaderT $ \p -> liftBase $ Pool.withResource p $ \c -> runTx c m t
where
runTx ::
Backend b =>
Backend.Connection b -> Mode -> (forall s. Tx b s r) -> IO r
runTx connection mode (Tx reader) =
maybe (const id) inTransaction mode connection (runReaderT reader connection)
where
inTransaction ::
Backend b =>
Backend.TransactionMode -> Backend.Connection b -> IO r -> IO r
inTransaction mode c io =
let
io' =
Backend.beginTransaction mode c *> io <* Backend.finishTransaction True c
in
try io' >>= \case
Left Backend.TransactionConflict -> do
Backend.finishTransaction False c
inTransaction mode c io
Left e -> throwIO e
Right r -> return r
tx :: Bknd.CxTx c => Pool c -> Bknd.TxMode -> (forall s. Tx c s r) -> IO (Either (TxError c) r)
tx (Pool pool) mode (Tx m) =
Pool.withResource pool $ \e ->
runEitherT $ do
c <- hoistEither $ mapLeft BackendCxError e
let
attempt =
do
r <- EitherT $ fmap (either (Left . BackendTxError) Right) $
Bknd.runTx c mode $ runEitherT m
maybe attempt hoistEither r
in attempt
-- * Results Stream
-- * Statements execution
-------------------------
-- |
-- Execute a statement without processing the result.
unitTx :: Bknd.Stmt c -> Tx c s ()
unitTx =
Tx . lift . Bknd.unitTx
-- |
-- Execute a statement and count the amount of affected rows.
-- Useful for resolving how many rows were updated or deleted.
countTx :: Bknd.CxValue c Word64 => Bknd.Stmt c -> Tx c s Word64
countTx =
Tx . lift . Bknd.countTx
-- |
-- Execute a statement,
-- which produces a single result row.
-- E.g.,
-- a @SELECT@
-- or an @INSERT@, which produces a generated value (e.g., an auto-incremented id),
-- or an @UPDATE@ or @DELETE@, counting the affected rows.
maybeTx :: RowParser.RowParser c r => Bknd.Stmt c -> Tx c s (Maybe r)
maybeTx s =
Tx $ do
r <- lift $ Bknd.maybeTx s
EitherT $ return $ traverse ((mapLeft UnparsableResult) . RowParser.parseRow) $ r
-- |
-- Execute a @SELECT@ statement,
-- and produce a vector of results.
vectorTx :: RowParser.RowParser c r => Bknd.Stmt c -> Tx c s (Vector r)
vectorTx s =
Tx $ do
r <- lift $ Bknd.vectorTx s
EitherT $ return $ traverse ((mapLeft UnparsableResult) . RowParser.parseRow) $ r
-- |
-- Execute a @SELECT@ statement with a cursor,
-- and produce a result stream.
--
-- Cursor allows you to fetch virtually limitless results in a constant memory
-- at a cost of a small overhead.
-- Note that in most databases cursors require establishing a database transaction,
-- so depending on a backend the transaction may result in an error,
-- if you run it improperly.
streamTx :: RowParser.RowParser c r => Bknd.Stmt c -> Tx c s (TxListT s (Tx c s) r)
streamTx s =
Tx $ do
r <- lift $ Bknd.streamTx s
return $ TxListT $ do
row <- hoist (Tx . lift) r
lift $ Tx $ EitherT $ return $ mapLeft UnparsableResult $ RowParser.parseRow $ row
-- * Result Stream
-------------------------
-- |
@ -273,7 +244,7 @@ tx m t =
-- which uses the same trick as the 'ST' monad to associate with the
-- context transaction and become impossible to be used outside of it.
-- This lets the library ensure that it is safe to automatically
-- release all the resources associated with this stream.
-- release all the connections associated with this stream.
--
-- All the functions of the \"list-t\" library are applicable to this type,
-- amongst which are 'ListT.head', 'ListT.toList', 'ListT.fold', 'ListT.traverse_'.
@ -289,69 +260,18 @@ instance ListT.MonadTransUncons (TxListT s) where
(unsafeCoerce :: TxListT s m r -> ListT.ListT m r)
-- * Statements execution
-------------------------
-- |
-- Execute a statement, which produces no result.
unit :: Backend b => Backend.Statement b -> Tx b s ()
unit s =
Tx $ ReaderT $ Backend.execute s
-- |
-- Execute a statement and count the amount of affected rows.
-- Useful for resolving how many rows were updated or deleted.
count :: (Backend b, Backend.Mapping b Word64) => Backend.Statement b -> Tx b s Word64
count s =
Tx $ ReaderT $ Backend.executeAndCountEffects s
-- |
-- Execute a statement,
-- which produces a single result row:
-- a @SELECT@
-- or an @INSERT@, which produces a generated value (e.g., an auto-incremented id).
single :: (Backend b, RowParser b r) => Backend.Statement b -> Tx b s (Maybe r)
single s =
headMay <$> list s
-- |
-- Execute a @SELECT@ statement,
-- and produce a list of results.
list :: (Backend b, RowParser b r) => Backend.Statement b -> Tx b s [r]
list s =
Tx $ ReaderT $ \c -> do
m <- Backend.executeAndGetMatrix s c
traverse (either (throwIO . UnparsableRow) return . RowParser.parseRow) $ Vector.toList m
-- |
-- Execute a @SELECT@ statement with a cursor,
-- and produce a results stream.
--
-- Cursor allows you to fetch virtually limitless results in a constant memory
-- at a cost of a small overhead.
-- Note that in most databases cursors require establishing a database transaction,
-- so a 'NotInTransaction' error will be raised if you run it improperly.
stream :: (Backend b, RowParser b r) => Backend.Statement b -> TxListT s (Tx b s) r
stream s =
do
s <- lift $ Tx $ ReaderT $ \c -> Backend.executeAndStream s c
TxListT $ hoist (Tx . lift) $ do
row <- s
either (lift . throwIO . UnparsableRow) return $ RowParser.parseRow row
-- * Statements quasi-quotation
-------------------------
-- |
-- Produces a lambda-expression,
-- which takes as many parameters as there are placeholders in the quoted text
-- and results in a 'Backend.Statement'.
-- and results in a 'Bknd.Stmt'.
--
-- E.g.:
--
-- >selectFive :: Statement b
-- >selectFive = [q|SELECT (? + ?)|] 2 3
-- >selectSum :: Int -> Int -> Stmt c
-- >selectSum = [q|SELECT (? + ?)|]
--
q :: TH.QuasiQuoter
q =
@ -364,18 +284,17 @@ q =
parseExp s =
do
n <- either (fail . showString "Parsing failure: ") return (QParser.parse (fromString s))
return $ statementF s n
return $ statementF s (fromIntegral n)
statementF s n =
TH.LamE
(map TH.VarP argNames)
(THUtil.purify [|(,,) $(pure statementE) $(pure argsE) True|])
(THUtil.purify [|Bknd.Stmt $(pure statementE) $(pure argsE) True|])
where
argNames =
map (TH.mkName . ('_' :) . show) [1 .. n]
statementE =
TH.LitE (TH.StringL s)
argsE =
TH.ListE $ flip map argNames $ \x ->
THUtil.purify
[| Backend.renderValue $(TH.varE x) |]
argsE =
THUtil.vectorE $
map (\x -> THUtil.purify [| Bknd.encodeValue $(TH.varE x) |]) $
argNames

View File

@ -1,15 +1,13 @@
module Hasql.Prelude
(
module Exports,
bug,
bottom,
)
where
-- base-prelude
-------------------------
import BasePrelude as Exports
import BasePrelude as Exports hiding (left, right, isLeft, isRight)
-- transformers
-------------------------
@ -19,21 +17,18 @@ import Control.Monad.Trans.Class as Exports
import Control.Monad.IO.Class as Exports
import Data.Functor.Identity as Exports
-- mtl
-------------------------
import Control.Monad.Error.Class as Exports
-- mmorph
-------------------------
import Control.Monad.Morph as Exports
-- monad-control
-- either
-------------------------
import Control.Monad.Trans.Control as Exports hiding (embed, embed_)
-- transformers-base
-------------------------
import Control.Monad.Base as Exports
-- safe
-------------------------
import Safe as Exports
import Control.Monad.Trans.Either as Exports
import Data.Either.Combinators as Exports
-- list-t
-------------------------
@ -46,26 +41,3 @@ import Data.Vector as Exports (Vector)
-- text
-------------------------
import Data.Text as Exports (Text)
-- bytestring
-------------------------
import Data.ByteString as Exports (ByteString)
-- time
-------------------------
import Data.Time as Exports
-- placeholders
-------------------------
import Development.Placeholders as Exports
-- custom
-------------------------
import qualified Debug.Trace.LocationTH
bug = [e| $(Debug.Trace.LocationTH.failure) . (msg <>) |]
where
msg = "A \"hasql\" package bug: " :: String
bottom = [e| $bug "Bottom evaluated" |]

View File

@ -2,23 +2,26 @@ module Hasql.RowParser where
import Hasql.Prelude
import Language.Haskell.TH
import qualified Hasql.Backend as Backend
import qualified Hasql.Backend as Bknd
import qualified Data.Vector as Vector
import qualified Hasql.TH as THUtil
class RowParser b r where
parseRow :: Vector.Vector (Backend.Result b) -> Either Text r
-- |
-- This class is only intended to be used with the supplied instances,
-- which should be enough to cover all use cases.
class RowParser c r where
parseRow :: Bknd.ResultRow c -> Either Text r
instance RowParser b () where
instance RowParser c () where
parseRow row =
if Vector.null row
then Right ()
else Left "Not an empty row"
instance Backend.Mapping b v => RowParser b (Identity v) where
instance Bknd.CxValue c v => RowParser c (Identity v) where
parseRow row = do
Identity <$> Backend.parseResult (Vector.unsafeHead row)
Identity <$> Bknd.decodeValue (Vector.unsafeHead row)
-- Generate tuple instaces using Template Haskell:
return $ flip map [2 .. 24] $ \arity ->
@ -28,9 +31,9 @@ return $ flip map [2 .. 24] $ \arity ->
varTypes =
map VarT varNames
connectionType =
VarT (mkName "b")
VarT (mkName "c")
constraints =
map (\t -> ClassP ''Backend.Mapping [connectionType, t]) varTypes
map (\t -> ClassP ''Bknd.CxValue [connectionType, t]) varTypes
head =
AppT (AppT (ConT ''RowParser) connectionType) (foldl AppT (TupleT arity) varTypes)
parseRowDec =
@ -54,7 +57,7 @@ return $ flip map [2 .. 24] $ \arity ->
i <- [0 .. pred arity]
return $ THUtil.purify $
[|
Backend.parseResult
Bknd.decodeValue
(Vector.unsafeIndex $(varE rowVarName) $(litE (IntegerL $ fromIntegral i)) )
|]
in InstanceD constraints head [parseRowDec]

View File

@ -4,6 +4,8 @@ module Hasql.TH where
import Hasql.Prelude
import Language.Haskell.TH
import qualified Data.Vector as Vector
import qualified Data.Vector.Mutable as MVector
applicativeE :: Exp -> [Exp] -> Exp
@ -18,7 +20,50 @@ applicativeE head =
\case
e : o : t -> UInfixE e o (reduce t)
e : [] -> e
_ -> $bug $ "Unexpected queue size. Exps: " <> show exps
_ -> error $ "Unexpected queue size. Exps: " <> show exps
purify :: Q a -> a
purify = unsafePerformIO . runQ
-- |
-- Produce a lambda expression of a given arity,
-- which efficiently constructs a vector of a size equal to the arity.
vectorLamE :: Int -> Exp
vectorLamE arity =
LamE (map VarP argNames) body
where
argNames =
map (mkName . ('_' :) . show) [1 .. arity]
body =
vectorE $ map VarE argNames
vectorE :: [Exp] -> Exp
vectorE cellExps =
AppE (VarE 'runST) $ DoE $
pure vectorDeclarationStmt <> cellAssignmentStmts <> pure freezingStmt
where
vectorVarName =
mkName "v"
vectorDeclarationStmt =
(BindS
(VarP vectorVarName)
(AppE
(VarE 'MVector.unsafeNew)
(LitE (IntegerL (fromIntegral (length cellExps))))))
cellAssignmentStmts =
map (NoBindS . uncurry cellAssignmentExp) $ zip [0..] cellExps
where
cellAssignmentExp index exp =
(AppE
(AppE
(AppE
(VarE 'MVector.unsafeWrite)
(VarE vectorVarName))
(LitE (IntegerL (fromIntegral index))))
(exp))
freezingStmt =
(NoBindS
(AppE
(VarE 'Vector.unsafeFreeze)
(VarE vectorVarName)))