diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+# 1.2
+
+- Removed the `unpreparedTransaction` session because the same effects can now be achieved via the connection settings in Hasql
+
+# 1.1
+
+- Add automatic retry on deadlock errors (code 40P01)
diff --git a/conflicts-test/Main.hs b/conflicts-test/Main.hs
deleted file mode 100644
--- a/conflicts-test/Main.hs
+++ /dev/null
@@ -1,100 +0,0 @@
-module Main where
-
-import qualified Control.Concurrent.Async as F
-import qualified Hasql.Connection as A
-import qualified Hasql.Session as B
-import qualified Hasql.Transaction as C
-import qualified Hasql.Transaction.Sessions as G
-import qualified Main.Statements as D
-import qualified Main.Transactions as E
-import Prelude
-
-main :: IO ()
-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" "postgres"
-    release (connection1, connection2) =
-      do
-        transaction connection1 E.dropSchema
-        A.release connection1
-        A.release connection2
-    use (connection1, connection2) =
-      do
-        try (transaction connection1 E.dropSchema) :: IO (Either SomeException ())
-        transaction 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 :: A.Connection -> B.Session a -> IO a
-session connection session =
-  B.run session connection
-    >>= either (fail . show) return
-
-transaction :: A.Connection -> C.Transaction a -> IO a
-transaction connection transaction =
-  session connection (G.transaction G.RepeatableRead G.Write 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 (transaction connection1 (E.transfer id1 id2 1)))
-    async2 <- F.async (replicateM_ 1000 (transaction 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 (transaction connection1 (E.transfer id1 id2 1)))
-    async2 <- F.async (replicateM_ 1000 (transaction connection2 (C.statement id1 D.getBalance)))
-    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 (transaction 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))
diff --git a/conflicts-test/Main/Statements.hs b/conflicts-test/Main/Statements.hs
deleted file mode 100644
--- a/conflicts-test/Main/Statements.hs
+++ /dev/null
@@ -1,46 +0,0 @@
-module Main.Statements where
-
-import Contravariant.Extras
-import qualified Hasql.Decoders as D
-import qualified Hasql.Encoders as E
-import Hasql.Statement
-import Prelude
-
-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
diff --git a/conflicts-test/Main/Transactions.hs b/conflicts-test/Main/Transactions.hs
deleted file mode 100644
--- a/conflicts-test/Main/Transactions.hs
+++ /dev/null
@@ -1,27 +0,0 @@
-module Main.Transactions where
-
-import Hasql.Transaction
-import qualified Main.Statements as A
-import Prelude
-
-createSchema :: Transaction ()
-createSchema =
-  do
-    statement () A.createAccountTable
-
-dropSchema :: Transaction ()
-dropSchema =
-  do
-    statement () A.dropAccountTable
-
-transfer :: Int64 -> Int64 -> Scientific -> Transaction Bool
-transfer id1 id2 amount =
-  do
-    success <- statement (id1, amount) A.modifyBalance
-    if success
-      then statement (id2, negate amount) A.modifyBalance
-      else return False
-
-transferTimes :: Int -> Int64 -> Int64 -> Scientific -> Transaction ()
-transferTimes times id1 id2 amount =
-  replicateM_ times (transfer id1 id2 amount)
diff --git a/hasql-transaction.cabal b/hasql-transaction.cabal
--- a/hasql-transaction.cabal
+++ b/hasql-transaction.cabal
@@ -1,30 +1,30 @@
-name:          hasql-transaction
-version:       1.0.1.4
-category:      Hasql, Database, PostgreSQL
+cabal-version: 3.0
+name: hasql-transaction
+version: 1.2.2
+category: Hasql, Database, PostgreSQL
 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>
-maintainer:    Nikita Volkov <nikita.y.volkov@mail.ru>
-copyright:     (c) 2015, Nikita Volkov
-license:       MIT
-license-file:  LICENSE
-build-type:    Simple
-cabal-version: >=1.10
+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>
+maintainer: Nikita Volkov <nikita.y.volkov@mail.ru>
+copyright: (c) 2015, Nikita Volkov
+license: MIT
+license-file: LICENSE
+extra-source-files: CHANGELOG.md
+tested-with: ghc ==9.8.2 || ==8.10.1
 
 source-repository head
-  type:     git
-  location: git://github.com/nikita-volkov/hasql-transaction.git
+  type: git
+  location: https://github.com/nikita-volkov/hasql-transaction
 
-library
-  hs-source-dirs:     library
+common base
+  default-language: Haskell2010
   default-extensions:
-    NoImplicitPrelude
-    NoMonomorphismRestriction
-    Arrows
+    ApplicativeDo
     BangPatterns
+    BlockArguments
     ConstraintKinds
     DataKinds
     DefaultSignatures
@@ -33,32 +33,51 @@
     DeriveFunctor
     DeriveGeneric
     DeriveTraversable
+    DerivingVia
     EmptyDataDecls
     FlexibleContexts
     FlexibleInstances
     FunctionalDependencies
     GADTs
     GeneralizedNewtypeDeriving
+    ImportQualifiedPost
     LambdaCase
     LiberalTypeSynonyms
-    MagicHash
     MultiParamTypeClasses
     MultiWayIf
+    NoImplicitPrelude
+    NoMonomorphismRestriction
     OverloadedStrings
-    ParallelListComp
     PatternGuards
     QuasiQuotes
     RankNTypes
     RecordWildCards
+    RoleAnnotations
     ScopedTypeVariables
     StandaloneDeriving
-    TemplateHaskell
+    StrictData
     TupleSections
     TypeFamilies
     TypeOperators
-    UnboxedTuples
 
-  default-language:   Haskell2010
+common executable
+  import: base
+  ghc-options:
+    -O2
+    -threaded
+    -with-rtsopts=-N
+    -rtsopts
+    -funbox-strict-fields
+
+common test
+  import: base
+  ghc-options:
+    -threaded
+    -with-rtsopts=-N
+
+library
+  import: base
+  hs-source-dirs: src/library
   exposed-modules:
     Hasql.Transaction
     Hasql.Transaction.Sessions
@@ -66,72 +85,37 @@
   other-modules:
     Hasql.Transaction.Config
     Hasql.Transaction.Private.Prelude
-    Hasql.Transaction.Private.Sessions
     Hasql.Transaction.Private.SQL
+    Hasql.Transaction.Private.Sessions
     Hasql.Transaction.Private.Statements
     Hasql.Transaction.Private.Transaction
 
   build-depends:
-      base >=4.12 && <5
-    , bytestring >=0.10 && <0.13
-    , bytestring-tree-builder >=0.2.7.8 && <0.3
-    , contravariant >=1.3 && <2
-    , contravariant-extras >=0.3 && <0.4
-    , hasql >=1.6 && <1.7
-    , mtl >=2.2 && <3
-    , transformers >=0.5 && <0.7
+    base >=4.12 && <5,
+    bytestring >=0.10 && <0.13,
+    bytestring-tree-builder >=0.2.7.8 && <0.3,
+    contravariant >=1.3 && <2,
+    hasql >=1.10 && <1.11,
+    mtl >=2.2 && <3,
+    text >=1 && <3,
+    transformers >=0.5 && <0.7,
 
 test-suite conflicts-test
-  type:               exitcode-stdio-1.0
-  hs-source-dirs:     conflicts-test
-  default-extensions:
-    NoImplicitPrelude
-    NoMonomorphismRestriction
-    Arrows
-    BangPatterns
-    ConstraintKinds
-    DataKinds
-    DefaultSignatures
-    DeriveDataTypeable
-    DeriveFoldable
-    DeriveFunctor
-    DeriveGeneric
-    DeriveTraversable
-    EmptyDataDecls
-    FlexibleContexts
-    FlexibleInstances
-    FunctionalDependencies
-    GADTs
-    GeneralizedNewtypeDeriving
-    LambdaCase
-    LiberalTypeSynonyms
-    MagicHash
-    MultiParamTypeClasses
-    MultiWayIf
-    OverloadedStrings
-    ParallelListComp
-    PatternGuards
-    QuasiQuotes
-    RankNTypes
-    RecordWildCards
-    ScopedTypeVariables
-    StandaloneDeriving
-    TemplateHaskell
-    TupleSections
-    TypeFamilies
-    TypeOperators
-    UnboxedTuples
-
-  default-language:   Haskell2010
-  main-is:            Main.hs
+  import: test
+  type: exitcode-stdio-1.0
+  hs-source-dirs: src/conflicts-test
+  main-is: Main.hs
   other-modules:
     Main.Statements
     Main.Transactions
 
-  ghc-options:        -O2 -threaded -with-rtsopts=-N
+  ghc-options:
+    -O2
+    -threaded
+    -with-rtsopts=-N
+
   build-depends:
-      async >=2.1 && <3
-    , contravariant-extras >=0.3 && <0.4
-    , hasql
-    , hasql-transaction
-    , rerebase >=1.11 && <2
+    async >=2.1 && <3,
+    hasql >=1.9,
+    hasql-transaction,
+    rerebase >=1.11 && <2,
diff --git a/library/Hasql/Transaction.hs b/library/Hasql/Transaction.hs
deleted file mode 100644
--- a/library/Hasql/Transaction.hs
+++ /dev/null
@@ -1,12 +0,0 @@
--- |
--- An API for declaration of transactions.
-module Hasql.Transaction
-  ( -- * Transaction monad
-    Transaction,
-    condemn,
-    sql,
-    statement,
-  )
-where
-
-import Hasql.Transaction.Private.Transaction
diff --git a/library/Hasql/Transaction/Config.hs b/library/Hasql/Transaction/Config.hs
deleted file mode 100644
--- a/library/Hasql/Transaction/Config.hs
+++ /dev/null
@@ -1,21 +0,0 @@
-module Hasql.Transaction.Config where
-
-import Hasql.Transaction.Private.Prelude
-
-data Mode
-  = -- |
-    -- Read-only. No writes possible.
-    Read
-  | -- |
-    -- Write and commit.
-    Write
-  deriving (Show, Eq, Ord, Enum, Bounded)
-
--- |
--- For reference see
--- <http://www.postgresql.org/docs/current/static/transaction-iso.html the Postgres' documentation>.
-data IsolationLevel
-  = ReadCommitted
-  | RepeatableRead
-  | Serializable
-  deriving (Show, Eq, Ord, Enum, Bounded)
diff --git a/library/Hasql/Transaction/Private/Prelude.hs b/library/Hasql/Transaction/Private/Prelude.hs
deleted file mode 100644
--- a/library/Hasql/Transaction/Private/Prelude.hs
+++ /dev/null
@@ -1,81 +0,0 @@
-module Hasql.Transaction.Private.Prelude
-  ( module Exports,
-    tryError,
-  )
-where
-
-import Contravariant.Extras as Exports
-import Control.Applicative as Exports
-import Control.Arrow as Exports
-import Control.Category as Exports
-import Control.Concurrent as Exports
-import Control.Exception as Exports
-import Control.Monad as Exports hiding (fail, forM, forM_, join, mapM, mapM_, msum, sequence, sequence_)
-import Control.Monad.Error.Class as Exports (MonadError (..))
-import Control.Monad.Fail as Exports
-import Control.Monad.Fix as Exports hiding (fix)
-import Control.Monad.IO.Class as Exports
-import Control.Monad.ST as Exports
-import Control.Monad.Trans.Class as Exports
-import Control.Monad.Trans.Maybe as Exports hiding (liftListen, liftPass)
-import Control.Monad.Trans.Reader as Exports hiding (liftCallCC, liftCatch)
-import Control.Monad.Trans.State.Strict as Exports hiding (liftCallCC, liftCatch, liftListen, liftPass)
-import Data.Bits as Exports
-import Data.Bool as Exports
-import Data.ByteString as Exports (ByteString)
-import Data.Char as Exports
-import Data.Coerce as Exports
-import Data.Complex as Exports
-import Data.Data as Exports
-import Data.Dynamic as Exports
-import Data.Either as Exports
-import Data.Fixed as Exports
-import Data.Foldable as Exports hiding (toList)
-import Data.Function as Exports hiding (id, (.))
-import Data.Functor as Exports hiding (unzip)
-import Data.Functor.Contravariant as Exports
-import Data.Functor.Contravariant.Divisible as Exports
-import Data.Functor.Identity as Exports
-import Data.IORef as Exports
-import Data.Int as Exports
-import Data.Ix as Exports
-import Data.List as Exports hiding (all, and, any, concat, concatMap, elem, find, foldl, foldl', foldl1, foldr, foldr1, isSubsequenceOf, mapAccumL, mapAccumR, maximum, maximumBy, minimum, minimumBy, notElem, or, product, sortOn, sum, uncons)
-import Data.Maybe as Exports
-import Data.Monoid as Exports hiding (Alt, First (..), Last (..), (<>))
-import Data.Ord as Exports
-import Data.Proxy as Exports
-import Data.Ratio as Exports
-import Data.STRef as Exports
-import Data.Semigroup as Exports
-import Data.String as Exports
-import Data.Traversable as Exports
-import Data.Tuple as Exports
-import Data.Unique as Exports
-import Data.Version as Exports
-import Data.Word as Exports
-import Debug.Trace as Exports
-import Foreign.ForeignPtr as Exports
-import Foreign.Ptr as Exports
-import Foreign.StablePtr as Exports
-import Foreign.Storable as Exports hiding (alignment, sizeOf)
-import GHC.Conc as Exports hiding (threadWaitRead, threadWaitReadSTM, threadWaitWrite, threadWaitWriteSTM, withMVar)
-import GHC.Exts as Exports (IsList (..), groupWith, inline, lazy, sortWith)
-import GHC.Generics as Exports (Generic, Generic1)
-import GHC.IO.Exception as Exports
-import Numeric as Exports
-import System.Environment as Exports
-import System.Exit as Exports
-import System.IO as Exports
-import System.IO.Error as Exports
-import System.IO.Unsafe as Exports
-import System.Mem as Exports
-import System.Mem.StableName as Exports
-import System.Timeout as Exports
-import Text.Printf as Exports (hPrintf, printf)
-import Text.Read as Exports (Read (..), readEither, readMaybe)
-import Unsafe.Coerce as Exports
-import Prelude as Exports hiding (all, and, any, concat, concatMap, elem, fail, foldl, foldl1, foldr, foldr1, id, mapM, mapM_, maximum, minimum, notElem, or, product, sequence, sequence_, sum, (.))
-
-tryError :: (MonadError e m) => m a -> m (Either e a)
-tryError m =
-  catchError (liftM Right m) (return . Left)
diff --git a/library/Hasql/Transaction/Private/SQL.hs b/library/Hasql/Transaction/Private/SQL.hs
deleted file mode 100644
--- a/library/Hasql/Transaction/Private/SQL.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-module Hasql.Transaction.Private.SQL where
-
-import qualified ByteString.TreeBuilder as D
-import Hasql.Transaction.Config
-import Hasql.Transaction.Private.Prelude
-
-beginTransaction :: IsolationLevel -> Mode -> ByteString
-beginTransaction isolation mode =
-  D.toByteString builder
-  where
-    builder =
-      "BEGIN " <> isolationBuilder <> " " <> modeBuilder
-      where
-        isolationBuilder =
-          case isolation of
-            ReadCommitted -> "ISOLATION LEVEL READ COMMITTED"
-            RepeatableRead -> "ISOLATION LEVEL REPEATABLE READ"
-            Serializable -> "ISOLATION LEVEL SERIALIZABLE"
-        modeBuilder =
-          case mode of
-            Write -> "READ WRITE"
-            Read -> "READ ONLY"
-
-declareCursor :: ByteString -> ByteString -> ByteString
-declareCursor name sql =
-  D.toByteString
-    $ "DECLARE "
-    <> D.byteString name
-    <> " NO SCROLL CURSOR FOR "
-    <> D.byteString sql
diff --git a/library/Hasql/Transaction/Private/Sessions.hs b/library/Hasql/Transaction/Private/Sessions.hs
deleted file mode 100644
--- a/library/Hasql/Transaction/Private/Sessions.hs
+++ /dev/null
@@ -1,43 +0,0 @@
-module Hasql.Transaction.Private.Sessions where
-
-import Hasql.Session
-import Hasql.Transaction.Config
-import Hasql.Transaction.Private.Prelude
-import qualified Hasql.Transaction.Private.Statements as Statements
-
-{-
-We may want to
-do one transaction retry in case of the 23505 error, and fail if an identical
-error is seen.
--}
-inRetryingTransaction :: IsolationLevel -> Mode -> Session (a, Bool) -> Bool -> Session a
-inRetryingTransaction level mode session preparable =
-  fix $ \retry -> do
-    attemptRes <- tryTransaction level mode session preparable
-    case attemptRes of
-      Just a -> return a
-      Nothing -> retry
-
-tryTransaction :: IsolationLevel -> Mode -> Session (a, Bool) -> Bool -> Session (Maybe a)
-tryTransaction level mode body preparable = do
-  statement () (Statements.beginTransaction level mode preparable)
-
-  bodyRes <- catchError (fmap Just body) $ \error -> do
-    statement () (Statements.abortTransaction preparable)
-    handleTransactionError error $ return Nothing
-
-  case bodyRes of
-    Just (res, commit) -> catchError (commitOrAbort commit preparable $> Just res) $ \error -> do
-      handleTransactionError error $ return Nothing
-    Nothing -> return Nothing
-
-commitOrAbort :: Bool -> Bool -> Session ()
-commitOrAbort commit preparable =
-  if commit
-    then statement () (Statements.commitTransaction preparable)
-    else statement () (Statements.abortTransaction preparable)
-
-handleTransactionError :: QueryError -> Session a -> Session a
-handleTransactionError error onTransactionError = case error of
-  QueryError _ _ (ResultError (ServerError "40001" _ _ _ _)) -> onTransactionError
-  error -> throwError error
diff --git a/library/Hasql/Transaction/Private/Statements.hs b/library/Hasql/Transaction/Private/Statements.hs
deleted file mode 100644
--- a/library/Hasql/Transaction/Private/Statements.hs
+++ /dev/null
@@ -1,45 +0,0 @@
-module Hasql.Transaction.Private.Statements where
-
-import qualified Hasql.Decoders as C
-import qualified Hasql.Encoders as B
-import qualified Hasql.Statement as A
-import Hasql.Transaction.Config
-import Hasql.Transaction.Private.Prelude
-import qualified Hasql.Transaction.Private.SQL as D
-
--- * Transactions
-
-beginTransaction :: IsolationLevel -> Mode -> Bool -> A.Statement () ()
-beginTransaction isolation mode preparable =
-  A.Statement (D.beginTransaction isolation mode) B.noParams C.noResult preparable
-
-commitTransaction :: Bool -> A.Statement () ()
-commitTransaction preparable =
-  A.Statement "COMMIT" B.noParams C.noResult preparable
-
-abortTransaction :: Bool -> A.Statement () ()
-abortTransaction preparable =
-  A.Statement "ABORT" B.noParams C.noResult preparable
-
--- * Streaming
-
-declareCursor :: ByteString -> ByteString -> B.Params a -> A.Statement a ()
-declareCursor name sql encoder =
-  A.Statement (D.declareCursor name sql) encoder C.noResult False
-
-closeCursor :: A.Statement ByteString ()
-closeCursor =
-  A.Statement "CLOSE $1" ((B.param . B.nonNullable) B.bytea) C.noResult True
-
-fetchFromCursor :: (b -> a -> b) -> b -> C.Row a -> A.Statement (Int64, ByteString) b
-fetchFromCursor step init rowDec =
-  A.Statement sql encoder decoder True
-  where
-    sql =
-      "FETCH FORWARD $1 FROM $2"
-    encoder =
-      contrazip2
-        ((B.param . B.nonNullable) B.int8)
-        ((B.param . B.nonNullable) B.bytea)
-    decoder =
-      C.foldlRows step init rowDec
diff --git a/library/Hasql/Transaction/Private/Transaction.hs b/library/Hasql/Transaction/Private/Transaction.hs
deleted file mode 100644
--- a/library/Hasql/Transaction/Private/Transaction.hs
+++ /dev/null
@@ -1,53 +0,0 @@
-module Hasql.Transaction.Private.Transaction where
-
-import qualified Hasql.Session as B
-import qualified Hasql.Statement as A
-import Hasql.Transaction.Config
-import Hasql.Transaction.Private.Prelude
-import qualified Hasql.Transaction.Private.Sessions as D
-
--- |
--- A composable abstraction over the retryable transactions.
---
--- Executes multiple queries under the specified mode and isolation level,
--- while automatically retrying the transaction in case of conflicts.
--- Thus this abstraction closely reproduces the behaviour of 'STM'.
-newtype Transaction a
-  = Transaction (StateT Bool B.Session a)
-  deriving (Functor, Applicative, Monad)
-
-instance (Semigroup a) => Semigroup (Transaction a) where
-  (<>) = liftA2 (<>)
-
-instance (Monoid a) => Monoid (Transaction a) where
-  mempty = pure mempty
-
--- |
--- Execute the transaction using the provided isolation level and mode.
-{-# INLINE run #-}
-run :: Transaction a -> IsolationLevel -> Mode -> Bool -> B.Session a
-run (Transaction session) isolation mode preparable =
-  D.inRetryingTransaction isolation mode (runStateT session True) preparable
-
--- |
--- Possibly a multi-statement query,
--- which however cannot be parameterized or prepared,
--- nor can any results of it be collected.
-{-# INLINE sql #-}
-sql :: ByteString -> Transaction ()
-sql =
-  Transaction . lift . B.sql
-
--- |
--- Parameters and a specification of the parametric query to apply them to.
-{-# INLINE statement #-}
-statement :: a -> A.Statement a b -> Transaction b
-statement params statement =
-  Transaction . lift $ B.statement params statement
-
--- |
--- Cause transaction to eventually roll back.
-{-# INLINE condemn #-}
-condemn :: Transaction ()
-condemn =
-  Transaction $ put False
diff --git a/library/Hasql/Transaction/Sessions.hs b/library/Hasql/Transaction/Sessions.hs
deleted file mode 100644
--- a/library/Hasql/Transaction/Sessions.hs
+++ /dev/null
@@ -1,31 +0,0 @@
-module Hasql.Transaction.Sessions
-  ( transaction,
-    unpreparedTransaction,
-
-    -- * Transaction settings
-    C.Mode (..),
-    C.IsolationLevel (..),
-  )
-where
-
-import Data.Bool
-import qualified Hasql.Session as B
-import qualified Hasql.Transaction.Config as C
-import qualified Hasql.Transaction.Private.Transaction as A
-
--- |
--- Execute the transaction using the provided isolation level and mode.
-{-# INLINE transaction #-}
-transaction :: C.IsolationLevel -> C.Mode -> A.Transaction a -> B.Session a
-transaction isolation mode transaction =
-  A.run transaction isolation mode True
-
--- |
--- Execute the transaction using the provided isolation level and mode,
--- and specifying that the generated BEGIN, COMMIT and ABORT statements should not be prepared.
---
--- Helps with transaction pooling due to its incompatibility with prepared statements.
-{-# INLINE unpreparedTransaction #-}
-unpreparedTransaction :: C.IsolationLevel -> C.Mode -> A.Transaction a -> B.Session a
-unpreparedTransaction isolation mode transaction =
-  A.run transaction isolation mode False
diff --git a/src/conflicts-test/Main.hs b/src/conflicts-test/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/conflicts-test/Main.hs
@@ -0,0 +1,121 @@
+module Main where
+
+import Control.Concurrent.Async qualified as F
+import Hasql.Connection qualified as A
+import Hasql.Connection.Settings qualified as H
+import Hasql.Session qualified as B
+import Hasql.Transaction qualified as C
+import Hasql.Transaction.Sessions qualified as G
+import Main.Statements qualified as D
+import Main.Transactions qualified as E
+import Prelude
+
+main :: IO ()
+main =
+  bracket acquire release use
+  where
+    acquire =
+      (,) <$> acquire <*> acquire
+      where
+        acquire =
+          join
+            $ fmap (either (fail . show) return)
+            $ A.acquire connectionSettings
+          where
+            connectionSettings =
+              H.hostAndPort "localhost" 5432
+                <> H.user "postgres"
+                <> H.password "postgres"
+                <> H.dbname "postgres"
+    release (connection1, connection2) =
+      do
+        transaction connection1 E.dropSchema
+        A.release connection1
+        A.release connection2
+    use (connection1, connection2) =
+      do
+        try (transaction connection1 E.dropSchema) :: IO (Either SomeException ())
+        transaction connection1 E.createSchema
+        success <- fmap and (traverse runTest tests)
+        if success
+          then exitSuccess
+          else exitFailure
+      where
+        runTest test =
+          test connection1 connection2
+        tests =
+          [readAndWriteTransactionsTest, transactionsTest, transactionsNoRetryTest, transactionAndQueryTest]
+
+session :: A.Connection -> B.Session a -> IO a
+session connection session =
+  A.use connection session
+    >>= either (fail . show) return
+
+transaction :: A.Connection -> C.Transaction a -> IO a
+transaction connection transaction =
+  session connection (G.transaction G.RepeatableRead G.Write transaction)
+
+transactionNoRetry :: A.Connection -> C.Transaction a -> IO a
+transactionNoRetry connection transaction =
+  session connection (G.transactionNoRetry G.RepeatableRead G.Write 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 (transaction connection1 (E.transfer id1 id2 1)))
+    async2 <- F.async (replicateM_ 1000 (transaction 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))
+
+transactionsNoRetryTest :: Test
+transactionsNoRetryTest connection1 connection2 =
+  do
+    id1 <- session connection1 (B.statement 0 D.createAccount)
+    id2 <- session connection1 (B.statement 0 D.createAccount)
+    async1 <- F.async (replicateM_ 1000 (transactionNoRetry connection1 (E.transfer id1 id2 1)))
+    async2 <- F.async (replicateM_ 1000 (transactionNoRetry connection2 (E.transfer id1 id2 1)))
+    result1 <- F.waitCatch async1
+    result2 <- F.waitCatch async2
+    let serialError = sequenceA [result1, result2]
+    traceShowM serialError
+    return $ either (("40001" `isInfixOf`) . show) (pure False) serialError
+
+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 (transaction connection1 (E.transfer id1 id2 1)))
+    async2 <- F.async (replicateM_ 1000 (transaction connection2 (C.statement id1 D.getBalance)))
+    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 (transaction 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))
diff --git a/src/conflicts-test/Main/Statements.hs b/src/conflicts-test/Main/Statements.hs
new file mode 100644
--- /dev/null
+++ b/src/conflicts-test/Main/Statements.hs
@@ -0,0 +1,41 @@
+module Main.Statements where
+
+import Hasql.Decoders qualified as D
+import Hasql.Encoders qualified as E
+import Hasql.Statement
+import Prelude
+
+createAccountTable :: Statement () ()
+createAccountTable =
+  unpreparable sql E.noParams D.noResult
+  where
+    sql =
+      "create table account (id bigserial not null, balance numeric not null, primary key (id))"
+
+dropAccountTable :: Statement () ()
+dropAccountTable =
+  unpreparable
+    "drop table account"
+    E.noParams
+    D.noResult
+
+createAccount :: Statement Scientific Int64
+createAccount =
+  preparable
+    "insert into account (balance) values ($1) returning id"
+    ((E.param . E.nonNullable) E.numeric)
+    (D.singleRow ((D.column . D.nonNullable) D.int8))
+
+modifyBalance :: Statement (Int64, Scientific) Bool
+modifyBalance =
+  preparable
+    "update account set balance = balance + $2 where id = $1"
+    ((fst >$< (E.param . E.nonNullable) E.int8) <> (snd >$< (E.param . E.nonNullable) E.numeric))
+    (fmap (> 0) D.rowsAffected)
+
+getBalance :: Statement Int64 (Maybe Scientific)
+getBalance =
+  preparable
+    "select balance from account where id = $1"
+    ((E.param . E.nonNullable) E.int8)
+    (D.rowMaybe ((D.column . D.nonNullable) D.numeric))
diff --git a/src/conflicts-test/Main/Transactions.hs b/src/conflicts-test/Main/Transactions.hs
new file mode 100644
--- /dev/null
+++ b/src/conflicts-test/Main/Transactions.hs
@@ -0,0 +1,27 @@
+module Main.Transactions where
+
+import Hasql.Transaction
+import Main.Statements qualified as A
+import Prelude
+
+createSchema :: Transaction ()
+createSchema =
+  do
+    statement () A.createAccountTable
+
+dropSchema :: Transaction ()
+dropSchema =
+  do
+    statement () A.dropAccountTable
+
+transfer :: Int64 -> Int64 -> Scientific -> Transaction Bool
+transfer id1 id2 amount =
+  do
+    success <- statement (id1, amount) A.modifyBalance
+    if success
+      then statement (id2, negate amount) A.modifyBalance
+      else return False
+
+transferTimes :: Int -> Int64 -> Int64 -> Scientific -> Transaction ()
+transferTimes times id1 id2 amount =
+  replicateM_ times (transfer id1 id2 amount)
diff --git a/src/library/Hasql/Transaction.hs b/src/library/Hasql/Transaction.hs
new file mode 100644
--- /dev/null
+++ b/src/library/Hasql/Transaction.hs
@@ -0,0 +1,12 @@
+-- |
+-- An API for declaration of transactions.
+module Hasql.Transaction
+  ( -- * Transaction monad
+    Transaction,
+    condemn,
+    sql,
+    statement,
+  )
+where
+
+import Hasql.Transaction.Private.Transaction
diff --git a/src/library/Hasql/Transaction/Config.hs b/src/library/Hasql/Transaction/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/library/Hasql/Transaction/Config.hs
@@ -0,0 +1,21 @@
+module Hasql.Transaction.Config where
+
+import Hasql.Transaction.Private.Prelude
+
+data Mode
+  = -- |
+    -- Read-only. No writes possible.
+    Read
+  | -- |
+    -- Write and commit.
+    Write
+  deriving (Show, Eq, Ord, Enum, Bounded)
+
+-- |
+-- For reference see
+-- <http://www.postgresql.org/docs/current/static/transaction-iso.html the Postgres' documentation>.
+data IsolationLevel
+  = ReadCommitted
+  | RepeatableRead
+  | Serializable
+  deriving (Show, Eq, Ord, Enum, Bounded)
diff --git a/src/library/Hasql/Transaction/Private/Prelude.hs b/src/library/Hasql/Transaction/Private/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/src/library/Hasql/Transaction/Private/Prelude.hs
@@ -0,0 +1,80 @@
+module Hasql.Transaction.Private.Prelude
+  ( module Exports,
+    tryError,
+  )
+where
+
+import Control.Applicative as Exports
+import Control.Arrow as Exports
+import Control.Category as Exports
+import Control.Concurrent as Exports
+import Control.Exception as Exports
+import Control.Monad as Exports hiding (fail, forM, forM_, join, mapM, mapM_, msum, sequence, sequence_)
+import Control.Monad.Error.Class as Exports (MonadError (..))
+import Control.Monad.Fail as Exports
+import Control.Monad.Fix as Exports hiding (fix)
+import Control.Monad.IO.Class as Exports
+import Control.Monad.ST as Exports
+import Control.Monad.Trans.Class as Exports
+import Control.Monad.Trans.Maybe as Exports hiding (liftListen, liftPass)
+import Control.Monad.Trans.Reader as Exports hiding (liftCallCC, liftCatch)
+import Control.Monad.Trans.State.Strict as Exports hiding (liftCallCC, liftCatch, liftListen, liftPass)
+import Data.Bits as Exports
+import Data.Bool as Exports
+import Data.ByteString as Exports (ByteString)
+import Data.Char as Exports
+import Data.Coerce as Exports
+import Data.Complex as Exports
+import Data.Data as Exports
+import Data.Dynamic as Exports
+import Data.Either as Exports
+import Data.Fixed as Exports
+import Data.Foldable as Exports hiding (toList)
+import Data.Function as Exports hiding (id, (.))
+import Data.Functor as Exports hiding (unzip)
+import Data.Functor.Contravariant as Exports
+import Data.Functor.Contravariant.Divisible as Exports
+import Data.Functor.Identity as Exports
+import Data.IORef as Exports
+import Data.Int as Exports
+import Data.Ix as Exports
+import Data.List as Exports hiding (all, and, any, concat, concatMap, elem, find, foldl, foldl', foldl1, foldr, foldr1, isSubsequenceOf, mapAccumL, mapAccumR, maximum, maximumBy, minimum, minimumBy, notElem, or, product, sortOn, sum, uncons)
+import Data.Maybe as Exports
+import Data.Monoid as Exports hiding (Alt, First (..), Last (..), (<>))
+import Data.Ord as Exports
+import Data.Proxy as Exports
+import Data.Ratio as Exports
+import Data.STRef as Exports
+import Data.Semigroup as Exports
+import Data.String as Exports
+import Data.Traversable as Exports
+import Data.Tuple as Exports
+import Data.Unique as Exports
+import Data.Version as Exports
+import Data.Word as Exports
+import Debug.Trace as Exports
+import Foreign.ForeignPtr as Exports
+import Foreign.Ptr as Exports
+import Foreign.StablePtr as Exports
+import Foreign.Storable as Exports hiding (alignment, sizeOf)
+import GHC.Conc as Exports hiding (threadWaitRead, threadWaitReadSTM, threadWaitWrite, threadWaitWriteSTM, withMVar)
+import GHC.Exts as Exports (IsList (..), groupWith, inline, lazy, sortWith)
+import GHC.Generics as Exports (Generic, Generic1)
+import GHC.IO.Exception as Exports
+import Numeric as Exports
+import System.Environment as Exports
+import System.Exit as Exports
+import System.IO as Exports
+import System.IO.Error as Exports
+import System.IO.Unsafe as Exports
+import System.Mem as Exports
+import System.Mem.StableName as Exports
+import System.Timeout as Exports
+import Text.Printf as Exports (hPrintf, printf)
+import Text.Read as Exports (Read (..), readEither, readMaybe)
+import Unsafe.Coerce as Exports
+import Prelude as Exports hiding (all, and, any, concat, concatMap, elem, fail, foldl, foldl1, foldr, foldr1, id, mapM, mapM_, maximum, minimum, notElem, or, product, sequence, sequence_, sum, (.))
+
+tryError :: (MonadError e m) => m a -> m (Either e a)
+tryError m =
+  catchError (liftM Right m) (return . Left)
diff --git a/src/library/Hasql/Transaction/Private/SQL.hs b/src/library/Hasql/Transaction/Private/SQL.hs
new file mode 100644
--- /dev/null
+++ b/src/library/Hasql/Transaction/Private/SQL.hs
@@ -0,0 +1,22 @@
+module Hasql.Transaction.Private.SQL where
+
+import ByteString.TreeBuilder qualified as D
+import Hasql.Transaction.Config
+import Hasql.Transaction.Private.Prelude
+
+beginTransaction :: IsolationLevel -> Mode -> ByteString
+beginTransaction isolation mode =
+  D.toByteString builder
+  where
+    builder =
+      "BEGIN " <> isolationBuilder <> " " <> modeBuilder
+      where
+        isolationBuilder =
+          case isolation of
+            ReadCommitted -> "ISOLATION LEVEL READ COMMITTED"
+            RepeatableRead -> "ISOLATION LEVEL REPEATABLE READ"
+            Serializable -> "ISOLATION LEVEL SERIALIZABLE"
+        modeBuilder =
+          case mode of
+            Write -> "READ WRITE"
+            Read -> "READ ONLY"
diff --git a/src/library/Hasql/Transaction/Private/Sessions.hs b/src/library/Hasql/Transaction/Private/Sessions.hs
new file mode 100644
--- /dev/null
+++ b/src/library/Hasql/Transaction/Private/Sessions.hs
@@ -0,0 +1,55 @@
+module Hasql.Transaction.Private.Sessions where
+
+import Hasql.Errors (ServerError (..), SessionError (..), StatementError (..))
+import Hasql.Session
+import Hasql.Transaction.Config
+import Hasql.Transaction.Private.Prelude
+import Hasql.Transaction.Private.Statements qualified as Statements
+
+{-
+We may want to
+do one transaction retry in case of the 23505 error, and fail if an identical
+error is seen.
+-}
+inRetryingTransaction :: IsolationLevel -> Mode -> Bool -> Session (a, Bool) -> Session a
+inRetryingTransaction level mode retryOnError session =
+  fix $ \retry -> do
+    attemptRes <- tryTransaction level mode retryOnError session
+    case attemptRes of
+      Just a -> return a
+      Nothing -> retry
+
+tryTransaction :: IsolationLevel -> Mode -> Bool -> Session (a, Bool) -> Session (Maybe a)
+tryTransaction level mode retryOnError body = do
+  statement () (Statements.beginTransaction level mode)
+
+  bodyRes <- catchError (fmap Just body) $ \error -> do
+    statement () Statements.abortTransaction
+    handleTransactionError error retryOnError $ return Nothing
+
+  case bodyRes of
+    Just (res, commit) -> catchError (commitOrAbort commit $> Just res) $ \error -> do
+      handleTransactionError error retryOnError $ return Nothing
+    Nothing -> return Nothing
+
+commitOrAbort :: Bool -> Session ()
+commitOrAbort commit =
+  if commit
+    then statement () Statements.commitTransaction
+    else statement () Statements.abortTransaction
+
+handleTransactionError :: SessionError -> Bool -> Session a -> Session a
+handleTransactionError error retryOnError onTransactionError = case error of
+  StatementSessionError _ _ _ _ _ statementError -> onStatementError statementError
+  ScriptSessionError _ serverError -> onServerError serverError
+  _ -> throwError error
+  where
+    retryOrThrow = if retryOnError then onTransactionError else throwError error
+    onStatementError = \case
+      ServerStatementError serverError -> onServerError serverError
+      _ -> throwError error
+    onServerError (ServerError code _ _ _ _) =
+      case code of
+        "40001" -> retryOrThrow
+        "40P01" -> retryOrThrow
+        _ -> throwError error
diff --git a/src/library/Hasql/Transaction/Private/Statements.hs b/src/library/Hasql/Transaction/Private/Statements.hs
new file mode 100644
--- /dev/null
+++ b/src/library/Hasql/Transaction/Private/Statements.hs
@@ -0,0 +1,20 @@
+module Hasql.Transaction.Private.Statements where
+
+import Data.Text.Encoding qualified as Text
+import Hasql.Decoders qualified as C
+import Hasql.Encoders qualified as B
+import Hasql.Statement qualified as A
+import Hasql.Transaction.Config
+import Hasql.Transaction.Private.SQL qualified as D
+
+beginTransaction :: IsolationLevel -> Mode -> A.Statement () ()
+beginTransaction isolation mode =
+  A.preparable (Text.decodeUtf8 (D.beginTransaction isolation mode)) B.noParams C.noResult
+
+commitTransaction :: A.Statement () ()
+commitTransaction =
+  A.preparable "COMMIT" B.noParams C.noResult
+
+abortTransaction :: A.Statement () ()
+abortTransaction =
+  A.preparable "ABORT" B.noParams C.noResult
diff --git a/src/library/Hasql/Transaction/Private/Transaction.hs b/src/library/Hasql/Transaction/Private/Transaction.hs
new file mode 100644
--- /dev/null
+++ b/src/library/Hasql/Transaction/Private/Transaction.hs
@@ -0,0 +1,54 @@
+module Hasql.Transaction.Private.Transaction where
+
+import Data.Text.Encoding qualified as Text
+import Hasql.Session qualified as B
+import Hasql.Statement qualified as A
+import Hasql.Transaction.Config
+import Hasql.Transaction.Private.Prelude
+import Hasql.Transaction.Private.Sessions qualified as D
+
+-- |
+-- A composable abstraction over the retryable transactions.
+--
+-- Executes multiple queries under the specified mode and isolation level,
+-- while automatically retrying the transaction in case of conflicts.
+-- Thus this abstraction closely reproduces the behaviour of 'STM'.
+newtype Transaction a
+  = Transaction (StateT Bool B.Session a)
+  deriving (Functor, Applicative, Monad)
+
+instance (Semigroup a) => Semigroup (Transaction a) where
+  (<>) = liftA2 (<>)
+
+instance (Monoid a) => Monoid (Transaction a) where
+  mempty = pure mempty
+
+-- |
+-- Execute the transaction using the provided isolation level and mode.
+{-# INLINE run #-}
+run :: Transaction a -> IsolationLevel -> Mode -> Bool -> B.Session a
+run (Transaction session) isolation mode retryOnError =
+  D.inRetryingTransaction isolation mode retryOnError (runStateT session True)
+
+-- |
+-- Possibly a multi-statement query,
+-- which however cannot be parameterized or prepared,
+-- nor can any results of it be collected.
+{-# INLINE sql #-}
+sql :: ByteString -> Transaction ()
+sql =
+  Transaction . lift . B.script . Text.decodeUtf8
+
+-- |
+-- Parameters and a specification of the parametric query to apply them to.
+{-# INLINE statement #-}
+statement :: a -> A.Statement a b -> Transaction b
+statement params statement =
+  Transaction . lift $ B.statement params statement
+
+-- |
+-- Cause transaction to eventually roll back.
+{-# INLINE condemn #-}
+condemn :: Transaction ()
+condemn =
+  Transaction $ put False
diff --git a/src/library/Hasql/Transaction/Sessions.hs b/src/library/Hasql/Transaction/Sessions.hs
new file mode 100644
--- /dev/null
+++ b/src/library/Hasql/Transaction/Sessions.hs
@@ -0,0 +1,28 @@
+module Hasql.Transaction.Sessions
+  ( transaction,
+    transactionNoRetry,
+
+    -- * Transaction settings
+    C.Mode (..),
+    C.IsolationLevel (..),
+  )
+where
+
+import Hasql.Session qualified as B
+import Hasql.Transaction.Config qualified as C
+import Hasql.Transaction.Private.Prelude
+import Hasql.Transaction.Private.Transaction qualified as A
+
+-- |
+-- Execute the transaction using the provided isolation level and mode.
+{-# INLINE transaction #-}
+transaction :: C.IsolationLevel -> C.Mode -> A.Transaction a -> B.Session a
+transaction isolation mode transaction =
+  A.run transaction isolation mode True
+
+-- |
+-- Execute the transaction but do not retry it on errors.
+{-# INLINE transactionNoRetry #-}
+transactionNoRetry :: C.IsolationLevel -> C.Mode -> A.Transaction a -> B.Session a
+transactionNoRetry isolation mode transaction =
+  A.run transaction isolation mode False
