Skip to content

Commit 86890cc

Browse files
committed
Merge pull request #7 from helium/feature/monad-trans
Make PGTransaction a monad transformer
2 parents ad37601 + 8bf7f9a commit 86890cc

File tree

3 files changed

+95
-35
lines changed

3 files changed

+95
-35
lines changed

README.md

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,65 @@
22

33
## Summary
44

5-
`postgresql-transactional` is a simple monadic wrapper around the SQL primitives introduced by the [postgresql-simple][psqls] package. It provides simple and predictable semantics for database operations, enforces awareness of Postgres's transactional nature at API boundaries, and obviates the need for transaction boilerplate in SQL queries.
5+
`postgresql-transactional` is a simple monadic wrapper around the SQL
6+
primitives introduced by the [postgresql-simple][psqls] package. It provides
7+
simple and predictable semantics for database operations, enforces awareness of
8+
Postgres's transactional nature at API boundaries, and obviates the need for
9+
transaction boilerplate in SQL queries.
610

711
## Details
812

9-
Though the primitives provided by the [postgresql-simple][psqls] package are fast and powerful, their interface is (by design) very basic: specifically, all query functions take a shared `Connection` parameter and operate in the `IO` monad.
13+
Though the primitives provided by the [postgresql-simple][psqls] package are
14+
fast and powerful, their interface is (by design) very basic: specifically, all
15+
query functions take a shared `Connection` parameter and operate in the `IO`
16+
monad.
1017

1118
```haskell
1219
query :: FromRow r => Connection -> Query -> IO [r]
1320
execute :: ToRow q => Connection -> Query -> q -> IO Int64
1421
```
1522

16-
By virtue of the fact that (usually) all queries in a given scope are routed through a single `Connection`, we can abstract away the shared `Connection` parameter by wrapping a `ReaderT Connection` around the `IO` monad:
23+
By virtue of the fact that (usually) all queries in a given scope are routed
24+
through a single `Connection`, we can abstract away the shared `Connection`
25+
parameter by wrapping a `ReaderT Connection` in a monad transformer:
1726

1827
```haskell
19-
newtype PGTransaction a = PGTransaction (ReaderT Connection IO a)
20-
deriving (Functor, Applicative, Monad, MonadIO)
28+
newtype PGTransactionT m a =
29+
PGTransactionT (ReaderT Postgres.Connection m a)
30+
deriving (Functor, Applicative, Monad, MonadTrans, MonadIO,
31+
MonadReader Postgres.Connection)
32+
33+
type PGTransaction a = PGTransactionT IO a
2134
```
2235

36+
In the common case, the `m` parameter will simply be `IO`. The library provides
37+
the type alias `type PGTransaction a = PGTransactionT IO a` to simplify type
38+
signatures in these cases.
39+
2340
We can then reimplement our query functions in a more natural fashion:
2441

2542
```haskell
26-
query :: FromRow r => Query -> PGTransaction [a]
27-
execute :: ToRow q => Query -> q -> PGTransaction Int64
43+
query :: (FromRow r, MonadIO m) => Query -> PGTransactionT m [a]
44+
execute :: (ToRow q, MonadIO m) => Query -> q -> PGTransactionT m Int64
2845
```
2946

30-
And we can then use the [postgresql-simple][psqls] `withTransaction` function to provide `runPGTransaction`, which executes a given `PGTransaction` block with rollback semantics:
47+
And we can then use the [postgresql-simple][psqls] `withTransaction` function
48+
to provide `runPGTransaction`, which executes a given `PGTransactionT` block
49+
with rollback semantics:
3150

3251
```haskell
33-
runPGTransaction :: MonadIO m => PGTransaction a -> Postgres.Connection -> m a
52+
runPGTransaction :: MonadBaseControl IO m => PGTransactionT m a -> Postgres.Connection -> m a
3453
```
3554

55+
Use of the `MonadBaseControl IO m` constraint leaves open the option of
56+
embedding additional effects with the `m` parameter, such as logging, state, or
57+
error-handling.
58+
3659
## About
3760

38-
`postgresql-transactional` was extracted from a production Haskell project at [Helium][helium]. It is open-source software © Helium Systems, Inc., and released to the public under the terms of the MIT license.
61+
`postgresql-transactional` was extracted from a production Haskell project at
62+
[Helium][helium]. It is open-source software © Helium Systems, Inc., and
63+
released to the public under the terms of the MIT license.
3964

4065
[psqls]: https://github.com/lpsmith/postgresql-simple
4166
[helium]: https://www.helium.com

postgresql-transactional.cabal

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: postgresql-transactional
2-
version: 0.1.0.0
2+
version: 0.2.0.0
33
synopsis: a transactional monad on top of postgresql-simple
44
license: MIT
55
license-file: LICENSE
@@ -22,5 +22,6 @@ library
2222
build-depends: base >= 4 && < 5
2323
, postgresql-simple >= 0.4
2424
, mtl
25+
, monad-control
2526
-- hs-source-dirs:
2627
default-language: Haskell2010
Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{-# LANGUAGE CPP #-}
2+
{-# LANGUAGE FlexibleContexts #-}
23
{-# LANGUAGE FlexibleInstances #-}
34
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
45
{-# LANGUAGE MultiParamTypeClasses #-}
@@ -7,6 +8,7 @@ module Database.PostgreSQL.TransactionalStore
78
( PGTransaction
89
, runPGTransaction
910
, runPGTransaction'
11+
, runPGTransactionIO
1012
, query
1113
, query_
1214
, execute
@@ -21,74 +23,106 @@ module Database.PostgreSQL.TransactionalStore
2123
import Control.Applicative
2224
#endif
2325
import Control.Monad.Reader
26+
import Control.Monad.Trans.Control
2427
import Data.Int
25-
import qualified Database.PostgreSQL.Simple as Postgres
26-
import qualified Database.PostgreSQL.Simple.Transaction as Postgres.Transaction
27-
import qualified Database.PostgreSQL.Simple.Types as PGTypes
28+
import qualified Database.PostgreSQL.Simple as Postgres
2829
import Database.PostgreSQL.Simple.FromRow
2930
import Database.PostgreSQL.Simple.ToRow
31+
import qualified Database.PostgreSQL.Simple.Transaction as Postgres.Transaction
32+
import qualified Database.PostgreSQL.Simple.Types as PGTypes
3033

31-
newtype PGTransaction a =
32-
PGTransaction (ReaderT Postgres.Connection IO a)
34+
newtype PGTransactionT m a =
35+
PGTransactionT (ReaderT Postgres.Connection m a)
3336
deriving ( Functor
3437
, Applicative
3538
, Monad
36-
, MonadIO
39+
, MonadTrans
3740
, MonadReader Postgres.Connection
41+
, MonadIO
3842
)
3943

40-
runPGTransaction' :: MonadIO m
44+
type PGTransaction = PGTransactionT IO
45+
46+
runPGTransaction' :: MonadBaseControl IO m
4147
=> Postgres.Transaction.IsolationLevel
42-
-> PGTransaction a
48+
-> PGTransactionT m a
4349
-> Postgres.Connection -> m a
44-
runPGTransaction' isolation (PGTransaction pgTrans) conn =
45-
liftIO (Postgres.Transaction.withTransactionLevel isolation conn (runReaderT pgTrans conn))
50+
runPGTransaction' isolation (PGTransactionT pgTrans) conn =
51+
let runTransaction run =
52+
Postgres.Transaction.withTransactionLevel isolation conn (run pgTrans)
53+
in control runTransaction `runReaderT` conn
4654

47-
runPGTransaction :: MonadIO m => PGTransaction a -> Postgres.Connection -> m a
55+
runPGTransaction :: MonadBaseControl IO m
56+
=> PGTransactionT m a
57+
-> Postgres.Connection
58+
-> m a
4859
runPGTransaction = runPGTransaction' Postgres.Transaction.DefaultIsolationLevel
4960

61+
62+
-- | Convenience function when there are no embedded monadic effects, only IO.
63+
runPGTransactionIO :: MonadIO m
64+
=> PGTransaction a
65+
-> Postgres.Connection
66+
-> m a
67+
runPGTransactionIO = (liftIO .) . runPGTransaction
68+
69+
5070
-- | Issue an SQL query, taking a 'ToRow' input and yielding 'FromRow' outputs.
51-
query :: (ToRow input, FromRow output)
71+
query :: (ToRow input, FromRow output, MonadIO m)
5272
=> Postgres.Query
5373
-> input
54-
-> PGTransaction [output]
74+
-> PGTransactionT m [output]
5575
query q params = ask >>= (\conn -> liftIO $ Postgres.query conn q params)
5676

5777
-- | As 'query', but for queries that take no arguments.
58-
query_ :: (FromRow output) => Postgres.Query -> PGTransaction [output]
78+
query_ :: (FromRow output, MonadIO m)
79+
=> Postgres.Query
80+
-> PGTransactionT m [output]
5981
query_ q = ask >>= liftIO . (`Postgres.query_` q)
6082

6183
-- | Run a single SQL action and return success.
62-
execute :: ToRow input => Postgres.Query -> input -> PGTransaction Int64
63-
execute q params = ask >>= (\conn -> liftIO $ Postgres.execute conn q params)
84+
execute :: (ToRow input, MonadIO m)
85+
=> Postgres.Query
86+
-> input
87+
-> PGTransactionT m Int64
88+
execute q params = ask >>= (\conn -> liftIO $ Postgres.execute conn q params)
6489

65-
executeMany :: ToRow input => Postgres.Query -> [input] -> PGTransaction Int64
66-
executeMany q params = ask >>= (\conn -> liftIO $ Postgres.executeMany conn q params)
90+
executeMany :: (ToRow input, MonadIO m)
91+
=> Postgres.Query
92+
-> [input]
93+
-> PGTransactionT m Int64
94+
executeMany q params = ask >>= (\conn -> liftIO $ Postgres.executeMany conn q params)
6795

68-
returning :: (ToRow input, FromRow output)
96+
returning :: (ToRow input, FromRow output, MonadIO m)
6997
=> Postgres.Query
7098
-> [input]
71-
-> PGTransaction [output]
99+
-> PGTransactionT m [output]
72100
returning q params = ask >>= (\conn -> liftIO $ Postgres.returning conn q params)
73101

74102
-- | Run a query and return 'Just' the first result found or 'Nothing'.
75-
queryHead :: (ToRow input, FromRow output)
103+
queryHead :: (ToRow input, FromRow output, MonadIO m)
76104
=> input
77105
-> Postgres.Query
78-
-> PGTransaction (Maybe output)
106+
-> PGTransactionT m (Maybe output)
79107
queryHead params q = do
80108
results <- query q params
81109
return $ case results of
82110
(a:_) -> Just a
83111
_ -> Nothing
84112

85113
-- | Run a statement and return 'True' if only a single record was modified.
86-
executeOne :: (ToRow input) => input -> Postgres.Query -> PGTransaction Bool
114+
executeOne :: (ToRow input, MonadIO m)
115+
=> input
116+
-> Postgres.Query
117+
-> PGTransactionT m Bool
87118
executeOne params q = do
88119
results <- execute q params
89120
return (results == 1)
90121

91-
formatQuery :: ToRow q => Postgres.Query -> q -> PGTransaction Postgres.Query
122+
formatQuery :: (ToRow q, MonadIO m)
123+
=> Postgres.Query
124+
-> q
125+
-> PGTransactionT m Postgres.Query
92126
formatQuery q params = do
93127
conn <- ask
94128
liftIO (PGTypes.Query <$> Postgres.formatQuery conn q params)

0 commit comments

Comments
 (0)