Skip to content

Commit 64ca33b

Browse files
Merge pull request #29 from brandonchinn178/transaction-monad
Separate Transaction monad
2 parents eb8413e + 5fc40f5 commit 64ca33b

File tree

13 files changed

+399
-60
lines changed

13 files changed

+399
-60
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# 0.2.0.0
2+
3+
* Use a separate monad within `withTransaction` to prevent unsafe/arbitrary IO actions ([#7](https://github.com/brandonchinn178/persistent-mtl/issues/7), [#28](https://github.com/brandonchinn178/persistent-mtl/issues/28))
4+
* Add `MonadRerunnableIO` to support IO actions within `withTransaction` only if the IO action is determined to be rerunnable
5+
* Add built-in support for retrying transactions if a serialization error occurs
6+
* Remove `SqlQueryRep` as an export from `Database.Persist.Monad`. You shouldn't ever need it for normal usage. It is now re-exported by `Database.Persist.Monad.TestUtils`, since most of the usage of `SqlQueryRep` is in mocking queries. If you need it otherwise, you can import it directly from `Database.Persist.Monad.SqlQueryRep`.
7+
18
# 0.1.0.1
29

310
Fix quickstart

README.md

+56-3
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ newtype MyApp a = MyApp
5454
instance MonadUnliftIO MyApp where
5555
withRunInIO = wrappedWithRunInIO MyApp unMyApp
5656

57-
getYoungPeople :: (MonadIO m, MonadSqlQuery m) => m [Entity Person]
57+
getYoungPeople :: MonadSqlQuery m => m [Entity Person]
5858
getYoungPeople = selectList [PersonAge <. 18] []
5959

6060
main :: IO ()
61-
main = runStderrLoggingT $ withSqlitePool "db.sqlite" 5 $ \conn ->
62-
liftIO $ runSqlQueryT conn $ unMyApp $ do
61+
main = runStderrLoggingT $ withSqlitePool "db.sqlite" 5 $ \pool ->
62+
liftIO $ runSqlQueryT pool $ unMyApp $ do
6363
runMigration migrate
6464
insert_ $ Person "Alice" 25
6565
insert_ $ Person "Bob" 10
@@ -229,6 +229,59 @@ So what does `persistent-mtl` do differently?
229229

230230
In summary, `persistent-mtl` takes all the good things about option 2, implements them out of the box (so you don't have to do it yourself), and makes your business logic functions composable with transactions behaving the way YOU want.
231231

232+
### Easy transaction management
233+
234+
Some databases will throw an error if two transactions conflict (e.g. [PostgreSQL](https://www.postgresql.org/docs/9.5/transaction-iso.html)). The client is expected to retry transactions if this error is thrown. `persistent` doesn't easily support this out of the box, but `persistent-mtl` does!
235+
236+
```hs
237+
import Database.PostgreSQL.Simple.Errors (isSerializationError)
238+
239+
main :: IO ()
240+
main = withPostgresqlPool "..." 5 $ \pool -> do
241+
let env = mkSqlQueryEnv pool $ \env -> env
242+
{ retryIf = isSerializationError . fromException
243+
, retryLimit = 100 -- defaults to 10
244+
}
245+
246+
-- in any of the marked transactions below, if someone else is querying
247+
-- the postgresql database at the same time with queries that conflict
248+
-- with yours, your operations will automatically be retried
249+
runSqlQueryTWith env $ do
250+
-- transaction 1
251+
insert_ $ ...
252+
253+
-- transaction 2
254+
withTransaction $ do
255+
insert_ $ ...
256+
257+
-- transaction 2.5: transaction-within-a-transaction is supported in PostgreSQL
258+
withTransaction $ do
259+
insert_ $ ...
260+
261+
insert_ $ ...
262+
263+
-- transaction 3
264+
insert_ $ ...
265+
```
266+
267+
Because of this built-in retry support, any IO actions inside `withTransaction` have to be explicitly marked with `rerunnableIO`. If you try to use a function with a `MonadIO m` constraint, you'll get a compile-time error!
268+
269+
```
270+
.../Foo.hs:100:5: error:
271+
• Cannot run arbitrary IO actions within a transaction. If the IO action is rerunnable, use rerunnableIO
272+
• In a stmt of a 'do' block: arbitraryIO
273+
In the second argument of ‘($)’, namely
274+
‘withTransaction
275+
$ do insert_ record1
276+
arbitraryIO
277+
insert_ record2’
278+
|
279+
100 | arbitraryIO
280+
| ^^^^^^^^^^^
281+
```
282+
283+
Note that this **only** applies for transactions, so `MonadIO` and `MonadSqlQuery` constraints can still co-exist (for a function with IO actions that are not rerunnable) as long as the function is never called within `withTransaction`.
284+
232285
### Testing functions that use `persistent` operations
233286

234287
Generally, I would recommend someone using `persistent` in their application to make a monad type class containing the API for their domain, like

package.yaml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: persistent-mtl
2-
version: 0.1.0.1
2+
version: 0.2.0.0
33
maintainer: Brandon Chinn <[email protected]>
44
synopsis: Monad transformer for the persistent API
55
description: |
@@ -29,6 +29,7 @@ library:
2929
- resourcet-pool >= 0.1.0.0 && < 0.2
3030
- text >= 1.2.3.0 && < 2
3131
- transformers >= 0.5.2.0 && < 0.6
32+
- unliftio >= 0.2.7.0 && < 0.3
3233
- unliftio-core >= 0.1.2.0 && < 0.3
3334

3435
tests:

persistent-mtl.cabal

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ cabal-version: 1.12
44
--
55
-- see: https://github.com/sol/hpack
66
--
7-
-- hash: a6b1252a25af52e3ddd10e843b4818e0255c269dfbc6a399eb29e41093ef0408
7+
-- hash: 9c53e0610dea4ca814133d0596978b961b85fd54cb6df7a82eccb6d260824dde
88

99
name: persistent-mtl
10-
version: 0.1.0.1
10+
version: 0.2.0.0
1111
synopsis: Monad transformer for the persistent API
1212
description: A monad transformer and mtl-style type class for using the
1313
persistent API directly in your monad transformer stack.
@@ -32,6 +32,7 @@ source-repository head
3232

3333
library
3434
exposed-modules:
35+
Control.Monad.IO.Rerunnable
3536
Database.Persist.Monad
3637
Database.Persist.Monad.Class
3738
Database.Persist.Monad.Shim
@@ -53,6 +54,7 @@ library
5354
, resourcet-pool >=0.1.0.0 && <0.2
5455
, text >=1.2.3.0 && <2
5556
, transformers >=0.5.2.0 && <0.6
57+
, unliftio >=0.2.7.0 && <0.3
5658
, unliftio-core >=0.1.2.0 && <0.3
5759
default-language: Haskell2010
5860

scripts/generate/templates/TestHelpers.mustache

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import Data.Int (Int64)
1212
import Data.Map (Map)
1313
import Data.Text (Text)
1414
import Data.Void (Void)
15-
import Database.Persist.Sql hiding (pattern Update)
15+
import Database.Persist.Sql (CautiousMigration, Entity, Key, PersistValue, Sql)
1616

17-
import Database.Persist.Monad
17+
import Database.Persist.Monad.TestUtils (SqlQueryRep(..))
1818
import Example
1919

2020
{-# ANN module "HLint: ignore" #-}

src/Control/Monad/IO/Rerunnable.hs

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{-|
2+
Module: Control.Monad.IO.Rerunnable
3+
4+
Defines the 'MonadRerunnableIO' type class that is functionally equivalent
5+
to 'Control.Monad.IO.Class.MonadIO', but use of it requires the user to
6+
explicitly acknowledge that the given IO operation can be rerun.
7+
-}
8+
9+
module Control.Monad.IO.Rerunnable
10+
( MonadRerunnableIO(..)
11+
) where
12+
13+
import Control.Monad.Trans.Class (lift)
14+
import qualified Control.Monad.Trans.Except as Except
15+
import qualified Control.Monad.Trans.Identity as Identity
16+
import qualified Control.Monad.Trans.Maybe as Maybe
17+
import qualified Control.Monad.Trans.RWS.Lazy as RWS.Lazy
18+
import qualified Control.Monad.Trans.RWS.Strict as RWS.Strict
19+
import qualified Control.Monad.Trans.Reader as Reader
20+
import qualified Control.Monad.Trans.Resource as Resource
21+
import qualified Control.Monad.Trans.State.Lazy as State.Lazy
22+
import qualified Control.Monad.Trans.State.Strict as State.Strict
23+
import qualified Control.Monad.Trans.Writer.Lazy as Writer.Lazy
24+
import qualified Control.Monad.Trans.Writer.Strict as Writer.Strict
25+
26+
-- | A copy of 'Control.Monad.IO.Class.MonadIO' to explicitly allow only IO
27+
-- operations that are rerunnable, e.g. in the context of a SQL transaction.
28+
class Monad m => MonadRerunnableIO m where
29+
-- | Lift the given IO operation to @m@.
30+
--
31+
-- The given IO operation may be rerun, so use of this function requires
32+
-- manually verifying that the given IO operation is rerunnable.
33+
rerunnableIO :: IO a -> m a
34+
35+
instance MonadRerunnableIO IO where
36+
rerunnableIO = id
37+
38+
{- Instances for common monad transformers -}
39+
40+
instance MonadRerunnableIO m => MonadRerunnableIO (Reader.ReaderT r m) where
41+
rerunnableIO = lift . rerunnableIO
42+
43+
instance MonadRerunnableIO m => MonadRerunnableIO (Except.ExceptT e m) where
44+
rerunnableIO = lift . rerunnableIO
45+
46+
instance MonadRerunnableIO m => MonadRerunnableIO (Identity.IdentityT m) where
47+
rerunnableIO = lift . rerunnableIO
48+
49+
instance MonadRerunnableIO m => MonadRerunnableIO (Maybe.MaybeT m) where
50+
rerunnableIO = lift . rerunnableIO
51+
52+
instance (Monoid w, MonadRerunnableIO m) => MonadRerunnableIO (RWS.Lazy.RWST r w s m) where
53+
rerunnableIO = lift . rerunnableIO
54+
55+
instance (Monoid w, MonadRerunnableIO m) => MonadRerunnableIO (RWS.Strict.RWST r w s m) where
56+
rerunnableIO = lift . rerunnableIO
57+
58+
instance MonadRerunnableIO m => MonadRerunnableIO (State.Lazy.StateT s m) where
59+
rerunnableIO = lift . rerunnableIO
60+
61+
instance MonadRerunnableIO m => MonadRerunnableIO (State.Strict.StateT s m) where
62+
rerunnableIO = lift . rerunnableIO
63+
64+
instance (Monoid w, MonadRerunnableIO m) => MonadRerunnableIO (Writer.Lazy.WriterT w m) where
65+
rerunnableIO = lift . rerunnableIO
66+
67+
instance (Monoid w, MonadRerunnableIO m) => MonadRerunnableIO (Writer.Strict.WriterT w m) where
68+
rerunnableIO = lift . rerunnableIO
69+
70+
instance MonadRerunnableIO m => MonadRerunnableIO (Resource.ResourceT m) where
71+
rerunnableIO = lift . rerunnableIO

0 commit comments

Comments
 (0)