packages feed

hasql-transaction 0.8 → 0.9

raw patch · 16 files changed

+458/−347 lines, 16 filesdep +asyncdep +hasql-transactiondep +rerebasedep ~hasqlPVP ok

version bump matches the API change (PVP)

Dependencies added: async, hasql-transaction, rerebase, selective

Dependency ranges changed: hasql

API changes (from Hackage documentation)

- Hasql.Transaction: retry :: Transaction i o
+ Hasql.Transaction: instance Control.Selective.Selective Hasql.Transaction.Transaction
+ Hasql.Transaction: instance GHC.Base.Alternative Hasql.Transaction.Transaction
+ Hasql.Transaction: instance GHC.Base.Applicative Hasql.Transaction.Transaction
+ Hasql.Transaction: instance GHC.Base.Functor Hasql.Transaction.Transaction
- Hasql.Transaction: condemn :: Transaction () ()
+ Hasql.Transaction: condemn :: Transaction ()
- Hasql.Transaction: data Transaction i o
+ Hasql.Transaction: data Transaction a
- Hasql.Transaction: session :: Mode -> Level -> (i -> Session o) -> Transaction i o
+ Hasql.Transaction: session :: Mode -> Level -> Session a -> Transaction a
- Hasql.Transaction: sql :: Mode -> Level -> ByteString -> Transaction () ()
+ Hasql.Transaction: sql :: Mode -> Level -> ByteString -> Transaction ()
- Hasql.Transaction: statement :: Mode -> Level -> Statement i o -> Transaction i o
+ Hasql.Transaction: statement :: Mode -> Level -> Statement i o -> i -> Transaction o
- Hasql.Transaction: transact :: Transaction i o -> i -> Session o
+ Hasql.Transaction: transact :: Transaction a -> Session a

Files

+ conflicts-test/Main.hs view
@@ -0,0 +1,99 @@+module Main where++import Prelude+import qualified Hasql.Connection as A+import qualified Hasql.Session as B+import qualified Hasql.Transaction as C+import qualified Main.Statements as D+import qualified Main.Transactions as E+import qualified Control.Concurrent.Async as F+++main =+  bracket acquire release use+  where+    acquire =+      (,) <$> acquire <*> acquire+      where+        acquire =+          join $+          fmap (either (fail . show) return) $+          A.acquire connectionSettings+          where+            connectionSettings =+              A.settings "localhost" 5432 "postgres" "" "postgres"+    release (connection1, connection2) =+      do+        transact connection1 E.dropSchema+        A.release connection1+        A.release connection2+    use (connection1, connection2) =+      do+        try (transact connection1 E.dropSchema) :: IO (Either SomeException ())+        transact connection1 E.createSchema+        success <- fmap and (traverse runTest tests)+        if success+          then exitSuccess+          else exitFailure+      where+        runTest test =+          test connection1 connection2+        tests =+          [readAndWriteTransactionsTest, transactionsTest, transactionAndQueryTest]+++session connection session =+  B.run session connection >>=+  either (fail . show) return++transact connection transaction =+  session connection (C.transact transaction)+++type Test =+  A.Connection -> A.Connection -> IO Bool++transactionsTest :: Test+transactionsTest connection1 connection2 =+  do+    id1 <- session connection1 (B.statement 0 D.createAccount)+    id2 <- session connection1 (B.statement 0 D.createAccount)+    async1 <- F.async (replicateM_ 1000 (transact connection1 (E.transfer id1 id2 1)))+    async2 <- F.async (replicateM_ 1000 (transact connection2 (E.transfer id1 id2 1)))+    F.wait async1+    F.wait async2+    balance1 <- session connection1 (B.statement id1 D.getBalance)+    balance2 <- session connection1 (B.statement id2 D.getBalance)+    traceShowM balance1+    traceShowM balance2+    return (balance1 == Just 2000 && balance2 == Just (-2000))++readAndWriteTransactionsTest :: Test+readAndWriteTransactionsTest connection1 connection2 =+  do+    id1 <- session connection1 (B.statement 0 D.createAccount)+    id2 <- session connection1 (B.statement 0 D.createAccount)+    async1 <- F.async (replicateM_ 1000 (transact connection1 (E.transfer id1 id2 1)))+    async2 <- F.async (replicateM_ 1000 (transact connection2 (E.getBalance id1)))+    F.wait async1+    F.wait async2+    balance1 <- session connection1 (B.statement id1 D.getBalance)+    balance2 <- session connection1 (B.statement id2 D.getBalance)+    traceShowM balance1+    traceShowM balance2+    return (balance1 == Just 1000 && balance2 == Just (-1000))++transactionAndQueryTest :: Test+transactionAndQueryTest connection1 connection2 =+  do+    id1 <- session connection1 (B.statement 0 D.createAccount)+    id2 <- session connection1 (B.statement 0 D.createAccount)+    async1 <- F.async (transact connection1 (E.transferTimes 200 id1 id2 1))+    async2 <- F.async (session connection2 (replicateM_ 200 (B.statement (id1, 1) D.modifyBalance)))+    F.wait async1+    F.wait async2+    balance1 <- session connection1 (B.statement id1 D.getBalance)+    balance2 <- session connection1 (B.statement id2 D.getBalance)+    traceShowM balance1+    traceShowM balance2+    return (balance1 == Just 400 && balance2 == Just (-200))
+ conflicts-test/Main/Statements.hs view
@@ -0,0 +1,47 @@+module Main.Statements where++import Prelude+import Hasql.Statement+import qualified Hasql.Encoders as E+import qualified Hasql.Decoders as D+++createAccountTable :: Statement () ()+createAccountTable =+  Statement sql E.noParams D.noResult False+  where+    sql =+      "create table account (id serial not null, balance numeric not null, primary key (id))"++dropAccountTable :: Statement () ()+dropAccountTable =+  Statement+    "drop table account"+    E.noParams+    D.noResult+    False++createAccount :: Statement Scientific Int64+createAccount =+  Statement+    "insert into account (balance) values ($1) returning id"+    ((E.param . E.nonNullable) E.numeric)+    (D.singleRow ((D.column . D.nonNullable) D.int8))+    True++modifyBalance :: Statement (Int64, Scientific) Bool+modifyBalance =+  Statement+    "update account set balance = balance + $2 where id = $1"+    (contrazip2 ((E.param . E.nonNullable) E.int8) ((E.param . E.nonNullable) E.numeric))+    (fmap (> 0) D.rowsAffected)+    True++getBalance :: Statement Int64 (Maybe Scientific)+getBalance =+  Statement+    "select balance from account where id = $1"+    ((E.param . E.nonNullable) E.int8)+    (D.rowMaybe ((D.column . D.nonNullable) D.numeric))+    True+
+ conflicts-test/Main/Transactions.hs view
@@ -0,0 +1,25 @@+module Main.Transactions where++import Prelude+import Hasql.Transaction+import qualified Main.Statements as A+++createSchema :: Transaction ()+createSchema = statement Write Serializable A.createAccountTable ()++dropSchema :: Transaction ()+dropSchema = statement Write Serializable A.dropAccountTable ()++transfer :: Int64 -> Int64 -> Scientific -> Transaction Bool+transfer id1 id2 amount = do+  ifS+    (statement Write Serializable A.modifyBalance (id1, amount))+    (statement Write Serializable A.modifyBalance (id2, negate amount))+    (pure False)++transferTimes :: Int -> Int64 -> Int64 -> Scientific -> Transaction ()+transferTimes times id1 id2 amount = replicateM_ times (transfer id1 id2 amount)++getBalance :: Int64 -> Transaction (Maybe Scientific)+getBalance id = statement Read ReadCommitted A.getBalance id
hasql-transaction.cabal view
@@ -1,7 +1,7 @@ name: hasql-transaction-version: 0.8+version: 0.9 category: Hasql, Database, PostgreSQL-synopsis: A composable abstraction over the retryable transactions for Hasql+synopsis: Composable abstraction over retryable transactions for Hasql homepage: https://github.com/nikita-volkov/hasql-transaction bug-reports: https://github.com/nikita-volkov/hasql-transaction/issues author: Nikita Volkov <nikita.y.volkov@mail.ru>@@ -23,13 +23,11 @@   exposed-modules:     Hasql.Transaction   other-modules:+    Hasql.Transaction.Builders     Hasql.Transaction.Prelude-    Hasql.Transaction.Requisites.Model-    Hasql.Transaction.Requisites.Builders-    Hasql.Transaction.Requisites.Statements-    Hasql.Transaction.Requisites.Sessions-    Hasql.Transaction.Session-    Hasql.Transaction.Transaction+    Hasql.Transaction.Sessions+    Hasql.Transaction.Statements+    Hasql.Transaction.Types   build-depends:     base >=4.9 && <5,     bytestring >=0.10 && <0.11,@@ -40,4 +38,24 @@     mtl >=2 && <3,     profunctors >=5 && <6,     transformers >=0.3 && <0.6,+    selective >=0.3 && <0.4,     semigroupoids >=5.3.2 && <6++test-suite conflicts-test+  type: exitcode-stdio-1.0+  hs-source-dirs: conflicts-test+  default-extensions: Arrows, BangPatterns, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFoldable, DeriveFunctor, DeriveGeneric, DeriveTraversable, EmptyDataDecls, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, LambdaCase, LiberalTypeSynonyms, MagicHash, MultiParamTypeClasses, MultiWayIf, NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, PatternGuards, ParallelListComp, QuasiQuotes, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, TemplateHaskell, TupleSections, TypeFamilies, TypeOperators, UnboxedTuples+  default-language: Haskell2010+  main-is: Main.hs+  other-modules:+    Main.Statements+    Main.Transactions+  ghc-options:+    -O2+    -threaded+    "-with-rtsopts=-N"+  build-depends:+    async >=2.1 && <3,+    hasql-transaction,+    hasql,+    rerebase >=1.4 && <2
library/Hasql/Transaction.hs view
@@ -1,17 +1,16 @@ {-|-Arrow DSL for composition of transactions with automated conflict resolution.+DSL for composition of transactions with automated conflict resolution. -} module Hasql.Transaction (-  {-* Session -}-  Session.transact,+  {-* Sessions -}+  transact,   {-* Transaction -}   Transaction,   statement,   sql,   session,   condemn,-  retry,   {-* Settings -}   Mode(..),   Level(..),@@ -23,25 +22,120 @@   * `Reexports.Statement`   -}   module Reexports,-  {-* FAQ -}-  {-|-  == Why does transaction have to be an arrow, why is monad or applicative not enough?--  Arrow allows us to determine the read mode and isolation level,-  while composing transactions in such a way that one can depend on the result of the other.-  -  A monadic interface wouldn't allow us to do the first, namely:-  to compose the modes and levels. -  -  An applicative interface wouldn't allow the second:-  to make a transaction depend on the result of the other.-  For details see the docs on the `Transaction` type.-  -} ) where -import Hasql.Transaction.Requisites.Model-import Hasql.Transaction.Transaction+import Hasql.Transaction.Prelude+import Hasql.Transaction.Types import qualified Hasql.Session as Reexports (Session) import qualified Hasql.Statement as Reexports (Statement)-import qualified Hasql.Transaction.Session as Session+import qualified Hasql.Session as Session+import qualified Hasql.Statement as Statement+import qualified Hasql.Transaction.Sessions as Sessions+++{-|+Execute transaction in session.+-}+transact :: Transaction a -> Session.Session a+transact (Transaction mode level list) =+  Sessions.inAlternatingTransaction mode level $+  fmap (flip runStateT Uncondemned) list+++{-|+Composable transaction providing for automated conflict resolution.++Mode and level is associated with the transaction,+which makes them participate in composition.+In a composed transaction they become the strictest of the ones+associated with the transactions that constitute it.+This allows you to safely compose transactions with different+ACID guarantees.++It cannot have an instance of Monad,+because it makes it impossible to implement composition of+mode and level associated with consituent transactions.+It still however is possible to compose transactions+in such a way that the result of a transaction is used+to decide which transaction to execute next,+thanks to the recently discovered `Selective` interface.++Supports alternative branching,+where the alternative gets executed in case of a transaction conflict.+-}+data Transaction a = Transaction Mode Level [StateT Condemnation Session.Session a]++deriving instance Functor Transaction++instance Applicative Transaction where+  pure = Transaction minBound minBound . pure . pure+  (<*>) (Transaction mode1 level1 list1) (Transaction mode2 level2 list2) =+    Transaction (max mode1 mode2) (max level1 level2) ((<*>) <$> list1 <*> list2)++instance Selective Transaction where+  select (Transaction mode1 level1 list1) (Transaction mode2 level2 list2) =+    Transaction (max mode1 mode2) (max level1 level2) (select <$> list1 <*> list2)++instance Alternative Transaction where+  empty = Transaction minBound minBound []+  (<|>) (Transaction mode1 level1 list1) (Transaction mode2 level2 list2) =+    Transaction (max mode1 mode2) (max level1 level2) (list1 <> list2)++{-|+Execute a single statement under mode and level.++__Warning:__ The statement must not be transaction-related+like @BEGIN@, @COMMIT@ or @ABORT@, otherwise you'll break the abstraction.+-}+statement :: Mode -> Level -> Statement.Statement i o -> i -> Transaction o+statement mode level statement i = session mode level $ Session.statement i statement++{-|+Execute a possibly multistatement SQL string under a mode and level.+SQL strings cannot be dynamically parameterized or produce a result.++__Warning:__ SQL must not be transaction-related+like @BEGIN@, @COMMIT@ or @ABORT@, otherwise you'll break the abstraction.+-}+sql :: Mode -> Level -> ByteString -> Transaction ()+sql mode level sql = session mode level $ Session.sql sql++{-|+Execute a composition of statements under the same mode and level.++__Warning:__++1. You must know that it is possible to break the abstraction,+if you execute statements such as @BEGIN@ inside of the session.++1. For the same reason you cannot execute other transactions inside of that session.++1. You must beware that in case of conflicts any IO code that you may lift+into session will get executed multiple times.+This is the way the automatic conflict resolution works:+the transaction gets retried, when a conflict arises.+So be cautious about doing any mutations or rocket launches in that IO!+Simply pinging for things such as current time is totally fine though.+Still it's not recommended because it's often a symptom of bad application design.++Due to the mentioned it's highly advised to keep all the session code+inside of the definition of a transaction.+Thus you'll be guaranteed to have control over what's going on inside of the+executed session and it will not be possible for this code+to be affected by any outside changes or used elsewhere.+-}+session :: Mode -> Level -> Session.Session a -> Transaction a+session mode level session = Transaction mode level [lift session]++{-|+Cause transaction to eventually roll back.++This allows to perform some transactional actions,+collecting their results, and decide,+whether to commit the introduced changes to the DB+based on those results,+as well as emit those results outside of the transaction.+-}+condemn :: Transaction ()+condemn = Transaction minBound minBound [put Condemned]
+ library/Hasql/Transaction/Builders.hs view
@@ -0,0 +1,22 @@+module Hasql.Transaction.Builders+where++import Hasql.Transaction.Prelude+import Hasql.Transaction.Types+import ByteString.StrictBuilder+++beginTransaction :: Mode -> Level -> Builder+beginTransaction mode_ level_ =+  "begin " <> level level_ <> " " <> mode mode_++level :: Level -> Builder+level = \ case+  ReadCommitted -> "isolation level read committed"+  RepeatableRead -> "isolation level repeatable read"+  Serializable -> "isolation level serializable"++mode :: Mode -> Builder+mode = \ case+  Write -> "read write"+  Read -> "read only"
library/Hasql/Transaction/Prelude.hs view
@@ -53,7 +53,7 @@ import Foreign.Ptr as Exports import Foreign.StablePtr as Exports import Foreign.Storable as Exports hiding (sizeOf, alignment)-import GHC.Conc as Exports hiding (withMVar, threadWaitWriteSTM, threadWaitWrite, threadWaitReadSTM, threadWaitRead)+import GHC.Conc as Exports hiding (orElse, withMVar, threadWaitWriteSTM, threadWaitWrite, threadWaitReadSTM, threadWaitRead) import GHC.Exts as Exports (lazy, inline, sortWith, groupWith, IsList(..)) import GHC.Generics as Exports (Generic, Generic1) import GHC.IO.Exception as Exports@@ -72,6 +72,10 @@ import Text.Printf as Exports (printf, hPrintf) import Text.Read as Exports (Read(..), readMaybe, readEither) import Unsafe.Coerce as Exports++-- selective+-------------------------+import Control.Selective as Exports  -- transformers -------------------------
− library/Hasql/Transaction/Requisites/Builders.hs
@@ -1,22 +0,0 @@-module Hasql.Transaction.Requisites.Builders-where--import Hasql.Transaction.Prelude-import Hasql.Transaction.Requisites.Model-import ByteString.StrictBuilder---beginTransaction :: Mode -> Level -> Builder-beginTransaction mode_ level_ =-  "begin " <> level level_ <> " " <> mode mode_--level :: Level -> Builder-level = \ case-  ReadCommitted -> "isolation level read committed"-  RepeatableRead -> "isolation level repeatable read"-  Serializable -> "isolation level serializable"--mode :: Mode -> Builder-mode = \ case-  Write -> "read write"-  Read -> "read only"
− library/Hasql/Transaction/Requisites/Model.hs
@@ -1,36 +0,0 @@-module Hasql.Transaction.Requisites.Model-where--import Hasql.Transaction.Prelude---{-|-Execution mode: either read or write.--}-data Mode =-  {-|-  Read-only. No writes possible.-  -}-  Read |-  {-|-  Write and commit.-  -}-  Write-  deriving (Show, Eq, Ord, Enum, Bounded)--{-|-Transaction isolation level.--For reference see-<http://www.postgresql.org/docs/current/static/transaction-iso.html the Postgres' documentation>.--}-data Level =-  ReadCommitted |-  RepeatableRead |-  Serializable-  deriving (Show, Eq, Ord, Enum, Bounded)--data Condemnation =-  Condemned |-  Uncondemned-  deriving (Show, Eq, Ord, Enum, Bounded)
− library/Hasql/Transaction/Requisites/Sessions.hs
@@ -1,60 +0,0 @@-{-|--TODO:-We may want to-do one transaction retry in case of the 23505 error, and fail if an identical-error is seen.--}-module Hasql.Transaction.Requisites.Sessions-where--import Hasql.Transaction.Prelude-import Hasql.Transaction.Requisites.Model-import Hasql.Session-import qualified Hasql.Transaction.Requisites.Statements as Statements---inRetryingTransaction :: Mode -> Level -> Session (a, Condemnation) -> Session a-inRetryingTransaction mode isolation session =-  fix $ \ recur -> catchError normal (onError recur)-  where-    normal =-      do-        statement () (Statements.beginTransaction mode isolation)-        (result, condemnation) <- session-        case condemnation of-          Uncondemned -> statement () Statements.commitTransaction-          Condemned -> statement () Statements.abortTransaction-        return result-    onError continue error =-      do-        statement () Statements.abortTransaction-        case error of-          QueryError _ _ (ResultError (ServerError "40001" _ _ _)) -> continue-          _ -> throwError error--inAlternatingTransaction :: Mode -> Level -> [Session (a, Condemnation)] -> Session a-inAlternatingTransaction mode level sessions =-  let-    loop = \ case-      session : sessionsTail -> tryTransaction mode level session >>= \ case-        Just a -> return a-        Nothing -> loop sessionsTail-      _ -> loop sessions-    in loop sessions--tryTransaction :: Mode -> Level -> Session (a, Condemnation) -> Session (Maybe a)-tryTransaction mode level session = do-  statement () (Statements.beginTransaction mode level)-  catchError-    (do-      (result, condemnation) <- session-      case condemnation of-        Uncondemned -> statement () Statements.commitTransaction-        Condemned -> statement () Statements.abortTransaction-      return (Just result))-    (\ error -> do-      statement () Statements.abortTransaction-      case error of-        QueryError _ _ (ResultError (ServerError "40001" _ _ _)) -> return Nothing-        error -> throwError error)
− library/Hasql/Transaction/Requisites/Statements.hs
@@ -1,23 +0,0 @@-module Hasql.Transaction.Requisites.Statements-where--import Hasql.Transaction.Prelude-import Hasql.Transaction.Requisites.Model-import Hasql.Statement-import qualified ByteString.StrictBuilder as Builder-import qualified Hasql.Encoders as B-import qualified Hasql.Decoders as C-import qualified Hasql.Transaction.Requisites.Builders as Builders---beginTransaction :: Mode -> Level -> Statement () ()-beginTransaction mode level =-  Statement (Builder.builderBytes (Builders.beginTransaction mode level)) B.noParams C.noResult True--commitTransaction :: Statement () ()-commitTransaction =-  Statement "commit" B.noParams C.noResult True--abortTransaction :: Statement () ()-abortTransaction =-  Statement "abort" B.noParams C.noResult True
− library/Hasql/Transaction/Session.hs
@@ -1,21 +0,0 @@-{-|-Extensions to Session.-Can be used imported in the same namespace as @Hasql.Session@ without conflicts.--}-module Hasql.Transaction.Session-where--import Hasql.Transaction.Prelude-import Hasql.Transaction.Requisites.Model-import Hasql.Transaction.Requisites.Sessions-import Hasql.Session-import qualified Hasql.Transaction.Transaction as Transaction-import qualified Hasql.Transaction.Transaction as Reexports (Transaction)---{-|-Execute an alternating transaction arrow providing an input for it.--}-transact :: Transaction.Transaction i o -> i -> Session o-transact (Transaction.Transaction mode level list) i =-  inAlternatingTransaction mode level (fmap (\ fn -> runStateT (fn i) Uncondemned) list)
+ library/Hasql/Transaction/Sessions.hs view
@@ -0,0 +1,60 @@+{-|++TODO:+We may want to+do one transaction retry in case of the 23505 error, and fail if an identical+error is seen.+-}+module Hasql.Transaction.Sessions+where++import Hasql.Transaction.Prelude+import Hasql.Transaction.Types+import Hasql.Session+import qualified Hasql.Transaction.Statements as Statements+++inRetryingTransaction :: Mode -> Level -> Session (a, Condemnation) -> Session a+inRetryingTransaction mode isolation session =+  fix $ \ recur -> catchError normal (onError recur)+  where+    normal =+      do+        statement () (Statements.beginTransaction mode isolation)+        (result, condemnation) <- session+        case condemnation of+          Uncondemned -> statement () Statements.commitTransaction+          Condemned -> statement () Statements.abortTransaction+        return result+    onError continue error =+      do+        statement () Statements.abortTransaction+        case error of+          QueryError _ _ (ResultError (ServerError "40001" _ _ _)) -> continue+          _ -> throwError error++inAlternatingTransaction :: Mode -> Level -> [Session (a, Condemnation)] -> Session a+inAlternatingTransaction mode level sessions =+  let+    loop = \ case+      session : sessionsTail -> tryTransaction mode level session >>= \ case+        Just a -> return a+        Nothing -> loop sessionsTail+      _ -> loop sessions+    in loop sessions++tryTransaction :: Mode -> Level -> Session (a, Condemnation) -> Session (Maybe a)+tryTransaction mode level session = do+  statement () (Statements.beginTransaction mode level)+  catchError+    (do+      (result, condemnation) <- session+      case condemnation of+        Uncondemned -> statement () Statements.commitTransaction+        Condemned -> statement () Statements.abortTransaction+      return (Just result))+    (\ error -> do+      statement () Statements.abortTransaction+      case error of+        QueryError _ _ (ResultError (ServerError "40001" _ _ _)) -> return Nothing+        error -> throwError error)
+ library/Hasql/Transaction/Statements.hs view
@@ -0,0 +1,23 @@+module Hasql.Transaction.Statements+where++import Hasql.Transaction.Prelude+import Hasql.Transaction.Types+import Hasql.Statement+import qualified ByteString.StrictBuilder as Builder+import qualified Hasql.Encoders as B+import qualified Hasql.Decoders as C+import qualified Hasql.Transaction.Builders as Builders+++beginTransaction :: Mode -> Level -> Statement () ()+beginTransaction mode level =+  Statement (Builder.builderBytes (Builders.beginTransaction mode level)) B.noParams C.noResult True++commitTransaction :: Statement () ()+commitTransaction =+  Statement "commit" B.noParams C.noResult True++abortTransaction :: Statement () ()+abortTransaction =+  Statement "abort" B.noParams C.noResult True
− library/Hasql/Transaction/Transaction.hs
@@ -1,155 +0,0 @@-module Hasql.Transaction.Transaction where--import Hasql.Transaction.Prelude hiding (map, retry)-import Hasql.Transaction.Requisites.Model-import Hasql.Session (Session)-import Hasql.Statement (Statement)-import qualified Hasql.Session as Session---{-|-Composable transaction providing for automated conflict resolution-with input @i@ and output @o@. Supports alternative branching.--Mode and level is associated with the transaction,-which makes them participate in composition.-In a composed transaction they become the strictest of the ones-associated with the transactions that constitute it.-This allows you to safely compose transactions with different-ACID guarantees.--}-data Transaction i o =-  Transaction Mode Level [i -> StateT Condemnation Session o]--deriving instance Functor (Transaction i)--instance Applicative (Transaction i) where-  pure = Transaction Read ReadCommitted . pure . const . pure-  (<*>) = binOp $ \ lSession rSession i -> lSession i <*> rSession i--instance Alternative (Transaction i) where-  empty = retry-  (<|>) (Transaction lMode lLevel lList) (Transaction rMode rLevel rList) =-    Transaction (max lMode rMode) (max lLevel rLevel) (lList <> rList)--instance Profunctor Transaction where-  dimap fn1 fn2 = map $ \ session -> fmap fn2 . session . fn1--instance Strong Transaction where-  first' = first-  second' = second--instance Choice Transaction where-  left' = left-  right' = right--instance Semigroupoid Transaction where-  o = binOp (<=<)--instance Category Transaction where-  id = Transaction Read ReadCommitted []-  (.) = o--instance Arrow Transaction where-  arr fn = Transaction Read ReadCommitted (return (return . fn))-  (***) = binOp $ \ lSession rSession (li, ri) -> (,) <$> lSession li <*> rSession ri--instance ArrowChoice Transaction where-  (+++) = binOp $ \ lSession rSession -> either (fmap Left . lSession) (fmap Right . rSession)--instance ArrowZero Transaction where-  zeroArrow = retry--instance ArrowPlus Transaction where-  (<+>) = (<|>)--{-|-Because mode and isolation are always composed the same way,-we can focus on composing just the sessions.--}-{-# INLINE binOp #-}-binOp ::-  (-    (li -> StateT Condemnation Session lo) ->-    (ri -> StateT Condemnation Session ro) ->-    (i -> StateT Condemnation Session o)-  ) ->-  Transaction li lo -> Transaction ri ro -> Transaction i o-binOp composeSessions (Transaction lMode lLevel lList) (Transaction rMode rLevel rList) = let-  mode = max lMode rMode-  level = max lLevel rLevel-  list = composeSessions <$> lList <*> rList-  in Transaction mode level list--{-# INLINE map #-}-map ::-  ((i1 -> StateT Condemnation Session o1) -> (i2 -> StateT Condemnation Session o2)) ->-  Transaction i1 o1 -> Transaction i2 o2-map sessionFn (Transaction mode isolation list) = Transaction mode isolation (fmap sessionFn list)--{-|-Cause transaction to eventually roll back.--This allows to perform some transactional actions,-collecting their results, and decide,-whether to commit the introduced changes to the DB-based on those results,-as well as emit those results outside of the transaction.--}-condemn :: Transaction () ()-condemn = Transaction Read ReadCommitted [\ _ -> put Condemned]--{-|-Execute a possibly multistatement SQL string under a mode and level.-SQL strings cannot be dynamically parameterized or produce a result.--__Warning:__ SQL must not be transaction-related-like `BEGIN`, `COMMIT` or `ABORT`, otherwise you'll break the abstraction.--}-sql :: Mode -> Level -> ByteString -> Transaction () ()-sql mode level sql = session mode level $ \ _ -> Session.sql sql--{-|-Execute a single statement under a mode and level.--__Warning:__ The statement must not be transaction-related-like `BEGIN`, `COMMIT` or `ABORT`, otherwise you'll break the abstraction.--}-statement :: Mode -> Level -> Statement i o -> Transaction i o-statement mode level statement = session mode level $ \ i -> Session.statement i statement--{-|-Execute a composition of statements under the same mode and level.--__Warning:__--1. You must know that it is possible to break the abstraction,-if you execute statements such as `BEGIN` inside of the session.--1. For the same reason you cannot execute other transactions inside of that session.--1. You must beware that in case of conflicts any IO code that you may lift-into session will get executed multiple times.-This is the way the automatic conflict resolution works:-the transaction gets retried, when a conflict arises.-So be cautious about doing any mutations or rocket launches in that IO!-Simply pinging for things such as current time is totally fine though.-Still it's not recommended because it's often a symptom of bad application design.--Due to the mentioned it's highly advised to keep all the session code-inside of the definition of a transaction.-Thus you'll be guaranteed to have control over what's going on inside of the-executed session and it will not be possible for this code-to be affected by any outside changes or used elsewhere.--}-session :: Mode -> Level -> (i -> Session o) -> Transaction i o-session mode level sessionFn = Transaction mode level [lift . sessionFn]--{-|-Fail the alternation branch, retrying with the other. Same as `empty` and `zeroArrow`.--Beware that if all your alternatives end up being a retry,-you'll get yourself a perfect infinite loop.--}-retry :: Transaction i o-retry = Transaction Read ReadCommitted []
+ library/Hasql/Transaction/Types.hs view
@@ -0,0 +1,36 @@+module Hasql.Transaction.Types+where++import Hasql.Transaction.Prelude+++{-|+Execution mode: either read or write.+-}+data Mode =+  {-|+  Read-only. No writes possible.+  -}+  Read |+  {-|+  Write and commit.+  -}+  Write+  deriving (Show, Eq, Ord, Enum, Bounded)++{-|+Transaction isolation level.++For reference see+<http://www.postgresql.org/docs/current/static/transaction-iso.html the Postgres' documentation>.+-}+data Level =+  ReadCommitted |+  RepeatableRead |+  Serializable+  deriving (Show, Eq, Ord, Enum, Bounded)++data Condemnation =+  Condemned |+  Uncondemned+  deriving (Show, Eq, Ord, Enum, Bounded)