diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,74 @@
+# hpqtypes-1.15.0.0 (2026-09-28)
+* The thread that starts a DB session now owns it. If another thread uses the
+  session, the library throws `ThreadMismatchError` wrapped in `DBException`.
+  Previously, threads shared the session, including its connection and its
+  transaction. This change affects all threads that get the session from a
+  fork, e.g. the threads of `race` and `concurrently` from `lifted-async`. To
+  run queries in another thread, call `withNewSession` in that thread.
+* Rename `withNewConnection` to `withNewSession`, because the new session does
+  not always acquire a connection immediately. Replace each use of
+  `withNewConnection` with `withNewSession`.
+* Fix two bugs of `withNewSession` (`withNewConnection` in earlier versions)
+  in a forked thread. If the parent session ended first, the function failed
+  with the error `finalized connection`. While the parent session ran a query,
+  the function waited for the query to finish.
+* Fix a use-after-free of the buffer that holds the connection string in
+  `connect`. If an asynchronous exception interrupted `connect`, the
+  use-after-free was possible.
+* Execution of a `COPY` statement now throws an error that says the statement
+  is not supported. Previously the library silently reported success and left
+  the connection in copy mode until the next query.
+* Fix transaction restart handling. A restarted transaction no longer runs
+  with asynchronous exceptions masked. An asynchronous exception, e.g. a
+  timeout, no longer triggers a restart. A restart predicate that matches
+  it makes no difference. If a transaction fails, a subsequent failure of
+  its cleanup no longer masks the original exception. Previously this hid
+  the exception from the restart predicate.
+* Fix a bug in the on demand connection acquisition mode. If a query left
+  the connection in a state that admits no further queries, e.g. because the
+  connection died, the `ROLLBACK` that ended the automatic transaction of the
+  query failed. Its failure masked the exception thrown from the query.
+* Fix a bug in `withSavepoint`. If the action threw and the cleanup of the
+  savepoint failed as well, e.g. because the connection died, the failure of
+  the cleanup masked the exception of the action.
+* Fix a bug in `unsafeWithoutTransaction`. If the action threw and the `BEGIN`
+  that restores the transaction failed as well, e.g. because the connection
+  died, the failure of the `BEGIN` masked the exception of the action.
+* Row fetching functions no longer decode all rows of the result up front
+  and retain them until the fold completes. They decode each row right
+  before the fold function consumes it. As a result, e.g. `mapDB_` over a
+  large result runs in constant additional memory.
+* Add `FromSQL` and `ToSQL` instances for `Word16`, `Word32` and `Word64`.
+* Add `FromSQL` and `ToSQL` instances for `Integer`, mapped to `numeric`.
+* `JSON` and `JSONB` now serialize and deserialize the wrapped type with its
+  `ToJSON` and `FromJSON` instances. They work for any such type and support
+  `deriving via`, e.g. `deriving (PQFormat, ToSQL, FromSQL) via JSONB Config`.
+  The `aesonFromSQL` and `aesonToSQL` helpers are gone. The instances for
+  unparsed JSON text moved to the dedicated `RawJSON` and `RawJSONB` types,
+  which wrap a strict `ByteString`. The instances for lazy `ByteString` are
+  gone. The `encodeRawJSON` and `encodeRawJSONB` functions build these types
+  from any type with a `ToJSON` instance. The `decodeRawJSON` and
+  `decodeRawJSONB` functions convert back and return `Nothing` on a failure.
+  The `eitherDecodeRawJSON` and `eitherDecodeRawJSONB` functions return the
+  reason for the failure instead.
+* Fix a bug in `changeAcquisitionModeTo`. If the commit of a transaction
+  failed during the transition from the `AcquireAndHold` to the
+  `AcquireOnDemand` mode, the library held on to an invalid connection
+  object.
+* Fix a bug in `withCursor`. If the enclosing transaction was in the aborted
+  state, the failure of the cursor cleanup masked an exception thrown from
+  the continuation. In particular, this prevented restarts of transactions
+  run with a `RestartPredicate`.
+* Fix a bug in `commit`, `rollback` and `unsafeWithoutTransaction`. If the
+  issued `COMMIT` failed, e.g. because of a deferred constraint violation,
+  the session stayed in the autocommit mode instead of starting a new
+  transaction.
+* Fix a bug in `withCursor` where an asynchronous exception cancelled the
+  `CLOSE` query and left the cursor open.
+* Fix a bug in `withSavepoint` where an asynchronous exception cancelled the
+  `ROLLBACK TO SAVEPOINT` or `RELEASE SAVEPOINT` query. The savepoint stayed in
+  place, or the transaction stayed in the aborted state.
+
 # hpqtypes-1.14.0.0 (2025-12-10)
 * Make `begin`, `commit` and `rollback` do nothing instead of throwing an error
   if the on demand connection acquisition mode is active.
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # hpqtypes
 
-[![CI](https://github.com/scrive/hpqtypes/actions/workflows/haskell-ci.yml/badge.svg?branch=master)](https://github.com/scrive/hpqtypes/actions/workflows/haskell-ci.yml)
+[![CI](https://github.com/scrive/hpqtypes/actions/workflows/haskell-gha.yml/badge.svg?branch=master)](https://github.com/scrive/hpqtypes/actions/workflows/haskell-gha.yml)
 [![Hackage](https://img.shields.io/hackage/v/hpqtypes.svg)](https://hackage.haskell.org/package/hpqtypes)
 [![Stackage LTS](https://www.stackage.org/package/hpqtypes/badge/lts)](https://www.stackage.org/lts/package/hpqtypes)
 [![Stackage Nightly](https://www.stackage.org/package/hpqtypes/badge/nightly)](https://www.stackage.org/nightly/package/hpqtypes)
diff --git a/bench-db/Main.hs b/bench-db/Main.hs
new file mode 100644
--- /dev/null
+++ b/bench-db/Main.hs
@@ -0,0 +1,195 @@
+module Main (main) where
+
+import Control.DeepSeq
+import Control.Exception
+import Control.Monad
+import Data.Int
+import Data.Pool (defaultPoolConfig)
+import Data.Text qualified as T
+import Data.Time
+import Database.PostgreSQL.PQTypes
+import System.Environment
+import Test.Tasty.Bench
+
+-- | Number of records inserted into each of the two tables.
+numRecords :: Int
+numRecords = 50000
+
+-- | Number of children per parent. Only the first @numRecords \`div\`
+-- childrenPerParent@ parents get children, so the selection phase decodes
+-- both non-trivial and empty arrays.
+childrenPerParent :: Int
+childrenPerParent = 10
+
+-- | Number of scalars fetched by 'selectBigArray' and 'selectManyRows'.
+-- They deliver the same amount of data, as one array and as that many rows
+-- respectively, so that the array decoder and the row decoder can be
+-- compared against each other.
+bigArraySize :: Int32
+bigArraySize = 100000
+
+----------------------------------------
+
+data Child = Child Int32 T.Text Double UTCTime Integer
+data Parent = Parent Int32 T.Text Double UTCTime Integer [Child]
+
+instance NFData Child where
+  rnf (Child a b c d e) =
+    rnf a `seq` rnf b `seq` rnf c `seq` rnf d `seq` rnf e
+
+instance NFData Parent where
+  rnf (Parent a b c d e f) =
+    rnf a `seq` rnf b `seq` rnf c `seq` rnf d `seq` rnf e `seq` rnf f
+
+type instance CompositeRow Child = (Int32, T.Text, Double, UTCTime, Integer)
+
+instance PQFormat Child where
+  pqFormat = "%bench_child_"
+
+instance CompositeFromSQL Child where
+  toComposite (cid, t, d, ts, n) = Child cid t d ts n
+
+-- | Deterministic record with the given id.
+recordData :: UTCTime -> Int -> (Int32, T.Text, Double, UTCTime, Integer)
+recordData base i =
+  ( fromIntegral i
+  , T.pack $ "record " <> show i <> " with some textual payload"
+  , fromIntegral i * 1.5
+  , addUTCTime (fromIntegral i) base
+  , 2 ^ (70 :: Int) + fromIntegral i
+  )
+
+----------------------------------------
+
+createTables :: DBT IO ()
+createTables = do
+  runSQL_ $
+    mconcat
+      [ "CREATE TABLE bench_parents_ ("
+      , "  id INTEGER NOT NULL"
+      , ", t TEXT NOT NULL"
+      , ", d DOUBLE PRECISION NOT NULL"
+      , ", ts TIMESTAMPTZ NOT NULL"
+      , ", n NUMERIC NOT NULL"
+      , ", PRIMARY KEY (id)"
+      , ")"
+      ]
+  runSQL_ $
+    mconcat
+      [ "CREATE TABLE bench_children_ ("
+      , "  id INTEGER NOT NULL"
+      , ", parent_id INTEGER NOT NULL"
+      , ", t TEXT NOT NULL"
+      , ", d DOUBLE PRECISION NOT NULL"
+      , ", ts TIMESTAMPTZ NOT NULL"
+      , ", n NUMERIC NOT NULL"
+      , ", PRIMARY KEY (id)"
+      , ", FOREIGN KEY (parent_id) REFERENCES bench_parents_ (id)"
+      , ")"
+      ]
+  runSQL_ "CREATE INDEX bench_children_parent_id_idx_ ON bench_children_ (parent_id)"
+  runSQL_ $
+    mconcat
+      [ "CREATE TYPE bench_child_ AS ("
+      , "  id INTEGER"
+      , ", t TEXT"
+      , ", d DOUBLE PRECISION"
+      , ", ts TIMESTAMPTZ"
+      , ", n NUMERIC"
+      , ")"
+      ]
+
+dropTables :: DBT IO ()
+dropTables = do
+  runSQL_ "DROP TYPE IF EXISTS bench_child_"
+  runSQL_ "DROP TABLE IF EXISTS bench_children_"
+  runSQL_ "DROP TABLE IF EXISTS bench_parents_"
+
+-- | Empty the tables so that 'insertData' can be run again.
+truncateTables :: DBT IO ()
+truncateTables = runSQL_ "TRUNCATE bench_children_, bench_parents_"
+
+insertData :: UTCTime -> DBT IO ()
+insertData base = do
+  forM_ [0 .. numRecords - 1] $ \i -> do
+    runQuery_ $
+      rawSQL
+        "INSERT INTO bench_parents_ (id, t, d, ts, n) VALUES ($1, $2, $3, $4, $5)"
+        (recordData base i)
+  forM_ [0 .. numRecords - 1] $ \i -> do
+    let (cid, t, d, ts, n) = recordData base i
+        pid = fromIntegral $ i `div` childrenPerParent :: Int32
+    runQuery_ $
+      rawSQL
+        "INSERT INTO bench_children_ (id, parent_id, t, d, ts, n) VALUES ($1, $2, $3, $4, $5, $6)"
+        (cid, pid, t, d, ts, n)
+
+selectParents :: DBT IO [Parent]
+selectParents = do
+  runSQL_ "SELECT p.id, p.t, p.d, p.ts, p.n FROM bench_parents_ p ORDER BY p.id"
+  fetchMany $ \(pid, t, d, ts, n) -> Parent pid t d ts n []
+
+selectData :: DBT IO [Parent]
+selectData = do
+  runSQL_ $
+    mconcat
+      [ "SELECT p.id, p.t, p.d, p.ts, p.n"
+      , ", ARRAY(SELECT (c.id, c.t, c.d, c.ts, c.n)::bench_child_"
+      , "        FROM bench_children_ c WHERE c.parent_id = p.id ORDER BY c.id)"
+      , " FROM bench_parents_ p ORDER BY p.id"
+      ]
+  fetchMany $ \(pid, t, d, ts, n, CompositeArray1 children) ->
+    Parent pid t d ts n children
+
+selectBigArray :: DBT IO [Int32]
+selectBigArray = do
+  runQuery_ $
+    rawSQL "SELECT ARRAY(SELECT generate_series(1, $1))::int4[]" (Identity bigArraySize)
+  fetchOne $ \(Identity (Array1 elems)) -> elems
+
+-- | The counterpart of 'selectBigArray': the same scalars, delivered as one
+-- column of that many rows instead of as one array.
+selectManyRows :: DBT IO [Int32]
+selectManyRows = do
+  runQuery_ $ rawSQL "SELECT generate_series(1, $1)" (Identity bigArraySize)
+  fetchMany runIdentity
+
+----------------------------------------
+
+-- | The connection info string is taken from the @CONNINFO@ environment
+-- variable, as the command line belongs to @tasty-bench@. If it's not set,
+-- the choice is left to @libpq@, i.e. to the @PG*@ variables.
+main :: IO ()
+main = do
+  connInfo <- maybe T.empty T.pack <$> lookupEnv "CONNINFO"
+  let settings = defaultConnectionSettings {csConnInfo = connInfo}
+  ConnectionSource cs <- pooled settings
+  let runDB :: DBT IO a -> IO a
+      runDB = runDBT cs defaultTransactionSettings
+  base <- getCurrentTime
+  runDB $ do
+    -- Keep the NOTICEs of the DROPs below out of the benchmark report.
+    runSQL_ "SET client_min_messages TO WARNING"
+    dropTables
+    createTables
+    insertData base
+  -- Registration of composites happens at connection time, so bench_child_
+  -- needs to exist before this source is used.
+  ConnectionSource csComposite <- pooled settings {csComposites = ["bench_child_"]}
+  let runDBComposite :: DBT IO a -> IO a
+      runDBComposite = runDBT csComposite defaultTransactionSettings
+  (`finally` runDB dropTables) . defaultMain $
+    [ -- Insertion refills the tables it empties, so the selection benchmarks
+      -- below see the same data regardless of whether this one ran.
+      bench "insert" . nfIO . runDB $ truncateTables >> insertData base
+    , bench "select parents" . nfIO $ runDB selectParents
+    , bench "select parents with children" . nfIO $ runDBComposite selectData
+    , bench "select big array" . nfIO $ runDB selectBigArray
+    , bench "select many rows" . nfIO $ runDB selectManyRows
+    ]
+  where
+    -- A pool holding a single connection is used rather than 'simpleSource'
+    -- so that establishing one isn't measured by every iteration of a
+    -- benchmark.
+    pooled settings = poolSource settings $ \connect disconnect ->
+      defaultPoolConfig connect disconnect 60 1
diff --git a/examples/Catalog.hs b/examples/Catalog.hs
--- a/examples/Catalog.hs
+++ b/examples/Catalog.hs
@@ -2,8 +2,8 @@
 
 import Control.Arrow (second)
 import Control.Monad
-import Control.Monad.Base
 import Control.Monad.Catch
+import Control.Monad.IO.Class
 import Data.Function
 import Data.Int
 import Data.Monoid.Utils
@@ -11,12 +11,12 @@
 import Data.Text qualified as T
 import Database.PostgreSQL.PQTypes
 import Database.PostgreSQL.PQTypes.Internal.Utils (mread)
-import System.Console.Readline
+import System.Console.Haskeline
 import System.Environment
 
 -- | Generic 'putStrLn'.
-printLn :: MonadBase IO m => String -> m ()
-printLn = liftBase . putStrLn
+printLn :: MonadIO m => String -> m ()
+printLn = liftIO . putStrLn
 
 -- | Get connection string from command line argument.
 getConnSettings :: IO ConnectionSettings
@@ -144,13 +144,12 @@
   cs <- getConnSettings
   withCatalog cs $ do
     ConnectionSource pool <- poolSource (cs {csComposites = ["book_"]}) (\connect disconnect -> defaultPoolConfig connect disconnect 1 10)
-    fix $ \next ->
-      readline "> "
+    runInputT defaultSettings . fix $ \next ->
+      getInputLine "> "
         >>= maybe
           (printLn "")
           ( \cmd -> do
               when (cmd /= "quit") $ do
-                processCommand pool cmd
-                addHistory cmd
+                liftIO $ processCommand pool cmd
                 next
           )
diff --git a/hpqtypes.cabal b/hpqtypes.cabal
--- a/hpqtypes.cabal
+++ b/hpqtypes.cabal
@@ -1,15 +1,15 @@
 cabal-version:       3.0
 build-type:          Simple
 name:                hpqtypes
-version:             1.14.0.0
+version:             1.15.0.0
 synopsis:            Haskell bindings to libpqtypes
 
 description:         Efficient and easy-to-use bindings to (slightly modified)
                      @libpqtypes@, a @libpq@ extension that adds support
                      for a binary transport format and composite types.
-                     .
+
                      Source code of libpqtypes is bundled along with the bindings.
-                     .
+
                      Examples can be found in the
                      <https://github.com/scrive/hpqtypes/tree/master/examples examples>
                      directory.
@@ -21,11 +21,12 @@
 maintainer:          Andrzej Rybczak <andrzej@rybczak.net>
 copyright:           Scrive AB
 category:            Database
-tested-with:         GHC == { 9.2.8, 9.4.8, 9.6.7, 9.8.4, 9.10.3, 9.12.2, 9.14.1 }
+tested-with:         GHC ^>= { 9.2, 9.4, 9.6, 9.8, 9.10, 9.12, 9.14 }
 
-extra-source-files: README.md
+extra-doc-files:    README.md
                   , CHANGELOG.md
-                  , examples/Catalog.hs
+
+extra-source-files: examples/Catalog.hs
                   , libpqtypes/AUTHORS
                   , libpqtypes/Makefile.in
                   , libpqtypes/config.guess
@@ -170,10 +171,34 @@
   other-extensions:   AllowAmbiguousTypes
                     , CPP
 
-test-suite hpqtypes-tests
+benchmark bench-db
   type:              exitcode-stdio-1.0
   ghc-options:       -Wall -Werror=prepositive-qualified-module -threaded
 
+  hs-source-dirs:    bench-db
+  main-is:           Main.hs
+
+  build-depends:       hpqtypes
+                     , base >= 4.14 && < 5
+                     , deepseq >= 1.4
+                     , resource-pool >= 0.4
+                     , tasty-bench >= 0.3
+                     , text >= 0.11
+                     , time >= 1.4
+
+  default-language:  Haskell2010
+  default-extensions: ConstraintKinds
+                    , GADTs
+                    , ImportQualifiedPost
+                    , LambdaCase
+                    , OverloadedStrings
+                    , RankNTypes
+                    , TypeFamilies
+
+test-suite test
+  type:              exitcode-stdio-1.0
+  ghc-options:       -Wall -Werror=prepositive-qualified-module -threaded
+
   hs-source-dirs:    test
   main-is:           Main.hs
   other-modules:     Prelude.Instances
@@ -196,7 +221,7 @@
                      , monad-control >= 1.0.3
                      , mtl >= 2.1
                      , random >= 1.0
-                     , readline >= 1.0.3.0
+                     , haskeline >= 0.8.0.0
                      , resource-pool >= 0.4
                      , scientific
                      , test-framework >= 0.8
diff --git a/libpqtypes/src/libpqtypes.h b/libpqtypes/src/libpqtypes.h
--- a/libpqtypes/src/libpqtypes.h
+++ b/libpqtypes/src/libpqtypes.h
@@ -180,6 +180,19 @@
 typedef double PGfloat8;
 typedef char *PGnumeric;
 
+#define NUMERIC_POS			0x0000
+#define NUMERIC_NEG			0x4000
+#define NUMERIC_NAN			0xC000
+
+typedef struct
+{
+	short ndigits;        /* # of digits in digits[] - can be 0! */
+	short weight;         /* weight of first digit */
+	short sign;           /* NUMERIC_POS, NUMERIC_NEG, or NUMERIC_NAN */
+	short dscale;         /* display scale */
+	const short *digits;  /* base-NBASE digits in network byte order */
+} NumericVar;
+
 /* Defined by an end-user if the system is missing long long. */
 #ifdef PQT_LONG_LONG
 	typedef PQT_LONG_LONG PGint8;
diff --git a/libpqtypes/src/numerics.c b/libpqtypes/src/numerics.c
--- a/libpqtypes/src/numerics.c
+++ b/libpqtypes/src/numerics.c
@@ -20,50 +20,6 @@
 #	pragma warning (disable : 4244)
 #endif
 
-/*
- * Macros and structures for receiving numeric field in binary
- */
-#define NBASE		10000
-#define HALF_NBASE	5000
-#define DEC_DIGITS	4			/* decimal digits per NBASE digit */
-#define MUL_GUARD_DIGITS	2	/* these are measured in NBASE digits */
-#define DIV_GUARD_DIGITS	4
-
-/*
- * Hardcoded precision limit - arbitrary, but must be small enough that
- * dscale values will fit in 14 bits.
- */
-#define NUMERIC_MAX_PRECISION		1000
-
-/*
- * Sign values and macros to deal with packing/unpacking n_sign_dscale
- */
-#define NUMERIC_SIGN_MASK	0xC000
-#define NUMERIC_POS			0x0000
-#define NUMERIC_NEG			0x4000
-#define NUMERIC_NAN			0xC000
-#define NUMERIC_DSCALE_MASK 0x3FFF
-#define NUMERIC_SIGN(n)		((n)->n_sign_dscale & NUMERIC_SIGN_MASK)
-#define NUMERIC_DSCALE(n)	((n)->n_sign_dscale & NUMERIC_DSCALE_MASK)
-#define NUMERIC_IS_NAN(n)	(NUMERIC_SIGN(n) != NUMERIC_POS &&	\
-							 NUMERIC_SIGN(n) != NUMERIC_NEG)
-
-typedef short NumericDigit;
-static const int round_powers[4] = {0, 1000, 100, 10};
-
-typedef struct NumericVar
-{
-	int ndigits;            /* # of digits in digits[] - can be 0! */
-	int weight;             /* weight of first digit */
-	int sign;               /* NUMERIC_POS, NUMERIC_NEG, or NUMERIC_NAN */
-	int dscale;             /* display scale */
-	NumericDigit *buf;			/* start of palloc'd space for digits[] */
-	NumericDigit *digits;		/* base-NBASE digits */
-} NumericVar;
-
-static int str2num(PGtypeArgs *args, const char *str, NumericVar *dest);
-static int num2str(char *out, size_t outl, NumericVar *var, int dscale);
-
 int
 pqt_put_int2(PGtypeArgs *args)
 {
@@ -223,526 +179,46 @@
 	return 0;
 }
 
-/* exposing a NumericVar struct to a libpq user, or something similar,
- * doesn't seem useful w/o a library to operate on it.  Instead, we
- * always expose a numeric in text format and let the API user decide
- * how to use it .. like strod or a 3rd party big number library.  We
- * always send a numeric in binary though.
- */
 int
 pqt_put_numeric(PGtypeArgs *args)
 {
-	int numlen;
-	NumericVar num = {0};
-	short *out;
-	PGnumeric str = va_arg(args->ap, PGnumeric);
-
-	PUTNULLCHK(args, str);
-
-	if (str2num(args, str, &num))
-	{
-		if (num.digits)
-			free(num.digits);
-		return -1;
-	}
+	NumericVar *num = va_arg(args->ap, NumericVar *);
+	PUTNULLCHK(args, num);
 
 	/* variable length data type, grow args->put.out buffer if needed */
-	numlen = (int) sizeof(short) * (4 + num.ndigits);
+	int numlen = (int) sizeof(short) * (4 + num->ndigits);
 	if (args->put.expandBuffer(args, numlen) == -1)
 		return -1;
 
-	out = (short *) args->put.out;
-	*out++ = htons((short) num.ndigits);
-	*out++ = htons((short) num.weight);
-	*out++ = htons((short) num.sign);
-	*out++ = htons((short) num.dscale);
+	short *out = (short *) args->put.out;
+	*out++ = htons(num->ndigits);
+	*out++ = htons(num->weight);
+	*out++ = htons(num->sign);
+	*out++ = htons(num->dscale);
 
-	if (num.digits)
-	{
-		int i;
-		for (i=0; i < num.ndigits; i++)
-			*out++ = htons(num.digits[i]);
-		free(num.digits);
-	}
+	for (int i = 0; i < num->ndigits; ++i)
+		*out++ = num->digits[i];
 
 	return numlen;
 }
 
-/* exposing a NumericVar struct to a libpq user, or something similar,
- * doesn't seem useful w/o a library to operate on it.  Instead, we
- * always expose a numeric in text format and let the API user decide
- * how to use it .. like strod or a 3rd party big number library.
- */
 int
 pqt_get_numeric(PGtypeArgs *args)
 {
-	int i;
-	short *s;
-	NumericVar num;
 	DECLVALUE(args);
-	char buf[4096];
-	size_t len;
-	PGnumeric *str = va_arg(args->ap, PGnumeric *);
+	NumericVar *num = va_arg(args->ap, NumericVar *);
 
-	CHKGETVALS(args, str);
+	CHKGETVALS(args, num);
 
 	if (args->format == TEXTFMT)
-	{
-		*str = value;
-		return 0;
-	}
-
-	s = (short *) value;
-	num.ndigits = ntohs(*s); s++;
-	num.weight  = ntohs(*s); s++;
-	num.sign    = ntohs(*s); s++;
-	num.dscale  = ntohs(*s); s++;
-	num.digits  = (short *) malloc(num.ndigits * sizeof(short));
-	if (!num.digits)
-		RERR_MEM(args);
-
-	for (i=0; i < num.ndigits; i++)
-	{
-		num.digits[i] = ntohs(*s);
-		s++;
-	}
-
-	i = num2str(buf, sizeof(buf), &num, num.dscale);
-	free(num.digits);
-
-	/* num2str failed, only fails when 'str' is too small */
-	if (i == -1)
-		RERR(args, "out buffer is too small");
+		return args->errorf(args, "text format is not supported");
 
-	len = strlen(buf)+1;
-	*str = PQresultAlloc(args->get.result, len);
-	if (!*str)
-		RERR_MEM(args);
+	short *s = (short *) value;
+	num->ndigits = ntohs(*s); s++;
+	num->weight  = ntohs(*s); s++;
+	num->sign    = ntohs(*s); s++;
+	num->dscale  = ntohs(*s); s++;
+	num->digits  = s;
 
-	memcpy(*str, buf, len);
 	return 0;
-
 }
-
-
-/*
- * round_var
- *
- * Round the value of a variable to no more than rscale decimal digits
- * after the decimal point.  NOTE: we allow rscale < 0 here, implying
- * rounding before the decimal point.
- */
-static void
-round_var(NumericVar *var, int rscale)
-{
-	NumericDigit *digits = var->digits;
-	int			di;
-	int			ndigits;
-	int			carry;
-
-	var->dscale = rscale;
-
-	/* decimal digits wanted */
-	di = (var->weight + 1) * DEC_DIGITS + rscale;
-
-	/*
-	 * If di = 0, the value loses all digits, but could round up to 1 if its
-	 * first extra digit is >= 5.  If di < 0 the result must be 0.
-	 */
-	if (di < 0)
-	{
-		var->ndigits = 0;
-		var->weight = 0;
-		var->sign = NUMERIC_POS;
-	}
-	else
-	{
-		/* NBASE digits wanted */
-		ndigits = (di + DEC_DIGITS - 1) / DEC_DIGITS;
-
-		/* 0, or number of decimal digits to keep in last NBASE digit */
-		di %= DEC_DIGITS;
-
-		if (ndigits < var->ndigits ||
-			(ndigits == var->ndigits && di > 0))
-		{
-			var->ndigits = ndigits;
-
-			if (di == 0)
-				carry = (digits[ndigits] >= HALF_NBASE) ? 1 : 0;
-			else
-			{
-				/* Must round within last NBASE digit */
-				int			extra,
-							pow10;
-
-				pow10 = round_powers[di];
-				extra = digits[--ndigits] % pow10;
-				digits[ndigits] = digits[ndigits] - (NumericDigit) extra;
-				carry = 0;
-				if (extra >= pow10 / 2)
-				{
-					pow10 += digits[ndigits];
-					if (pow10 >= NBASE)
-					{
-						pow10 -= NBASE;
-						carry = 1;
-					}
-					digits[ndigits] = (NumericDigit) pow10;
-				}
-			}
-
-			/* Propagate carry if needed */
-			while (carry)
-			{
-				carry += digits[--ndigits];
-				if (carry >= NBASE)
-				{
-					digits[ndigits] = (NumericDigit) (carry - NBASE);
-					carry = 1;
-				}
-				else
-				{
-					digits[ndigits] = (NumericDigit) carry;
-					carry = 0;
-				}
-			}
-
-			if (ndigits < 0)
-			{
-				var->digits--;
-				var->ndigits++;
-				var->weight++;
-			}
-		}
-	}
-}
-
-/*
- * strip_var
- *
- * Strip any leading and trailing zeroes from a numeric variable
- */
-static void
-strip_var(NumericVar *var)
-{
-	NumericDigit *digits = var->digits;
-	int			ndigits = var->ndigits;
-
-	/* Strip leading zeroes */
-	while (ndigits > 0 && *digits == 0)
-	{
-		digits++;
-		var->weight--;
-		ndigits--;
-	}
-
-	/* Strip trailing zeroes */
-	while (ndigits > 0 && digits[ndigits - 1] == 0)
-		ndigits--;
-
-	/* If it's zero, normalize the sign and weight */
-	if (ndigits == 0)
-	{
-		var->sign = NUMERIC_POS;
-		var->weight = 0;
-	}
-
-	var->digits = digits;
-	var->ndigits = ndigits;
-}
-
-/*
- * str2num()
- *
- *	Parse a string and put the number into a variable
- *  returns -1 on error and 0 for success.
- */
-static int
-str2num(PGtypeArgs *args, const char *str, NumericVar *dest)
-{
-	const char *cp = str;
-	int		have_dp = FALSE;
-	int			i;
-	unsigned char *decdigits;
-	int			sign = NUMERIC_POS;
-	int			dweight = -1;
-	int			ddigits;
-	int			dscale = 0;
-	int			weight;
-	int			ndigits;
-	int			offset;
-	NumericDigit *digits;
-
-	/*
-	 * We first parse the string to extract decimal digits and determine the
-	 * correct decimal weight.	Then convert to NBASE representation.
-	 */
-
-	/* skip leading spaces */
-	while (*cp)
-	{
-		if (!isspace((unsigned char) *cp))
-			break;
-		cp++;
-	}
-
-	switch (*cp)
-	{
-		case '+':
-			sign = NUMERIC_POS;
-			cp++;
-			break;
-
-		case '-':
-			sign = NUMERIC_NEG;
-			cp++;
-			break;
-	}
-
-	if (*cp == '.')
-	{
-		have_dp = TRUE;
-		cp++;
-	}
-
-	if (!isdigit((unsigned char) *cp))
-		return args->errorf(args,
-			"invalid input syntax for type numeric: '%s'", str);
-
-	decdigits = (unsigned char *) malloc(strlen(cp) + DEC_DIGITS * 2);
-
-	/* leading padding for digit alignment later */
-	memset(decdigits, 0, DEC_DIGITS);
-	i = DEC_DIGITS;
-
-	while (*cp)
-	{
-		if (isdigit((unsigned char) *cp))
-		{
-			decdigits[i++] = *cp++ - '0';
-			if (!have_dp)
-				dweight++;
-			else
-				dscale++;
-		}
-		else if (*cp == '.')
-		{
-			if (have_dp)
-			{
-				free(decdigits);
-				return args->errorf(args,
-					"invalid input syntax for type numeric: '%s'", str);
-			}
-
-			have_dp = TRUE;
-			cp++;
-		}
-		else
-			break;
-	}
-
-	ddigits = i - DEC_DIGITS;
-	/* trailing padding for digit alignment later */
-	memset(decdigits + i, 0, DEC_DIGITS - 1);
-
-	/* Handle exponent, if any */
-	if (*cp == 'e' || *cp == 'E')
-	{
-		long		exponent;
-		char	   *endptr;
-
-		cp++;
-		exponent = strtol(cp, &endptr, 10);
-		if (endptr == cp)
-		{
-			free(decdigits);
-			return args->errorf(args,
-				"invalid input syntax for type numeric: '%s'", str);
-		}
-
-		cp = endptr;
-		if (exponent > NUMERIC_MAX_PRECISION ||
-			exponent < -NUMERIC_MAX_PRECISION)
-		{
-			free(decdigits);
-			return args->errorf(args,
-				"invalid input syntax for type numeric: '%s'", str);
-		}
-
-		dweight += (int) exponent;
-		dscale -= (int) exponent;
-		if (dscale < 0)
-			dscale = 0;
-	}
-
-	/* Should be nothing left but spaces */
-	while (*cp)
-	{
-		if (!isspace((unsigned char) *cp))
-		{
-			free(decdigits);
-			return args->errorf(args,
-				"invalid input syntax for type numeric: '%s'", str);
-		}
-		cp++;
-	}
-
-	/*
-	 * Okay, convert pure-decimal representation to base NBASE.  First we need
-	 * to determine the converted weight and ndigits.  offset is the number of
-	 * decimal zeroes to insert before the first given digit to have a
-	 * correctly aligned first NBASE digit.
-	 */
-	if (dweight >= 0)
-		weight = (dweight + 1 + DEC_DIGITS - 1) / DEC_DIGITS - 1;
-	else
-		weight = -((-dweight - 1) / DEC_DIGITS + 1);
-	offset = (weight + 1) * DEC_DIGITS - (dweight + 1);
-	ndigits = (ddigits + offset + DEC_DIGITS - 1) / DEC_DIGITS;
-
-	dest->digits = (NumericDigit *) malloc((ndigits) * sizeof(NumericDigit));
-	dest->ndigits = ndigits;
-	dest->sign = sign;
-	dest->weight = weight;
-	dest->dscale = dscale;
-
-	i = DEC_DIGITS - offset;
-	digits = dest->digits;
-
-	while (ndigits-- > 0)
-	{
-		*digits++ = ((decdigits[i] * 10 + decdigits[i + 1]) * 10 +
-					 decdigits[i + 2]) * 10 + decdigits[i + 3];
-		i += DEC_DIGITS;
-	}
-
-	free(decdigits);
-
-	/* Strip any leading/trailing zeroes, and normalize weight if zero */
-	strip_var(dest);
-	return 0;
-}
-
-/*
- * num2str() -
- *
- *	Convert a var to text representation (guts of numeric_out).
- *	CAUTION: var's contents may be modified by rounding!
- *	returns -1 on error and 0 for success.
- */
-static int
-num2str(char *out, size_t outl, NumericVar *var, int dscale)
-{
-	//char	   *str;
-	char	   *cp;
-	char	   *endcp;
-	int			i;
-	int			d;
-	NumericDigit dig;
-	NumericDigit d1;
-
-	if (dscale < 0)
-		dscale = 0;
-
-	/*
-	 * Check if we must round up before printing the value and do so.
-	 */
-	round_var(var, dscale);
-
-	/*
-	 * Allocate space for the result.
-	 *
-	 * i is set to to # of decimal digits before decimal point. dscale is the
-	 * # of decimal digits we will print after decimal point. We may generate
-	 * as many as DEC_DIGITS-1 excess digits at the end, and in addition we
-	 * need room for sign, decimal point, null terminator.
-	 */
-	i = (var->weight + 1) * DEC_DIGITS;
-	if (i <= 0)
-		i = 1;
-
-	if (outl <= (size_t) (i + dscale + DEC_DIGITS + 2))
-		return -1;
-
-	//str = palloc(i + dscale + DEC_DIGITS + 2);
-	//cp = str
-	cp = out;
-
-	/*
-	 * Output a dash for negative values
-	 */
-	if (var->sign == NUMERIC_NEG)
-		*cp++ = '-';
-
-	/*
-	 * Output all digits before the decimal point
-	 */
-	if (var->weight < 0)
-	{
-		d = var->weight + 1;
-		*cp++ = '0';
-	}
-	else
-	{
-		for (d = 0; d <= var->weight; d++)
-		{
-			dig = (d < var->ndigits) ? var->digits[d] : 0;
-			/* In the first digit, suppress extra leading decimal zeroes */
-			{
-				int		putit = (d > 0);
-
-				d1 = dig / 1000;
-				dig -= d1 * 1000;
-				putit |= (d1 > 0);
-				if (putit)
-					*cp++ = (char) (d1 + '0');
-				d1 = dig / 100;
-				dig -= d1 * 100;
-				putit |= (d1 > 0);
-				if (putit)
-					*cp++ = (char) (d1 + '0');
-				d1 = dig / 10;
-				dig -= d1 * 10;
-				putit |= (d1 > 0);
-				if (putit)
-					*cp++ = (char) (d1 + '0');
-				*cp++ = (char) (dig + '0');
-			}
-		}
-	}
-
-	/*
-	 * If requested, output a decimal point and all the digits that follow it.
-	 * We initially put out a multiple of DEC_DIGITS digits, then truncate if
-	 * needed.
-	 */
-	if (dscale > 0)
-	{
-		*cp++ = '.';
-		endcp = cp + dscale;
-		for (i = 0; i < dscale; d++, i += DEC_DIGITS)
-		{
-			dig = (d >= 0 && d < var->ndigits) ? var->digits[d] : 0;
-			d1 = dig / 1000;
-			dig -= d1 * 1000;
-			*cp++ = (char) (d1 + '0');
-			d1 = dig / 100;
-			dig -= d1 * 100;
-			*cp++ = (char) (d1 + '0');
-			d1 = dig / 10;
-			dig -= d1 * 10;
-			*cp++ = (char) (d1 + '0');
-			*cp++ = (char) (dig + '0');
-		}
-		cp = endcp;
-	}
-
-	/*
-	 * terminate the string and return it
-	 */
-	*cp = '\0';
-	return 0;
-}
-
-
diff --git a/src/Database/PostgreSQL/PQTypes.hs b/src/Database/PostgreSQL/PQTypes.hs
--- a/src/Database/PostgreSQL/PQTypes.hs
+++ b/src/Database/PostgreSQL/PQTypes.hs
@@ -19,6 +19,7 @@
   , DetailedQueryError (..)
   , QueryError (..)
   , HPQTypesError (..)
+  , ThreadMismatchError (..)
   , LibPQError (..)
   , ConversionError (..)
   , ArrayItemError (..)
diff --git a/src/Database/PostgreSQL/PQTypes/Class.hs b/src/Database/PostgreSQL/PQTypes/Class.hs
--- a/src/Database/PostgreSQL/PQTypes/Class.hs
+++ b/src/Database/PostgreSQL/PQTypes/Class.hs
@@ -20,10 +20,8 @@
 import Database.PostgreSQL.PQTypes.Transaction.Settings
 
 class (Applicative m, Monad m) => MonadDB m where
-  -- | Run SQL query and return number of affected/returned rows. Note that
-  -- for a given connection, only one thread may be executing 'runQuery' at
-  -- a given time. If simultaneous call is made from another thread, it
-  -- will block until currently running 'runQuery' finishes.
+  -- | Run an SQL query and return the number of affected or returned rows.
+  -- Only the thread that owns a session can use it, see 'withNewSession'.
   runQuery :: (HasCallStack, IsSQL sql) => sql -> m Int
 
   -- | Similar to 'runQuery', but it prepares and executes a statement under a
@@ -81,13 +79,17 @@
   -- were received before the transaction began.
   getNotification :: HasCallStack => Int -> m (Maybe Notification)
 
-  -- | Execute supplied monadic action with new connection
-  -- using current connection source and transaction settings.
+  -- | Run an action in a new session with the current connection source and
+  -- transaction settings.
   --
-  -- Particularly useful when you want to spawn a new thread, but
-  -- do not want the connection in child thread to be shared with
-  -- the parent one.
-  withNewConnection :: HasCallStack => m a -> m a
+  -- Only the thread that owns a session can use it, so use this function to
+  -- run queries from a child thread by calling it /after/ forking:
+  --
+  -- @
+  -- fork . withNewSession $ do
+  --   ...
+  -- @
+  withNewSession :: HasCallStack => m a -> m a
 
 -- | Generic, overlappable instance.
 instance
@@ -111,4 +113,4 @@
   acquireAndHoldConnection isoLevel = lift . acquireAndHoldConnection isoLevel
   unsafeAcquireOnDemandConnection = lift unsafeAcquireOnDemandConnection
   getNotification = lift . getNotification
-  withNewConnection m = controlT $ \run -> withNewConnection (run m)
+  withNewSession m = controlT $ \run -> withNewSession (run m)
diff --git a/src/Database/PostgreSQL/PQTypes/Cursor.hs b/src/Database/PostgreSQL/PQTypes/Cursor.hs
--- a/src/Database/PostgreSQL/PQTypes/Cursor.hs
+++ b/src/Database/PostgreSQL/PQTypes/Cursor.hs
@@ -21,6 +21,7 @@
 
 import Data.Monoid.Utils
 import Database.PostgreSQL.PQTypes.Class
+import Database.PostgreSQL.PQTypes.Internal.Utils
 import Database.PostgreSQL.PQTypes.SQL
 import Database.PostgreSQL.PQTypes.SQL.Class
 import Database.PostgreSQL.PQTypes.Utils
@@ -107,10 +108,19 @@
   -> (Cursor sql -> m r)
   -> m r
 withCursor name scroll hold sql k =
-  bracket_
-    (runQuery_ declareCursor)
-    (runQuery_ closeCursor)
-    (k $ Cursor name sql)
+  fst
+    <$> generalBracket
+      (runQuery_ declareCursor)
+      ( \() ec ->
+          -- Hard mask asynchronous exceptions, otherwise the query below can be
+          -- interrupted and leave the cursor open.
+          --
+          -- If the continuation threw, the transaction (that cursors declared
+          -- WITHOUT HOLD require) can be in the aborted state, in which case
+          -- closing the cursor fails with in_failed_sql_transaction.
+          uninterruptibleMask_ . runCleanup ec $ runQuery_ closeCursor
+      )
+      (\() -> k $ Cursor name sql)
   where
     declareCursor =
       smconcat
diff --git a/src/Database/PostgreSQL/PQTypes/Format.hs b/src/Database/PostgreSQL/PQTypes/Format.hs
--- a/src/Database/PostgreSQL/PQTypes/Format.hs
+++ b/src/Database/PostgreSQL/PQTypes/Format.hs
@@ -85,6 +85,18 @@
 instance PQFormat Double where
   pqFormat = BS.pack "%float8"
 
+instance PQFormat Word16 where
+  pqFormat = BS.pack "%int2"
+
+instance PQFormat Word32 where
+  pqFormat = BS.pack "%int4"
+
+instance PQFormat Word64 where
+  pqFormat = BS.pack "%int8"
+
+instance PQFormat Integer where
+  pqFormat = BS.pack "%numeric"
+
 -- CHAR
 
 instance PQFormat Char where
diff --git a/src/Database/PostgreSQL/PQTypes/FromSQL.hs b/src/Database/PostgreSQL/PQTypes/FromSQL.hs
--- a/src/Database/PostgreSQL/PQTypes/FromSQL.hs
+++ b/src/Database/PostgreSQL/PQTypes/FromSQL.hs
@@ -68,6 +68,26 @@
   fromSQL Nothing = unexpectedNULL
   fromSQL (Just n) = pure . realToFrac $ n
 
+instance FromSQL Word16 where
+  type PQBase Word16 = CUShort
+  fromSQL Nothing = unexpectedNULL
+  fromSQL (Just n) = pure . fromIntegral $ n
+
+instance FromSQL Word32 where
+  type PQBase Word32 = CUInt
+  fromSQL Nothing = unexpectedNULL
+  fromSQL (Just n) = pure . fromIntegral $ n
+
+instance FromSQL Word64 where
+  type PQBase Word64 = CULLong
+  fromSQL Nothing = unexpectedNULL
+  fromSQL (Just n) = pure . fromIntegral $ n
+
+instance FromSQL Integer where
+  type PQBase Integer = NumericVar
+  fromSQL Nothing = unexpectedNULL
+  fromSQL (Just nv) = numericVarToInteger nv
+
 -- CHAR
 
 instance FromSQL Char where
diff --git a/src/Database/PostgreSQL/PQTypes/Internal/C/Types.hsc b/src/Database/PostgreSQL/PQTypes/Internal/C/Types.hsc
--- a/src/Database/PostgreSQL/PQTypes/Internal/C/Types.hsc
+++ b/src/Database/PostgreSQL/PQTypes/Internal/C/Types.hsc
@@ -34,6 +34,10 @@
   , PGdate(..)
   , PGtime(..)
   , PGtimestamp(..)
+  , c_NUMERIC_POS
+  , c_NUMERIC_NEG
+  , c_NUMERIC_NAN
+  , NumericVar(..)
   ) where
 
 import Data.Word
@@ -355,3 +359,38 @@
     #{poke PGtimestamp, epoch} ptr pgTimestampEpoch
     #{poke PGtimestamp, date}  ptr pgTimestampDate
     #{poke PGtimestamp, time}  ptr pgTimestampTime
+
+----------------------------------------
+
+c_NUMERIC_POS :: CShort
+c_NUMERIC_POS = #{const NUMERIC_POS}
+
+c_NUMERIC_NEG :: CShort
+c_NUMERIC_NEG = #{const NUMERIC_NEG}
+
+c_NUMERIC_NAN :: CShort
+c_NUMERIC_NAN = #{const NUMERIC_NAN}
+
+data NumericVar = NumericVar
+  { numVarNdigits :: !CShort
+  , numVarWeight  :: !CShort
+  , numVarSign    :: !CShort
+  , numVarDscale  :: !CShort
+  , numVarDigits  :: !(Ptr CShort) -- elements in network byte order
+  }
+
+instance Storable NumericVar where
+  sizeOf _ = #{size NumericVar}
+  alignment _ = #{alignment NumericVar}
+  peek ptr = NumericVar
+    <$> #{peek NumericVar, ndigits} ptr
+    <*> #{peek NumericVar, weight} ptr
+    <*> #{peek NumericVar, sign} ptr
+    <*> #{peek NumericVar, dscale} ptr
+    <*> #{peek NumericVar, digits} ptr
+  poke ptr NumericVar{..} = do
+    #{poke NumericVar, ndigits} ptr numVarNdigits
+    #{poke NumericVar, weight} ptr numVarWeight
+    #{poke NumericVar, sign} ptr numVarSign
+    #{poke NumericVar, dscale} ptr numVarDscale
+    #{poke NumericVar, digits} ptr numVarDigits
diff --git a/src/Database/PostgreSQL/PQTypes/Internal/Connection.hs b/src/Database/PostgreSQL/PQTypes/Internal/Connection.hs
--- a/src/Database/PostgreSQL/PQTypes/Internal/Connection.hs
+++ b/src/Database/PostgreSQL/PQTypes/Internal/Connection.hs
@@ -38,7 +38,6 @@
 import Data.String
 import Data.Text qualified as T
 import Data.Text.Encoding qualified as T
-import Foreign.C.String
 import Foreign.ForeignPtr
 import Foreign.Ptr
 import GHC.Clock (getMonotonicTime)
@@ -187,7 +186,7 @@
 -- 'disconnect', otherwise there will be a resource leak.
 connect :: ConnectionSettings -> IO Connection
 connect ConnectionSettings {..} = mask $ \unmask -> do
-  connPtr <- BS.useAsCString (T.encodeUtf8 csConnInfo) (openConnection unmask)
+  connPtr <- openConnection unmask $ T.encodeUtf8 csConnInfo
   (`onException` c_PQfinish connPtr) . unmask $ do
     status <- c_PQstatus connPtr
     when (status /= c_CONNECTION_OK) $
@@ -218,7 +217,7 @@
   where
     fname = "connect"
 
-    openConnection :: (forall r. IO r -> IO r) -> CString -> IO (Ptr PGconn)
+    openConnection :: (forall r. IO r -> IO r) -> BS.ByteString -> IO (Ptr PGconn)
     openConnection unmask conninfo = do
       -- We use synchronous version of connecting to the database using
       -- 'PQconnectdb' instead of 'PQconnectStart' and 'PQconnectPoll', because
@@ -230,11 +229,14 @@
       -- exception, so to guarantee prompt return in such scenario 'PQconnectdb'
       -- is run in a separate child thread. If the parent receives an exception
       -- while the child still runs, the child is signaled to clean up after
-      -- itself and left behind.
+      -- itself and left behind. This is why the child itself allocates the
+      -- buffer with the connection string. If the parent allocated it, the
+      -- exception would free it when it unwinds the stack, possibly while
+      -- 'PQconnectdb' still reads it.
       connVar <- newEmptyTMVarIO
       runningVar <- newTVarIO True
       _ <- forkIO $ do
-        conn <- c_PQconnectdb conninfo
+        conn <- BS.useAsCString conninfo c_PQconnectdb
         join . atomically $
           readTVar runningVar >>= \case
             True -> do
@@ -421,8 +423,18 @@
     _ | rst == c_PGRES_TUPLES_OK -> Right . fromIntegral <$> c_PQntuples res
     _ | rst == c_PGRES_FATAL_ERROR -> throwSQLError
     _ | rst == c_PGRES_BAD_RESPONSE -> throwSQLError
+    -- The library doesn't support the copy modes a COPY statement puts the
+    -- connection in. Erroring out is fine: when libpq executes the next
+    -- query, it terminates the copy mode internally.
+    _ | isCopyStatus rst -> do
+      rethrowWithContext sql pid . toException $
+        HPQTypesError "verifyResult: COPY statements are not supported"
     _ | otherwise -> pure . Left $ 0
   where
+    isCopyStatus :: ExecStatusType -> Bool
+    isCopyStatus st =
+      st == c_PGRES_COPY_IN || st == c_PGRES_COPY_OUT || st == c_PGRES_COPY_BOTH
+
     throwSQLError =
       rethrowWithContext sql pid
         =<< if res == nullPtr
diff --git a/src/Database/PostgreSQL/PQTypes/Internal/Error.hs b/src/Database/PostgreSQL/PQTypes/Internal/Error.hs
--- a/src/Database/PostgreSQL/PQTypes/Internal/Error.hs
+++ b/src/Database/PostgreSQL/PQTypes/Internal/Error.hs
@@ -3,6 +3,7 @@
   ( DetailedQueryError (..)
   , QueryError (..)
   , HPQTypesError (..)
+  , ThreadMismatchError (..)
   , LibPQError (..)
   , ConversionError (..)
   , ArrayItemError (..)
@@ -13,6 +14,7 @@
   , AffectedRowsMismatch (..)
   ) where
 
+import Control.Concurrent (ThreadId)
 import Control.Exception qualified as E
 import Data.Typeable
 
@@ -48,6 +50,27 @@
 instance Show HPQTypesError where
   show (HPQTypesError s) = "HPQTypesError (PostgreSQL): " <> s
 
+-- | Thrown when a thread other than the one that started a DB session uses it.
+data ThreadMismatchError = ThreadMismatchError
+  { tmeOwnerThread :: !ThreadId
+  -- ^ Thread that started the session.
+  , tmeCurrentThread :: !ThreadId
+  -- ^ Thread that used the session.
+  }
+  deriving (Eq)
+
+instance Show ThreadMismatchError where
+  show ThreadMismatchError {..} =
+    concat
+      [ "ThreadMismatchError (PostgreSQL): "
+      , show tmeCurrentThread
+      , " used a DB session that "
+      , show tmeOwnerThread
+      , " started. Only the thread that started a session can use it. "
+      , "To run queries from another thread, start a separate session there "
+      , "with withNewSession."
+      ]
+
 -- | Internal error in libpq/libpqtypes library.
 newtype LibPQError = LibPQError String
   deriving (Eq, Ord)
@@ -131,6 +154,7 @@
 instance E.Exception DetailedQueryError
 instance E.Exception QueryError
 instance E.Exception HPQTypesError
+instance E.Exception ThreadMismatchError
 instance E.Exception LibPQError
 instance E.Exception ConversionError
 instance E.Exception ArrayItemError
diff --git a/src/Database/PostgreSQL/PQTypes/Internal/Monad.hs b/src/Database/PostgreSQL/PQTypes/Internal/Monad.hs
--- a/src/Database/PostgreSQL/PQTypes/Internal/Monad.hs
+++ b/src/Database/PostgreSQL/PQTypes/Internal/Monad.hs
@@ -35,6 +35,12 @@
 
 -- | Evaluate monadic action with supplied
 -- connection source and transaction settings.
+--
+-- The calling thread owns the session. If another thread invokes a
+-- 'MonadDB' operation that uses the connection, the operation throws
+-- 'Database.PostgreSQL.PQTypes.ThreadMismatchError' wrapped in
+-- 'Database.PostgreSQL.PQTypes.DBException'. To run queries
+-- from another thread, start a separate session there with 'withNewSession'.
 runDBT
   :: (HasCallStack, MonadBase IO m, MonadMask m)
   => ConnectionSourceM m
@@ -56,10 +62,10 @@
 
 instance (m ~ n, MonadBase IO m, MonadMask m) => MonadDB (DBT_ m n) where
   runQuery sql = withFrozenCallStack $ do
-    DBT . StateT $ \st -> withConnection (dbConnectionData st) $ \conn -> do
+    DBT . StateT $ \st -> withConnection st $ \conn -> do
       liftBase $ updateStateWith conn st sql =<< runQueryIO conn sql
   runPreparedQuery name sql = withFrozenCallStack $ do
-    DBT . StateT $ \st -> withConnection (dbConnectionData st) $ \conn -> do
+    DBT . StateT $ \st -> withConnection st $ \conn -> do
       liftBase $ updateStateWith conn st sql =<< runPreparedQueryIO conn name sql
 
   getLastQuery = DBT . gets $ dbLastQuery
@@ -78,16 +84,16 @@
     (,st) <$> liftBase (getConnectionAcquisitionModeIO $ dbConnectionData st)
 
   acquireAndHoldConnection isolationLevel permissions = DBT . StateT $ \st -> do
-    (,st) <$> changeAcquisitionModeTo (AcquireAndHold isolationLevel permissions) (dbConnectionData st)
+    (,st) <$> changeAcquisitionModeTo (AcquireAndHold isolationLevel permissions) st
 
   unsafeAcquireOnDemandConnection = DBT . StateT $ \st -> do
-    (,st) <$> changeAcquisitionModeTo AcquireOnDemand (dbConnectionData st)
+    (,st) <$> changeAcquisitionModeTo AcquireOnDemand st
 
   getNotification time = DBT . StateT $ \st -> do
-    withConnection (dbConnectionData st) $ \conn -> do
+    withConnection st $ \conn -> do
       (,st) <$> liftBase (getNotificationIO conn time)
 
-  withNewConnection m = DBT . StateT $ \st -> do
+  withNewSession m = DBT . StateT $ \st -> do
     cam <- liftBase . getConnectionAcquisitionModeIO $ dbConnectionData st
     let cs = getConnectionSource $ dbConnectionData st
         ts =
diff --git a/src/Database/PostgreSQL/PQTypes/Internal/Notification.hsc b/src/Database/PostgreSQL/PQTypes/Internal/Notification.hsc
--- a/src/Database/PostgreSQL/PQTypes/Internal/Notification.hsc
+++ b/src/Database/PostgreSQL/PQTypes/Internal/Notification.hsc
@@ -94,7 +94,8 @@
       ptr <- c_PQnotifies connPtr
       if ptr /= nullPtr
         then do
-          msg <- peek ptr
-          c_PQfreemem ptr
+          -- Free the struct even if peek throws, e.g. when the channel name
+          -- or payload is not valid UTF-8.
+          msg <- peek ptr `E.finally` c_PQfreemem ptr
           pure $ Just msg
         else pure Nothing
diff --git a/src/Database/PostgreSQL/PQTypes/Internal/QueryResult.hs b/src/Database/PostgreSQL/PQTypes/Internal/QueryResult.hs
--- a/src/Database/PostgreSQL/PQTypes/Internal/QueryResult.hs
+++ b/src/Database/PostgreSQL/PQTypes/Internal/QueryResult.hs
@@ -17,7 +17,6 @@
 import Foreign.C.Types
 import Foreign.ForeignPtr
 import Foreign.Marshal.Alloc
-import Foreign.Ptr
 import GHC.Stack
 import System.IO.Unsafe
 
@@ -64,6 +63,10 @@
   foldl f acc = runIdentity . foldlImpl False (coerce f) acc
   foldl' f acc = runIdentity . foldlImpl True (coerce f) acc
 
+-- | Fold rows of a query result from the right. The fold decodes each row
+-- right before it passes it to the fold function, i.e. only after it folded
+-- the rows after it. No more rows than the fold function retains are alive at
+-- any time.
 foldrImpl
   :: (HasCallStack, Monad m)
   => Bool
@@ -71,8 +74,20 @@
   -> acc
   -> QueryResult t
   -> m acc
-foldrImpl = foldImpl (fmap pred . c_PQntuples) (const . pure $ -1) pred
+foldrImpl strict f iacc qr = worker iacc $ checkedNtuples qr - 1
+  where
+    worker acc i
+      | i < 0 = pure acc
+      | otherwise = do
+          let t = decodeRow qr i
+          acc' <- t `seq` f t acc
+          worker `apply` acc' $ i - 1
 
+    apply = if strict then ($!) else ($)
+
+-- | Fold rows of a query result from the left. The fold decodes each row
+-- right before it passes it to the fold function. No more rows than the fold
+-- function retains are alive at any time.
 foldlImpl
   :: (HasCallStack, Monad m)
   => Bool
@@ -80,23 +95,27 @@
   -> acc
   -> QueryResult t
   -> m acc
-foldlImpl strict = foldImpl (const $ pure 0) c_PQntuples succ strict . flip
+foldlImpl strict f iacc qr = worker iacc 0
+  where
+    n = checkedNtuples qr
+    worker acc i
+      | i == n = pure acc
+      | otherwise = do
+          let t = decodeRow qr i
+          acc' <- t `seq` f acc t
+          worker `apply` acc' $ i + 1
 
-foldImpl
-  :: (HasCallStack, Monad m)
-  => (Ptr PGresult -> IO CInt)
-  -> (Ptr PGresult -> IO CInt)
-  -> (CInt -> CInt)
-  -> Bool
-  -> (t -> acc -> m acc)
-  -> acc
-  -> QueryResult t
-  -> m acc
-foldImpl initCtr termCtr advCtr strict f iacc (QueryResult (SomeSQL ctx) pid fres g) =
+    apply = if strict then ($!) else ($)
+
+-- | The number of rows of a query result, after a comparison of its width
+-- with the row type. A mismatch throws 'RowLengthMismatch' with the query
+-- attached as context.
+--
+-- Both are pure information about the immutable query result, so
+-- 'unsafePerformIO' is fine here.
+checkedNtuples :: HasCallStack => QueryResult t -> CInt
+checkedNtuples (QueryResult (SomeSQL ctx) pid fres g) =
   unsafePerformIO . withForeignPtr fres $ \res -> do
-    -- This bit is referentially transparent iff appropriate
-    -- FrowRow and FromSQL instances are (the ones provided
-    -- by the library fulfil this requirement).
     rowlen <- fromIntegral <$> c_PQnfields res
     when (rowlen /= pqVariablesP rowp) $
       E.throwIO
@@ -110,22 +129,29 @@
                 }
           , dbeCallStack = callStack
           }
-    alloca $ \err -> do
-      n <- termCtr res
-      let worker acc i =
-            if i == n
-              then pure acc
-              else do
-                -- mask asynchronous exceptions so they won't be wrapped in DBException
-                obj <- E.mask_ (g <$> fromRow res err 0 i `E.catch` rethrowWithContext ctx pid)
-                worker `apply` (f obj =<< acc) $ advCtr i
-      worker (pure iacc) =<< initCtr res
+    c_PQntuples res
   where
     -- ⊥ of existential type hidden in QueryResult
     row = let _ = g row in row
     rowp = pure row
 
-    apply = if strict then ($!) else ($)
+-- | Decode a row of a query result and attach the query as context to
+-- exceptions thrown in the process.
+--
+-- Decoding is referentially transparent iff the FromRow and FromSQL instances
+-- are (the ones provided by the library fulfil this requirement), so
+-- 'unsafePerformIO' is fine here.
+--
+-- The caller must force the result right when it applies the fold function
+-- to it. The fold function itself is not obligated to force it, e.g.
+-- 'Database.PostgreSQL.PQTypes.Fold.fetchMany' doesn't. Without that,
+-- unforced thunks that retain the whole query result escape the fold, and
+-- decoding errors surface wherever the thunks are forced.
+decodeRow :: HasCallStack => QueryResult t -> CInt -> t
+decodeRow (QueryResult (SomeSQL ctx) pid fres g) i =
+  unsafePerformIO . withForeignPtr fres $ \res -> alloca $ \err -> do
+    -- mask asynchronous exceptions so they won't be wrapped in DBException
+    E.mask_ $ (g <$> fromRow res err 0 i) `E.catch` rethrowWithContext ctx pid
 
 -- Note: c_PQntuples/c_PQnfields are pure on a C level and QueryResult
 -- constructor is not exported to the end user (so it's not possible
diff --git a/src/Database/PostgreSQL/PQTypes/Internal/State.hs b/src/Database/PostgreSQL/PQTypes/Internal/State.hs
--- a/src/Database/PostgreSQL/PQTypes/Internal/State.hs
+++ b/src/Database/PostgreSQL/PQTypes/Internal/State.hs
@@ -14,11 +14,13 @@
   , updateStateWith
   ) where
 
+import Control.Concurrent qualified as C
 import Control.Concurrent.MVar.Lifted
 import Control.Monad
 import Control.Monad.Base
 import Control.Monad.Catch
 import Data.Function
+import Data.IORef
 import Data.Typeable
 import Foreign.ForeignPtr
 import GHC.Stack
@@ -28,8 +30,10 @@
 import Database.PostgreSQL.PQTypes.Internal.BackendPid
 import Database.PostgreSQL.PQTypes.Internal.C.Types
 import Database.PostgreSQL.PQTypes.Internal.Connection
+import Database.PostgreSQL.PQTypes.Internal.Error
 import Database.PostgreSQL.PQTypes.Internal.Exception
 import Database.PostgreSQL.PQTypes.Internal.QueryResult
+import Database.PostgreSQL.PQTypes.Internal.Utils
 import Database.PostgreSQL.PQTypes.SQL
 import Database.PostgreSQL.PQTypes.SQL.Class
 import Database.PostgreSQL.PQTypes.Transaction.Settings
@@ -103,6 +107,12 @@
 data ConnectionData m = forall cdata. ConnectionData
   { cdConnectionSource :: !(InternalConnectionSource m cdata)
   , cdConnectionState :: !(MVar (ConnectionState cdata))
+  , cdAcquisitionMode :: !(IORef ConnectionAcquisitionMode)
+  -- ^ Kept apart from the connection state, so that
+  -- 'Database.PostgreSQL.PQTypes.Class.withNewSession' of a child thread
+  -- does not wait for a query of the parent and works after the session ended.
+  , cdOwnerThread :: !C.ThreadId
+  -- ^ Thread that started the session. Only this thread can use it.
   }
 
 getConnectionSource :: ConnectionData m -> ConnectionSourceM m
@@ -112,12 +122,7 @@
   :: HasCallStack
   => ConnectionData m
   -> IO ConnectionAcquisitionMode
-getConnectionAcquisitionModeIO ConnectionData {..} = do
-  readMVar cdConnectionState >>= \case
-    OnDemand -> pure AcquireOnDemand
-    Acquired isolationLevel permissions _ _ -> do
-      pure $ AcquireAndHold isolationLevel permissions
-    Finalized -> error "finalized connection"
+getConnectionAcquisitionModeIO ConnectionData {..} = readIORef cdAcquisitionMode
 
 withConnectionData
   :: (HasCallStack, MonadBase IO m, MonadMask m)
@@ -126,13 +131,23 @@
   -> (ConnectionData m -> m r)
   -> m r
 withConnectionData cs ts action = (`fix` 1) $ \loop n -> do
-  let maybeRestart = case tsRestartPredicate ts of
-        Just _ -> handleJust (expred n) $ \_ -> loop $ n + 1
-        Nothing -> id
-  maybeRestart
-    . fmap fst
-    . generalBracket (initConnectionData cs cam) finalizeConnectionData
-    $ action
+  eres <-
+    try
+      . fmap fst
+      . generalBracket
+        (initConnectionData cs cam)
+        (\cd ec -> runCleanup ec $ finalizeConnectionData cd ec)
+      $ action
+  case eres of
+    Right res -> pure res
+    -- The restart happens outside of the exception handler, so that the
+    -- retried transaction doesn't run with asynchronous exceptions masked.
+    -- An asynchronous exception never triggers a restart. The restart
+    -- predicate can match it if it's instantiated at SomeException.
+    Left e
+      | isAsyncException e -> throwM e
+      | Just () <- expred n e -> loop $ n + 1
+      | otherwise -> throwM e
   where
     cam = tsConnectionAcquisitionMode ts
 
@@ -154,48 +169,62 @@
 changeAcquisitionModeTo
   :: (HasCallStack, MonadBase IO m, MonadMask m)
   => ConnectionAcquisitionMode
-  -> ConnectionData m
+  -> DBState m
   -> m ()
-changeAcquisitionModeTo cam ConnectionData {..} = do
-  bracketOnError (takeMVar cdConnectionState) (putMVar cdConnectionState) $ \case
-    OnDemand -> case cam of
-      AcquireOnDemand -> putMVar cdConnectionState OnDemand
-      _ -> mask_ $ do
-        -- Need to mask, if asynchronous exception arrives between
-        -- initConnectionState and putMVar, the connection leaks.
-        newConnState <- initConnectionState cdConnectionSource cam
-        putMVar cdConnectionState newConnState
-    connState@(Acquired isolationLevel permissions _ _) -> case cam of
-      AcquireOnDemand -> mask_ $ do
-        -- Need to mask, if asynchronous exception arrives between
-        -- finalizeConnectionState and putMVar, we end up with an invalid
-        -- (finalized) connection state.
-        finalizeConnectionState cdConnectionSource (ExitCaseSuccess ()) connState
-        putMVar cdConnectionState OnDemand
-      AcquireAndHold newIsolationLevel newPermissions -> do
-        when (isolationLevel /= newIsolationLevel) $ do
-          error $
-            "isolation level mismatch (current: "
-              ++ show isolationLevel
-              ++ ", new: "
-              ++ show newIsolationLevel
-              ++ ")"
-        when (permissions /= newPermissions) $ do
-          error $
-            "permissions mismatch (current: "
-              ++ show permissions
-              ++ ", new: "
-              ++ show newPermissions
-              ++ ")"
-        putMVar cdConnectionState connState
-    Finalized -> error "finalized connection"
+changeAcquisitionModeTo cam st@DBState {dbConnectionData = ConnectionData {..}} = mask_ $ do
+  checkSessionOwner st
+  -- Each branch of 'mkNewState' determines the new connection state along with
+  -- a follow-up action. The new state is installed before the follow-up action
+  -- runs, because the action can throw, e.g. to rethrow a failed COMMIT.
+  connState <- takeMVar cdConnectionState
+  (newConnState, after) <-
+    mkNewState connState `onException` putMVar cdConnectionState connState
+  putMVar cdConnectionState newConnState
+  liftBase $ atomicWriteIORef cdAcquisitionMode cam
+  after
+  where
+    mkNewState = \case
+      OnDemand -> case cam of
+        AcquireOnDemand -> pure (OnDemand, pure ())
+        AcquireAndHold {} -> do
+          -- If initConnectionState throws, no connection is held, so the
+          -- state stays OnDemand (restored by the onException handler above).
+          newConnState <- initConnectionState cdConnectionSource cam
+          pure (newConnState, pure ())
+      connState@(Acquired isolationLevel permissions _ _) -> case cam of
+        AcquireOnDemand -> do
+          -- If finalizeConnectionState throws (e.g. COMMIT fails), it has
+          -- already returned the connection to the source, so the state needs
+          -- to become OnDemand either way, otherwise it would refer to an
+          -- invalid connection.
+          eres :: Either SomeException () <- try $ do
+            finalizeConnectionState cdConnectionSource (ExitCaseSuccess ()) connState
+          pure (OnDemand, either throwM pure eres)
+        AcquireAndHold newIsolationLevel newPermissions -> do
+          when (isolationLevel /= newIsolationLevel) $ do
+            error $
+              "isolation level mismatch (current: "
+                ++ show isolationLevel
+                ++ ", new: "
+                ++ show newIsolationLevel
+                ++ ")"
+          when (permissions /= newPermissions) $ do
+            error $
+              "permissions mismatch (current: "
+                ++ show permissions
+                ++ ", new: "
+                ++ show newPermissions
+                ++ ")"
+          pure (connState, pure ())
+      Finalized -> error "finalized connection"
 
 withConnection
   :: (HasCallStack, MonadBase IO m, MonadMask m)
-  => ConnectionData m
+  => DBState m
   -> (Connection -> m r)
   -> m r
-withConnection ConnectionData {..} action = do
+withConnection st@DBState {dbConnectionData = ConnectionData {..}} action = do
+  checkSessionOwner st
   bracket (takeMVar cdConnectionState) (putMVar cdConnectionState) $ \case
     OnDemand ->
       fst
@@ -203,25 +232,61 @@
           (takeConnection cdConnectionSource)
           (putConnection cdConnectionSource)
           ( \(conn, _cdata) ->
-              bracket_
-                (liftBase . uninterruptibleMask_ $ runQueryIO @SQL conn "BEGIN READ ONLY")
-                (liftBase . uninterruptibleMask_ $ runQueryIO @SQL conn "ROLLBACK")
-                (action conn)
+              fmap fst
+                . generalBracket
+                  (autoQuery conn "BEGIN READ ONLY")
+                  -- If the ROLLBACK fails after the action failed, the
+                  -- connection returns to its source as failed either way,
+                  -- and the source disposes of it.
+                  (\() ec -> runCleanup ec $ autoQuery conn "ROLLBACK")
+                $ \() -> action conn
           )
     Acquired _ _ conn _ -> action conn
     Finalized -> error "finalized connection"
+  where
+    -- The queries that delimit the automatic transaction can't be
+    -- interrupted, otherwise the connection would end up in an unexpected
+    -- transaction state.
+    autoQuery :: MonadBase IO m => Connection -> SQL -> m ()
+    autoQuery conn sql =
+      liftBase . uninterruptibleMask_ . void $ runQueryIO @SQL conn sql
 
+checkSessionOwner
+  :: (HasCallStack, MonadBase IO m, MonadThrow m)
+  => DBState m
+  -> m ()
+checkSessionOwner DBState {dbConnectionData = ConnectionData {..}, ..} = do
+  currentThread <- liftBase C.myThreadId
+  when (currentThread /= cdOwnerThread) $ do
+    case dbLastQuery of
+      (pid, SomeSQL sql) ->
+        throwM
+          DBException
+            { dbeQueryContext = sql
+            , dbeBackendPid = pid
+            , dbeError =
+                ThreadMismatchError
+                  { tmeOwnerThread = cdOwnerThread
+                  , tmeCurrentThread = currentThread
+                  }
+            , dbeCallStack = callStack
+            }
+
 initConnectionData
   :: (MonadBase IO m, MonadMask m)
   => ConnectionSourceM m
   -> ConnectionAcquisitionMode
   -> m (ConnectionData m)
 initConnectionData (ConnectionSourceM ics) cam = do
+  ownerThread <- liftBase C.myThreadId
   connState <- newMVar =<< initConnectionState ics cam
+  acquisitionMode <- liftBase $ newIORef cam
   pure $
     ConnectionData
       { cdConnectionSource = ics
       , cdConnectionState = connState
+      , cdAcquisitionMode = acquisitionMode
+      , cdOwnerThread = ownerThread
       }
 
 finalizeConnectionData
@@ -230,9 +295,9 @@
   -> ExitCase r
   -> m ()
 finalizeConnectionData ConnectionData {..} ec = do
-  (`finally` putMVar cdConnectionState Finalized) $ do
-    connState <- takeMVar cdConnectionState
-    finalizeConnectionState cdConnectionSource ec connState
+  connState <- takeMVar cdConnectionState
+  finalizeConnectionState cdConnectionSource ec connState
+    `finally` putMVar cdConnectionState Finalized
 
 ----------------------------------------
 
diff --git a/src/Database/PostgreSQL/PQTypes/Internal/Utils.hs b/src/Database/PostgreSQL/PQTypes/Internal/Utils.hs
--- a/src/Database/PostgreSQL/PQTypes/Internal/Utils.hs
+++ b/src/Database/PostgreSQL/PQTypes/Internal/Utils.hs
@@ -6,6 +6,8 @@
   , cStringLenToBytea
   , byteaToCStringLen
   , textToCString
+  , numericVarToInteger
+  , withIntegerAsNumericVar
   , verifyPQTRes
   , withPGparam
   , throwLibPQError
@@ -13,15 +15,20 @@
   , rethrowWithArrayError
   , hpqTypesError
   , unexpectedNULL
+  , isAsyncException
+  , catchSync
+  , runCleanup
   ) where
 
 import Control.Exception qualified as E
 import Control.Monad
+import Control.Monad.Catch
 import Data.ByteString.Unsafe
 import Data.Kind (Type)
 import Data.Maybe
 import Data.Text qualified as T
 import Data.Text.Encoding qualified as T
+import Data.Vector.Storable qualified as V
 import Foreign.C
 import Foreign.ForeignPtr
 import Foreign.Marshal.Alloc
@@ -44,6 +51,36 @@
   MkConstraint m '[] = ()
   MkConstraint m (c ': cs) = (c m, MkConstraint m cs)
 
+-- | Whether an exception is asynchronous.
+isAsyncException :: E.SomeException -> Bool
+isAsyncException e = case E.fromException e of
+  Just E.SomeAsyncException {} -> True
+  Nothing -> False
+
+-- | Like 'catch' with a handler for any exception. An asynchronous exception
+-- bypasses the handler and propagates.
+catchSync :: MonadCatch m => m a -> (E.SomeException -> m a) -> m a
+catchSync action handler =
+  action `catch` \e ->
+    if isAsyncException e
+      then throwM e
+      else handler e
+
+-- | Run the cleanup of a bracket, given the exit case of the bracketed
+-- action.
+--
+-- If the action succeeded, a failure of the cleanup propagates. If the action
+-- failed, its own exception must propagate instead, so a synchronous failure
+-- of the cleanup is dropped. Without this, the failure of the cleanup would
+-- replace the exception of the action, and e.g. a restart predicate would
+-- never see it. This happens in practice: when the connection died, both the
+-- action and its cleanup fail. An asynchronous exception delivered during the
+-- cleanup still propagates, so that e.g. a thread cancellation is not lost.
+runCleanup :: MonadCatch m => ExitCase a -> m () -> m ()
+runCleanup ec cleanup = case ec of
+  ExitCaseSuccess _ -> cleanup
+  _ -> cleanup `catchSync` \_ -> pure ()
+
 -- Safely read value.
 mread :: Read a => String -> Maybe a
 mread s = do
@@ -80,6 +117,70 @@
     copyBytes ptr cs len
     pokeByteOff ptr len (0 :: CChar)
   pure fptr
+
+----------------------------------------
+
+-- Note: these can be generalized to convert from/to Scientific and support
+-- arbitrary floating point precision (relevant code can be borrowed from the
+-- postgresql-binary package), but while deserialization is easy either way,
+-- serialization is significantly more annoying, so let's leave this until it's
+-- actually needed.
+
+numericVarToInteger :: NumericVar -> IO Integer
+numericVarToInteger NumericVar {..}
+  | numVarDscale /= 0 = hpqTypesError "not an integer"
+  | numVarSign == c_NUMERIC_NAN = hpqTypesError "not a number"
+  | numVarNdigits > numVarWeight + 1 =
+      hpqTypesError $
+        "digits after the decimal point (ndigits: "
+          ++ show numVarNdigits
+          ++ ", weight: "
+          ++ show numVarWeight
+          ++ ")"
+  | numVarSign == c_NUMERIC_POS = scale <$> mkInteger 0 numVarDigits numVarNdigits
+  | numVarSign == c_NUMERIC_NEG = negate . scale <$> mkInteger 0 numVarDigits numVarNdigits
+  | otherwise = hpqTypesError $ "unexpected sign: " ++ show numVarSign
+  where
+    -- The wire value is Σ digits[i] * 10000^(weight - i) and the server strips
+    -- trailing zero digit groups, so ndigits can be smaller than weight + 1.
+    scale :: Integer -> Integer
+    scale acc = case numVarWeight + 1 - numVarNdigits of
+      0 -> acc
+      n -> acc * 10000 ^ n
+
+    mkInteger :: Integer -> Ptr CShort -> CShort -> IO Integer
+    mkInteger acc ptr = \case
+      0 -> pure acc
+      n -> do
+        v <- ntohs <$> peek ptr
+        when (v < 0 || v > 9999) $ do
+          hpqTypesError $ "invalid digit: " ++ show v
+        mkInteger (acc * 10000 + fromIntegral v) (ptr `plusPtr` 2) (n - 1)
+
+withIntegerAsNumericVar :: Integer -> (NumericVar -> IO r) -> IO r
+withIntegerAsNumericVar n k = V.unsafeWith digits $ \digitsPtr -> do
+  k $
+    NumericVar
+      { numVarNdigits = digitsLen
+      , numVarWeight = max 0 (digitsLen - 1)
+      , numVarSign = if n < 0 then c_NUMERIC_NEG else c_NUMERIC_POS
+      , numVarDscale = 0
+      , numVarDigits = digitsPtr
+      }
+  where
+    digitsLen :: CShort
+    digitsLen = fromIntegral $ V.length digits
+
+    digits :: V.Vector CShort
+    digits = V.reverse . (`V.unfoldr` abs n) $ \case
+      0 -> Nothing
+      x -> case x `quotRem` 10000 of
+        (d, m) -> Just (htons $ fromIntegral m, d)
+
+foreign import ccall unsafe "htons" htons :: CShort -> CShort
+foreign import ccall unsafe "ntohs" ntohs :: CShort -> CShort
+
+----------------------------------------
 
 -- | Check return value of a function from libpqtypes
 -- and if it indicates an error, throw appropriate exception.
diff --git a/src/Database/PostgreSQL/PQTypes/JSON.hs b/src/Database/PostgreSQL/PQTypes/JSON.hs
--- a/src/Database/PostgreSQL/PQTypes/JSON.hs
+++ b/src/Database/PostgreSQL/PQTypes/JSON.hs
@@ -1,104 +1,168 @@
 module Database.PostgreSQL.PQTypes.JSON
-  ( JSON (..)
+  ( -- * Helpers, to be used with @deriving via@ (@-XDerivingVia@).
+    JSON (..)
   , JSONB (..)
-  , aesonFromSQL
-  , aesonToSQL
+
+    -- * Unparsed JSON values
+  , RawJSON (..)
+  , encodeRawJSON
+  , decodeRawJSON
+  , eitherDecodeRawJSON
+  , RawJSONB (..)
+  , encodeRawJSONB
+  , decodeRawJSONB
+  , eitherDecodeRawJSONB
   ) where
 
-import Control.Exception qualified as E
 import Data.Aeson
 import Data.ByteString.Char8 qualified as BS
 import Data.ByteString.Lazy.Char8 qualified as BSL
+import Data.Typeable
 import Foreign.Ptr
 
 import Database.PostgreSQL.PQTypes.Format
 import Database.PostgreSQL.PQTypes.FromSQL
 import Database.PostgreSQL.PQTypes.Internal.C.Types
+import Database.PostgreSQL.PQTypes.Internal.Utils
 import Database.PostgreSQL.PQTypes.ToSQL
 
--- | Wrapper for (de)serializing underlying type as 'json'.
-newtype JSON json = JSON {unJSON :: json}
+-- | Wrapper that serializes and deserializes the underlying type as @json@
+-- with its 'ToJSON' and 'FromJSON' instances.
+--
+-- /Note:/ To get the SQL instances for a type of your own, use
+-- @DerivingVia@:
+--
+-- @
+-- data Foo = ...
+--   deriving anyclass (FromJSON, ToJSON)
+--   deriving (PQFormat, ToSQL, FromSQL) via JSON Foo
+-- @
+newtype JSON a = JSON {unJSON :: a}
   deriving (Eq, Functor, Ord, Show)
 
-instance PQFormat (JSON json) where
+instance PQFormat (JSON a) where
   pqFormat = BS.pack "%json"
 
-instance FromSQL (JSON BS.ByteString) where
-  type PQBase (JSON BS.ByteString) = PGbytea
-  fromSQL = fmap JSON . fromSQL
-
-instance FromSQL (JSON BSL.ByteString) where
-  type PQBase (JSON BSL.ByteString) = PGbytea
-  fromSQL = fmap JSON . fromSQL
-
-instance ToSQL (JSON BS.ByteString) where
-  type PQDest (JSON BS.ByteString) = PGbytea
-  toSQL = toSQL . unJSON
-
-instance ToSQL (JSON BSL.ByteString) where
-  type PQDest (JSON BSL.ByteString) = PGbytea
-  toSQL = toSQL . unJSON
-
-instance FromSQL (JSON Value) where
-  type PQBase (JSON Value) = PGbytea
+instance (FromJSON a, Typeable a) => FromSQL (JSON a) where
+  type PQBase (JSON a) = PGbytea
   fromSQL = fmap JSON . aesonFromSQL
 
-instance ToSQL (JSON Value) where
-  type PQDest (JSON Value) = PGbytea
+instance ToJSON a => ToSQL (JSON a) where
+  type PQDest (JSON a) = PGbytea
   toSQL = aesonToSQL . unJSON
 
 ----------------------------------------
 
--- | Wrapper for (de)serializing underlying type as 'jsonb'.
-newtype JSONB jsonb = JSONB {unJSONB :: jsonb}
+-- | Wrapper that serializes and deserializes the underlying type as @jsonb@
+-- with its 'ToJSON' and 'FromJSON' instances.
+--
+-- /Note:/ To get the SQL instances for a type of your own, use
+-- @DerivingVia@:
+--
+-- @
+-- data Foo = ...
+--   deriving anyclass (FromJSON, ToJSON)
+--   deriving (PQFormat, ToSQL, FromSQL) via JSONB Foo
+-- @
+newtype JSONB a = JSONB {unJSONB :: a}
   deriving (Eq, Functor, Ord, Show)
 
-instance PQFormat (JSONB jsonb) where
+instance PQFormat (JSONB a) where
   pqFormat = BS.pack "%jsonb"
 
-instance FromSQL (JSONB BS.ByteString) where
-  type PQBase (JSONB BS.ByteString) = PGbytea
-  fromSQL = fmap JSONB . fromSQL
+instance (FromJSON a, Typeable a) => FromSQL (JSONB a) where
+  type PQBase (JSONB a) = PGbytea
+  fromSQL = fmap JSONB . aesonFromSQL
 
-instance FromSQL (JSONB BSL.ByteString) where
-  type PQBase (JSONB BSL.ByteString) = PGbytea
-  fromSQL = fmap JSONB . fromSQL
+instance ToJSON a => ToSQL (JSONB a) where
+  type PQDest (JSONB a) = PGbytea
+  toSQL = aesonToSQL . unJSONB
 
-instance ToSQL (JSONB BS.ByteString) where
-  type PQDest (JSONB BS.ByteString) = PGbytea
-  toSQL = toSQL . unJSONB
+----------------------------------------
 
-instance ToSQL (JSONB BSL.ByteString) where
-  type PQDest (JSONB BSL.ByteString) = PGbytea
-  toSQL = toSQL . unJSONB
+-- | A @json@ value as its unparsed UTF-8 text.
+newtype RawJSON = RawJSON {unRawJSON :: BS.ByteString}
+  deriving (Eq, Ord, Show)
 
-instance FromSQL (JSONB Value) where
-  type PQBase (JSONB Value) = PGbytea
-  fromSQL = fmap JSONB . aesonFromSQL
+instance PQFormat RawJSON where
+  pqFormat = BS.pack "%json"
 
-instance ToSQL (JSONB Value) where
-  type PQDest (JSONB Value) = PGbytea
-  toSQL = aesonToSQL . unJSONB
+instance FromSQL RawJSON where
+  type PQBase RawJSON = PGbytea
+  fromSQL = fmap RawJSON . fromSQL
 
+instance ToSQL RawJSON where
+  type PQDest RawJSON = PGbytea
+  toSQL = toSQL . unRawJSON
+
+-- | Encode a value with its 'ToJSON' instance.
+--
+-- @since 1.15.0.0
+encodeRawJSON :: ToJSON a => a -> RawJSON
+encodeRawJSON = RawJSON . BSL.toStrict . encode
+
+-- | Decode a value with its 'FromJSON' instance.
+--
+-- @since 1.15.0.0
+decodeRawJSON :: FromJSON a => RawJSON -> Maybe a
+decodeRawJSON = decodeStrict' . unRawJSON
+
+-- | Decode a value with its 'FromJSON' instance and report the reason for a
+-- failure.
+--
+-- @since 1.15.0.0
+eitherDecodeRawJSON :: FromJSON a => RawJSON -> Either String a
+eitherDecodeRawJSON = eitherDecodeStrict' . unRawJSON
+
 ----------------------------------------
 
--- | Helper for defining 'FromSQL' instance for a type with 'FromJSON' instance.
+-- | A @jsonb@ value as its unparsed UTF-8 text.
+newtype RawJSONB = RawJSONB {unRawJSONB :: BS.ByteString}
+  deriving (Eq, Ord, Show)
+
+instance PQFormat RawJSONB where
+  pqFormat = BS.pack "%jsonb"
+
+instance FromSQL RawJSONB where
+  type PQBase RawJSONB = PGbytea
+  fromSQL = fmap RawJSONB . fromSQL
+
+instance ToSQL RawJSONB where
+  type PQDest RawJSONB = PGbytea
+  toSQL = toSQL . unRawJSONB
+
+-- | Encode a value with its 'ToJSON' instance.
 --
--- @since 1.9.1.0
-aesonFromSQL :: FromJSON t => Maybe PGbytea -> IO t
-aesonFromSQL mbase = do
-  evalue <- eitherDecodeStrict' <$> fromSQL mbase
-  case evalue of
-    Left err -> E.throwIO . E.ErrorCall $ "aesonFromSQL: " ++ err
-    Right value -> pure value
+-- /Note:/ The server normalizes a @jsonb@ value on input, e.g. it reorders the
+-- keys of an object and drops insignificant whitespace. The result of
+-- 'encodeRawJSONB' therefore differs in general from the text that the server
+-- returns for the same value.
+--
+-- @since 1.15.0.0
+encodeRawJSONB :: ToJSON a => a -> RawJSONB
+encodeRawJSONB = RawJSONB . BSL.toStrict . encode
 
--- | Helper for defining 'ToSQL' instance for a type with 'ToJSON' instance.
+-- | Decode a value with its 'FromJSON' instance.
 --
--- @since 1.9.1.0
-aesonToSQL
-  :: ToJSON t
-  => t
-  -> ParamAllocator
-  -> (Ptr PGbytea -> IO r)
-  -> IO r
+-- @since 1.15.0.0
+decodeRawJSONB :: FromJSON a => RawJSONB -> Maybe a
+decodeRawJSONB = decodeStrict' . unRawJSONB
+
+-- | Decode a value with its 'FromJSON' instance and report the reason for a
+-- failure.
+--
+-- @since 1.15.0.0
+eitherDecodeRawJSONB :: FromJSON a => RawJSONB -> Either String a
+eitherDecodeRawJSONB = eitherDecodeStrict' . unRawJSONB
+
+----------------------------------------
+
+aesonFromSQL :: forall a. (FromJSON a, Typeable a) => Maybe PGbytea -> IO a
+aesonFromSQL mbase = do
+  v <- fromSQL mbase
+  case eitherDecodeStrict' v of
+    Right a -> pure a
+    Left err -> hpqTypesError $ "aesonFromSQL (" ++ show (typeRep $ Proxy @a) ++ "): " ++ err
+
+aesonToSQL :: ToJSON a => a -> ParamAllocator -> (Ptr PGbytea -> IO r) -> IO r
 aesonToSQL = toSQL . BSL.toStrict . encode
diff --git a/src/Database/PostgreSQL/PQTypes/ToSQL.hs b/src/Database/PostgreSQL/PQTypes/ToSQL.hs
--- a/src/Database/PostgreSQL/PQTypes/ToSQL.hs
+++ b/src/Database/PostgreSQL/PQTypes/ToSQL.hs
@@ -84,6 +84,22 @@
   type PQDest Double = CDouble
   toSQL n _ = putAsPtr (realToFrac n)
 
+instance ToSQL Word16 where
+  type PQDest Word16 = CUShort
+  toSQL n _ = putAsPtr (fromIntegral n)
+
+instance ToSQL Word32 where
+  type PQDest Word32 = CUInt
+  toSQL n _ = putAsPtr (fromIntegral n)
+
+instance ToSQL Word64 where
+  type PQDest Word64 = CULLong
+  toSQL n _ = putAsPtr (fromIntegral n)
+
+instance ToSQL Integer where
+  type PQDest Integer = NumericVar
+  toSQL n _ k = withIntegerAsNumericVar n $ \nv -> putAsPtr nv k
+
 -- CHAR
 
 instance ToSQL Char where
diff --git a/src/Database/PostgreSQL/PQTypes/Transaction.hs b/src/Database/PostgreSQL/PQTypes/Transaction.hs
--- a/src/Database/PostgreSQL/PQTypes/Transaction.hs
+++ b/src/Database/PostgreSQL/PQTypes/Transaction.hs
@@ -13,7 +13,8 @@
 
 import Data.Monoid.Utils
 import Database.PostgreSQL.PQTypes.Class
-import Database.PostgreSQL.PQTypes.Internal.Error
+import Database.PostgreSQL.PQTypes.Internal.Exception
+import Database.PostgreSQL.PQTypes.Internal.Utils
 import Database.PostgreSQL.PQTypes.SQL.Raw
 import Database.PostgreSQL.PQTypes.Transaction.Settings
 import Database.PostgreSQL.PQTypes.Utils
@@ -34,9 +35,12 @@
   fst
     <$> generalBracket
       (runQuery_ $ "SAVEPOINT" <+> savepoint)
-      ( \() -> \case
-          ExitCaseSuccess _ -> runQuery_ sqlReleaseSavepoint
-          _ -> rollbackAndReleaseSavepoint
+      ( \() ec ->
+          -- Hard mask asynchronous exceptions, otherwise the queries below can
+          -- be interrupted and leave the savepoint in an unexpected state.
+          uninterruptibleMask_ . runCleanup ec $ case ec of
+            ExitCaseSuccess _ -> runQuery_ sqlReleaseSavepoint
+            _ -> rollbackAndReleaseSavepoint
       )
       (\() -> m)
   where
@@ -78,7 +82,7 @@
   getConnectionAcquisitionMode >>= \case
     AcquireOnDemand -> pure ()
     AcquireAndHold {} -> uninterruptibleMask_ $ do
-      runSQL_ "COMMIT"
+      runSQL_ "COMMIT" `onException` beginNoException
       begin
 
 -- | Rollback active transaction using given transaction settings.
@@ -87,7 +91,7 @@
   getConnectionAcquisitionMode >>= \case
     AcquireOnDemand -> pure ()
     AcquireAndHold {} -> uninterruptibleMask_ $ do
-      runSQL_ "ROLLBACK"
+      runSQL_ "ROLLBACK" `onException` beginNoException
       begin
 
 -- | Run a block of code without an open transaction.
@@ -102,7 +106,19 @@
   getConnectionAcquisitionMode >>= \case
     AcquireOnDemand -> action
     AcquireAndHold {} ->
-      bracket_
-        (uninterruptibleMask_ $ runSQL_ "COMMIT")
-        begin
-        action
+      fst
+        <$> generalBracket
+          (uninterruptibleMask_ $ runSQL_ "COMMIT" `onException` beginNoException)
+          (\() ec -> runCleanup ec begin)
+          (\() -> action)
+
+----------------------------------------
+-- Helpers
+
+-- If COMMIT (or ROLLBACK) fails, e.g. with a serialization error, the server
+-- has already rolled the transaction back, so start a new one to uphold the
+-- invariant that a transaction is always active in the AcquireAndHold mode. If
+-- that fails as well (e.g. because the connection is gone), suppress the error
+-- so that the original exception propagates.
+beginNoException :: (HasCallStack, MonadDB m, MonadMask m) => m ()
+beginNoException = begin `catch` \DBException {} -> pure ()
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -3,6 +3,7 @@
 module Main (main) where
 
 import Control.Concurrent.Lifted
+import Control.Exception qualified as E
 import Control.Monad
 import Control.Monad.Base
 import Control.Monad.Catch
@@ -13,6 +14,7 @@
 import Data.Char
 import Data.Function
 import Data.Int
+import Data.List qualified as L
 import Data.Maybe
 import Data.Text qualified as T
 import Data.Time
@@ -245,6 +247,7 @@
     , scrollableCursorWorks
     , withHoldCursorWorks
     , doubleCloseWorks
+    , cleanupDoesNotMaskErrors
     ]
   where
     basicCursorWorks = testCase "Basic cursor works" $ do
@@ -303,6 +306,20 @@
           -- Commiting a transaction closes the cursor
           commit
 
+    cleanupDoesNotMaskErrors = testCase "Cursor cleanup doesn't mask the original error" $ do
+      runTestEnv td defaultTransactionSettings $ do
+        -- The failing query puts the transaction in the aborted state, in
+        -- which closing the cursor fails with in_failed_sql_transaction. The
+        -- original error needs to propagate regardless.
+        eres <- try . withCursorSQL "ints" NoScroll NoHold (sqlGenInts 5) $ \_cursor -> do
+          runSQL_ "SELECT 1/0"
+        liftBase $ case eres :: Either DBException () of
+          Left DBException {..}
+            | Just DetailedQueryError {..} <- cast dbeError -> do
+                assertEqualEq "Unexpected error code" DivisionByZero qeErrorCode
+            | otherwise -> assertFailure $ "Unexpected exception: " ++ show dbeError
+          Right () -> assertFailure "DBException wasn't thrown"
+
 queryInterruptionTest :: TestData -> Test
 queryInterruptionTest td = testCase "Queries are interruptible" $ do
   let sleep = "SELECT pg_sleep(2)"
@@ -327,7 +344,7 @@
   $ do
     let sint = Identity (1 :: Int32)
     runQuery_ $ rawSQL "INSERT INTO test1_ (a) VALUES ($1)" sint
-    withNewConnection $ do
+    withNewSession $ do
       n <- runQuery $ rawSQL "SELECT a FROM test1_ WHERE a = $1" sint
       assertEqualEq "Other connection sees autocommited data" 1 n
     runQuery_ $ rawSQL "DELETE FROM test1_ WHERE a = $1" sint
@@ -433,10 +450,40 @@
     res2 <- fetchMany runIdentity
     assertEqualEq "Result of all queries is visible" [int1, int2] res2
 
+savepointDeadConnectionTest :: TestData -> Test
+savepointDeadConnectionTest td = testCase
+  "Failed savepoint cleanup doesn't mask the error of the action"
+  $ do
+    -- The action kills its own backend. The savepoint cleanup fails as well,
+    -- because the connection is gone. The error of the action must propagate
+    -- regardless.
+    eres <- try . runTestEnv td defaultTransactionSettings . withSavepoint "test" $ do
+      runSQL_ "SELECT pg_terminate_backend(pg_backend_pid())"
+    case eres of
+      Left DBException {..} ->
+        assertBool ("Exception comes from the action: " ++ show dbeQueryContext) $
+          "pg_terminate_backend" `L.isInfixOf` show dbeQueryContext
+      Right () -> assertFailure "DBException wasn't thrown"
+
+withoutTransactionDeadConnectionTest :: TestData -> Test
+withoutTransactionDeadConnectionTest td = testCase
+  "Failed BEGIN after unsafeWithoutTransaction doesn't mask the error of the action"
+  $ do
+    -- The action kills its own backend. The BEGIN that restores the
+    -- transaction fails as well, because the connection is gone. The error of
+    -- the action must propagate regardless.
+    eres <- try . runTestEnv td defaultTransactionSettings . unsafeWithoutTransaction $ do
+      runSQL_ "SELECT pg_terminate_backend(pg_backend_pid())"
+    case eres of
+      Left DBException {..} ->
+        assertBool ("Exception comes from the action: " ++ show dbeQueryContext) $
+          "pg_terminate_backend" `L.isInfixOf` show dbeQueryContext
+      Right () -> assertFailure "DBException wasn't thrown"
+
 notifyTest :: TestData -> Test
 notifyTest td = testCase "Notifications work" . runTestEnv td defaultTransactionSettings . unsafeWithoutTransaction $ do
   listen chan
-  forkNewConn $ notify chan payload
+  forkNewSession $ notify chan payload
   mnt1 <- getNotification 250000
   liftBase $ assertBool "Notification received" (isJust mnt1)
   Just nt1 <- pure mnt1
@@ -444,29 +491,19 @@
   assertEqualEq "Payloads are equal" payload (ntPayload nt1)
 
   unlisten chan
-  forkNewConn $ notify chan payload
+  forkNewSession $ notify chan payload
   mnt2 <- getNotification 250000
   assertEqualEq "No notification received after unlisten" Nothing mnt2
 
   listen chan
   unlistenAll
-  forkNewConn $ notify chan payload
+  forkNewSession $ notify chan payload
   mnt3 <- getNotification 250000
   assertEqualEq "No notification received after unlistenAll" Nothing mnt3
   where
     chan = "test_channel"
     payload = "test_payload"
-    forkNewConn action = do
-      sem <- newEmptyMVar
-      void . fork . withNewConnection $ do
-        -- withNewConnection needs access to the connection state to get current
-        -- ConnectionAcquisitionMode, but getNotification called immediately
-        -- after takes ownership of the connection state for its duration, so if
-        -- CPU gets to it first, withNewConnection will block and notification
-        -- will never be sent.
-        putMVar sem ()
-        action
-      takeMVar sem
+    forkNewSession = void . fork . withNewSession
 
 transactionTest :: TestData -> IsolationLevel -> Test
 transactionTest td lvl =
@@ -480,7 +517,7 @@
     $ do
       let sint = Identity (5 :: Int32)
       runQuery_ $ rawSQL "INSERT INTO test1_ (a) VALUES ($1)" sint
-      withNewConnection $ do
+      withNewSession $ do
         n <- runQuery $ rawSQL "SELECT a FROM test1_ WHERE a = $1" sint
         assertEqualEq "Other connection doesn't see uncommited data" 0 n
       rollback
@@ -539,6 +576,123 @@
     uuidStr2 <- fetchOne runIdentity
     assertEqual "UUID is encoded correctly" uuidStr uuidStr2 (==)
 
+integerTest :: TestData -> Test
+integerTest td = testCase "Integer decoding from numeric works"
+  . runTestEnv td defaultTransactionSettings
+  . forM_ values
+  $ \n -> do
+    -- The server strips trailing zero base-10000 digit groups from the wire
+    -- representation of numeric, so values that are multiples of 10000 arrive
+    -- with fewer digits than their weight indicates.
+    runSQL_ . mkSQL $ "SELECT " <> showt n <> " :: numeric"
+    n' <- fetchOne runIdentity
+    assertEqualEq ("Integer" <+> show n <+> "is decoded correctly") n n'
+
+    runQuery_ $ rawSQL "SELECT $1" (Identity n)
+    n'' <- fetchOne runIdentity
+    assertEqualEq ("Integer" <+> show n <+> "roundtrips correctly") n n''
+  where
+    values :: [Integer]
+    values =
+      [ 0
+      , 1
+      , -1
+      , 9999
+      , 10000
+      , -10000
+      , 10001
+      , 99990000
+      , 100000000
+      , 1000000000000
+      , -1000000000000
+      , 123400005678
+      , 10 ^ (100 :: Int)
+      , negate $ 10 ^ (100 :: Int)
+      , 10 ^ (100 :: Int) + 1
+      ]
+
+jsonTest :: TestData -> Test
+jsonTest td = testCase "JSON conversion failures and raw values"
+  . runTestEnv td defaultTransactionSettings
+  $ do
+    -- If the FromJSON instance of the target type rejects a value, decoding
+    -- fails with the error of the instance.
+    runSQL_ "SELECT '\"str\"'::json"
+    expectAesonError "json as Int64" . void $
+      fetchOne (runIdentity @(JSON Int64))
+    runSQL_ "SELECT '\"str\"'::jsonb"
+    expectAesonError "jsonb as Int64" . void $
+      fetchOne (runIdentity @(JSONB Int64))
+
+    -- A raw value arrives as the text of the value. The server stores it
+    -- verbatim for json and normalizes it for jsonb.
+    runSQL_ "SELECT '{\"b\": 1,  \"a\": 2}'::json, '{\"b\": 1,  \"a\": 2}'::jsonb"
+    (rawJson, rawJsonb) <- fetchOne id
+    assertEqualEq "json text is verbatim" (RawJSON "{\"b\": 1,  \"a\": 2}") rawJson
+    assertEqualEq "jsonb text is normalized" (RawJSONB "{\"a\": 2, \"b\": 1}") rawJsonb
+  where
+    -- The error of the FromJSON instance arrives inside a ConversionError.
+    expectAesonError :: String -> TestEnv () -> TestEnv ()
+    expectAesonError preface action =
+      try action >>= \case
+        Left (err :: DBException) ->
+          liftBase . assertBool (preface <> ": " <> show err) $
+            "expected Number, but encountered String" `L.isInfixOf` show err
+        Right () -> liftBase . assertFailure $ preface <> ": no error was thrown"
+
+restartTest :: TestData -> Test
+restartTest td =
+  testGroup
+    "Transaction restarts"
+    [ restartedTransactionIsNotMasked
+    , asyncExceptionsDontTriggerRestarts
+    ]
+  where
+    restartedTransactionIsNotMasked = testCase
+      "Restarted transaction doesn't run with asynchronous exceptions masked"
+      $ do
+        let ts =
+              defaultTransactionSettings
+                { tsRestartPredicate = Just . RestartPredicate $ \(e :: E.ErrorCall) _ ->
+                    e == E.ErrorCall "restart"
+                }
+        attempts <- newMVar (0 :: Int)
+        runTestEnv td ts $ do
+          n <- modifyMVar attempts $ \n -> pure (n + 1, n + 1)
+          when (n == 1) . throwM $ E.ErrorCall "restart"
+          ms <- liftBase E.getMaskingState
+          assertEqualEq "Unexpected masking state" E.Unmasked ms
+
+    asyncExceptionsDontTriggerRestarts = testCase
+      "Asynchronous exceptions don't trigger a transaction restart"
+      $ do
+        let ts =
+              defaultTransactionSettings
+                { tsRestartPredicate = Just . RestartPredicate $ \(_ :: SomeException) n ->
+                    n < 3
+                }
+        timeout 500000 (runTestEnv td ts $ runSQL_ "SELECT pg_sleep(2)") >>= \case
+          Just _ -> assertFailure "Query wasn't interrupted in time"
+          Nothing -> pure ()
+
+copyNotSupportedTest :: TestData -> Test
+copyNotSupportedTest td = testCase "COPY statements fail with an error"
+  . runTestEnv td defaultTransactionSettings
+  $ do
+    eres <- try $ runSQL_ "COPY (SELECT 1) TO STDOUT"
+    case eres of
+      Left DBException {dbeError = err} -> case fromException $ toException err of
+        Just (HPQTypesError msg) ->
+          liftBase . assertBool ("Error message mentions COPY: " ++ msg) $
+            "COPY" `L.isInfixOf` msg
+        Nothing -> liftBase . assertFailure $ "Unexpected error: " ++ show err
+      Right () -> liftBase $ assertFailure "COPY statement didn't fail"
+    -- libpq ends the copy mode when the next query runs, so the connection
+    -- stays usable.
+    runSQL_ "SELECT 1"
+    n <- fetchOne (runIdentity @Int32)
+    assertEqualEq "Connection is usable after the failed COPY" 1 n
+
 xmlTest :: TestData -> Test
 xmlTest td = testCase "Put and get XML value works"
   . runTestEnv td defaultTransactionSettings
@@ -553,6 +707,23 @@
     assertEqualEq "XML value correct" v v''
     runSQL_ "SET CLIENT_ENCODING TO 'latin-1'"
 
+onDemandDeadConnectionTest :: TestData -> Test
+onDemandDeadConnectionTest td = testCase
+  "Failed ROLLBACK of an on demand transaction doesn't mask the query error"
+  . runTestEnv td ts
+  $ do
+    -- The query kills its own backend. The ROLLBACK that ends the automatic
+    -- transaction fails as well, because the connection is gone. The error of
+    -- the query must propagate regardless.
+    eres <- try $ runSQL_ "SELECT pg_terminate_backend(pg_backend_pid())"
+    liftBase $ case eres of
+      Left DBException {..} ->
+        assertBool ("Exception comes from the query: " ++ show dbeQueryContext) $
+          "pg_terminate_backend" `L.isInfixOf` show dbeQueryContext
+      Right () -> assertFailure "DBException wasn't thrown"
+  where
+    ts = defaultTransactionSettings {tsConnectionAcquisitionMode = AcquireOnDemand}
+
 onDemandTest :: TestData -> Test
 onDemandTest td = testCase "OnDemand mode works" . runTestEnv td ts $ do
   runSQL_ "SELECT a FROM test1_"
@@ -586,6 +757,106 @@
     v :: Int32
     v = 1337
 
+sessionOwnerTest :: TestData -> Test
+sessionOwnerTest td =
+  testCase "Only the thread that owns a DB session can use it"
+    . runTestEnv td defaultTransactionSettings
+    $ do
+      result <- newEmptyMVar
+      _ <- fork $ do
+        sameSessionQuery <- try $ runSQL_ "SELECT 1"
+        sameSessionModeChange <- try unsafeAcquireOnDemandConnection
+        newSession <- try . withNewSession $ runSQL_ "SELECT 1"
+        putMVar result (sameSessionQuery, sameSessionModeChange, newSession)
+      (sameSessionQuery, sameSessionModeChange, newSession) <- takeMVar result
+      liftBase $ do
+        assertThreadMismatch "runSQL_" sameSessionQuery
+        assertThreadMismatch "unsafeAcquireOnDemandConnection" sameSessionModeChange
+        case newSession of
+          Left (e :: SomeException) ->
+            assertFailure $ "withNewSession failed in another thread: " ++ show e
+          Right () -> pure ()
+  where
+    assertThreadMismatch :: String -> Either SomeException () -> Assertion
+    assertThreadMismatch op = \case
+      Left e
+        | Just DBException {..} <- fromException e
+        , Just ThreadMismatchError {} <- cast dbeError ->
+            pure ()
+        | otherwise -> assertFailure $ op ++ " threw an unexpected exception: " ++ show e
+      Right () -> assertFailure $ op ++ " didn't throw ThreadMismatchError"
+
+childSessionTest :: TestData -> Test
+childSessionTest td = testCase
+  "Child thread can start a session after the parent session ended"
+  $ do
+    parentEnded <- newEmptyMVar
+    result <- newEmptyMVar
+    runTestEnv td defaultTransactionSettings $ do
+      void . fork $ do
+        takeMVar parentEnded
+        putMVar result =<< try (withNewSession $ runSQL_ "SELECT 1")
+    putMVar parentEnded ()
+    takeMVar result >>= \case
+      Left (e :: SomeException) ->
+        assertFailure $ "withNewSession failed in the child thread: " ++ show e
+      Right () -> pure ()
+
+commitFailureTest :: TestData -> Test
+commitFailureTest td = testCase
+  "Transaction is active after a failed commit"
+  . runTestEnv td defaultTransactionSettings
+  $ do
+    runSQL_ "CREATE TABLE commit_failure_ (a INTEGER UNIQUE DEFERRABLE INITIALLY DEFERRED)"
+    commit
+    (`finally` cleanup) $ do
+      -- Deferred constraint violation makes the COMMIT fail.
+      runSQL_ "INSERT INTO commit_failure_ (a) VALUES (1), (1)"
+      eres <- try commit
+      liftBase $ case eres :: Either DBException () of
+        Left DBException {..}
+          | Just DetailedQueryError {..} <- cast dbeError -> do
+              assertEqualEq "Unexpected error code" UniqueViolation qeErrorCode
+          | otherwise -> assertFailure $ "Unexpected exception: " ++ show dbeError
+        Right () -> assertFailure "DBException wasn't thrown"
+      -- A new transaction needs to be active at this point, so a write made
+      -- after the failed commit must be reverted by a rollback.
+      runSQL_ "INSERT INTO commit_failure_ (a) VALUES (2)"
+      rollback
+      n <- runSQL "SELECT a FROM commit_failure_"
+      assertEqualEq "Unexpected number of rows" 0 n
+  where
+    cleanup = do
+      rollback
+      runSQL_ "DROP TABLE commit_failure_"
+      commit
+
+acquisitionModeChangeFailureTest :: TestData -> Test
+acquisitionModeChangeFailureTest td = testCase
+  "Connection state is usable after a failed acquisition mode change"
+  . runTestEnv td defaultTransactionSettings
+  $ do
+    -- Violate a deferred constraint so that the COMMIT issued by
+    -- unsafeAcquireOnDemandConnection fails.
+    runSQL_ "CREATE TABLE mode_change_ (a INTEGER UNIQUE DEFERRABLE INITIALLY DEFERRED)"
+    runSQL_ "INSERT INTO mode_change_ (a) VALUES (1), (1)"
+    eres <- try unsafeAcquireOnDemandConnection
+    liftBase $ case eres of
+      Left DBException {..}
+        | Just DetailedQueryError {..} <- cast dbeError -> do
+            assertEqualEq "Unexpected error code" UniqueViolation qeErrorCode
+        | otherwise -> assertFailure $ "Unexpected exception: " ++ show dbeError
+      Right () -> assertFailure "DBException wasn't thrown"
+
+    -- The failed COMMIT returned the connection to its source, so the
+    -- connection state needs to be on demand now. In particular, it must not
+    -- refer to the connection that is already gone.
+    mode <- getConnectionAcquisitionMode
+    assertEqualEq "Unexpected connection acquisition mode" AcquireOnDemand mode
+    runSQL_ "SELECT 1"
+    n <- fetchOne $ runIdentity @Int32
+    assertEqualEq "Unexpected query result" 1 n
+
 rowTest
   :: forall row
    . (Arbitrary row, Eq row, Show row, ToRow row, FromRow row)
@@ -619,14 +890,25 @@
   [ autocommitTest td
   , setRoleTest td
   , preparedStatementTest td
+  , copyNotSupportedTest td
   , xmlTest td
   , readOnlyTest td
   , savepointTest td
+  , savepointDeadConnectionTest td
+  , withoutTransactionDeadConnectionTest td
+  , restartTest td
   , notifyTest td
   , queryInterruptionTest td
   , cursorTest td
   , uuidTest td
+  , integerTest td
+  , jsonTest td
   , onDemandTest td
+  , onDemandDeadConnectionTest td
+  , sessionOwnerTest td
+  , childSessionTest td
+  , acquisitionModeChangeFailureTest td
+  , commitFailureTest td
   , transactionTest td ReadCommitted
   , transactionTest td RepeatableRead
   , transactionTest td Serializable
@@ -638,12 +920,18 @@
   , nullTest td (u :: Bool)
   , nullTest td (u :: AsciiChar)
   , nullTest td (u :: Word8)
+  , nullTest td (u :: Word16)
+  , nullTest td (u :: Word32)
+  , nullTest td (u :: Word64)
+  , nullTest td (u :: Integer)
   , nullTest td (u :: String)
   , nullTest td (u :: BS.ByteString)
   , nullTest td (u :: T.Text)
   , nullTest td (u :: U.UUID)
   , nullTest td (u :: JSON Value)
   , nullTest td (u :: JSONB Value)
+  , nullTest td (u :: RawJSON)
+  , nullTest td (u :: RawJSONB)
   , nullTest td (u :: XML)
   , nullTest td (u :: Interval)
   , nullTest td (u :: Day)
@@ -663,12 +951,17 @@
   , putGetTest td 100 (u :: Bool) (==)
   , putGetTest td 100 (u :: AsciiChar) (==)
   , putGetTest td 100 (u :: Word8) (==)
+  , putGetTest td 100 (u :: Word16) (==)
+  , putGetTest td 100 (u :: Word32) (==)
+  , putGetTest td 100 (u :: Word64) (==)
+  , putGetTest td 1000000000000 (u :: Integer) (==)
   , putGetTest td 1000 (u :: String0) (==)
   , putGetTest td 1000 (u :: BS.ByteString) (==)
   , putGetTest td 1000 (u :: T.Text) (==)
   , putGetTest td 1000 (u :: U.UUID) (==)
   , putGetTest td 50 (u :: JSON Value0) (==)
   , putGetTest td 50 (u :: JSONB Value0) (==)
+  , putGetTest td 50 (u :: RawJSON) (==)
   , putGetTest td 20 (u :: Array1 (JSON Value0)) (==)
   , putGetTest td 20 (u :: Array1 (JSONB Value0)) (==)
   , putGetTest td 50 (u :: Interval) (==)
@@ -723,20 +1016,19 @@
   , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple))
   , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested))
   , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, U.UUID))
-  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, U.UUID, Day))
+  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Word16))
+  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Word16, Word32))
+  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Word16, Word32, Word64, Integer))
+  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Word16, Word32, Word64, Integer, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float))
+  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Word16, Word32, Word64, Integer, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double))
+  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Word16, Word32, Word64, Integer, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool))
+  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Word16, Word32, Word64, Integer, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar))
+  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Word16, Word32, Word64, Integer, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8))
+  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Word16, Word32, Word64, Integer, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0))
+  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Word16, Word32, Word64, Integer, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString))
+  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Word16, Word32, Word64, Integer, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text))
+  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Word16, Word32, Word64, Integer, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString))
+  , rowTest td (u :: (Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Word16, Word32, Word64, Integer, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, Day, Array1 Int32, Composite Simple, CompositeArray1 Simple, Composite Nested, CompositeArray1 Nested, Int16, Int32, Int64, Float, Double, Bool, AsciiChar, Word8, String0, BS.ByteString, T.Text, BS.ByteString, U.UUID))
   ]
   where
     u = undefined
@@ -763,7 +1055,7 @@
     connString : args -> pure (T.pack connString, args)
     [] ->
       lookupEnv "GITHUB_ACTIONS" >>= \case
-        Just "true" -> pure ("host=postgres user=postgres password=postgres", [])
+        Just "true" -> pure ("host=localhost user=postgres password=postgres", [])
         _ -> printUsage >> exitFailure
   where
     printUsage = do
diff --git a/test/Prelude/Instances.hs b/test/Prelude/Instances.hs
--- a/test/Prelude/Instances.hs
+++ b/test/Prelude/Instances.hs
@@ -505,4 +505,3 @@
   , Show a41, Show a42, Show a43, Show a44, Show a45, Show a46, Show a47, Show a48
   , Show a49, Show a50
   ) => Show (a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32, a33, a34, a35, a36, a37, a38, a39, a40, a41, a42, a43, a44, a45, a46, a47, a48, a49, a50)
-
diff --git a/test/Test/Aeson/Compat.hs b/test/Test/Aeson/Compat.hs
--- a/test/Test/Aeson/Compat.hs
+++ b/test/Test/Aeson/Compat.hs
@@ -16,30 +16,11 @@
 import Data.Aeson.Key qualified as K
 import Data.Aeson.KeyMap qualified as KM
 
-import Database.PostgreSQL.PQTypes.Internal.C.Types
-import Database.PostgreSQL.PQTypes
-
 fromList :: [(Text, v)] -> KM.KeyMap v
 fromList = KM.fromList . map (first K.fromText)
 
-newtype Value0 = Value0 { unValue0 :: Value }
-  deriving newtype (Eq, Show)
-
-instance ToSQL (JSON Value0) where
-  type PQDest (JSON Value0) = PGbytea
-  toSQL = aesonToSQL . unValue0 . unJSON
-
-instance FromSQL (JSON Value0) where
-  type PQBase (JSON Value0) = PGbytea
-  fromSQL = fmap (JSON . Value0) . aesonFromSQL
-
-instance ToSQL (JSONB Value0) where
-  type PQDest (JSONB Value0) = PGbytea
-  toSQL = aesonToSQL . unValue0 . unJSONB
-
-instance FromSQL (JSONB Value0) where
-  type PQBase (JSONB Value0) = PGbytea
-  fromSQL = fmap (JSONB . Value0) . aesonFromSQL
+newtype Value0 = Value0 Value
+  deriving newtype (Eq, FromJSON, Show, ToJSON)
 
 mkValue0 :: Value -> Value0
 mkValue0 = Value0
diff --git a/test/Test/QuickCheck/Arbitrary/Instances.hs b/test/Test/QuickCheck/Arbitrary/Instances.hs
--- a/test/Test/QuickCheck/Arbitrary/Instances.hs
+++ b/test/Test/QuickCheck/Arbitrary/Instances.hs
@@ -5,7 +5,9 @@
 
 import Data.Aeson
 import Data.ByteString qualified as BS
+import Data.ByteString.Lazy qualified as BSL
 import Data.Char
+import Data.Int
 import Data.Scientific
 import Data.Text qualified as T
 import Data.Time
@@ -89,17 +91,32 @@
           subValue = value (i - 1) n
           shortListOf = fmap (take depth) . listOf
 
+-- | The @json@ type stores its input text verbatim. A raw value therefore
+-- roundtrips if the generator produces the encoding of a JSON value. The
+-- server rejects anything else.
+instance Arbitrary RawJSON where
+  arbitrary = RawJSON . BSL.toStrict . encode <$> arbitrary @C.Value0
+
 ----------------------------------------
 
 instance Arbitrary Day where
   arbitrary = ModifiedJulianDay <$> arbitrary
 
+-- | Generate a duration of a whole number of microseconds, less than the
+-- given number of seconds. Time related values have microsecond precision,
+-- because that's the precision of their binary wire format. Haskell's time
+-- types can hold sub-microsecond digits, but the server returns such values
+-- rounded to whole microseconds. If the generator produced them, they would
+-- not roundtrip exactly and the tests would need approximate comparison.
+microseconds :: Fractional a => Int64 -> Gen a
+microseconds secs = (/ 1000000) . fromIntegral <$> choose (0, secs * 1000000 - 1)
+
 instance Arbitrary TimeOfDay where
   arbitrary = do
     hours <- choose (0, 23)
     mins <- choose (0, 59)
-    secs :: Double <- choose (0, 60)
-    pure $ TimeOfDay hours mins (realToFrac secs)
+    secs <- microseconds 60
+    pure $ TimeOfDay hours mins secs
 
 instance Arbitrary LocalTime where
   arbitrary = LocalTime <$> arbitrary <*> arbitrary
@@ -107,14 +124,10 @@
 instance Arbitrary UTCTime where
   arbitrary = do
     day <- arbitrary
-    secs :: Double <- choose (0, 86401)
-    pure $ UTCTime day (realToFrac secs)
-
-instance Arbitrary TimeZone where
-  arbitrary = elements $ map hoursToTimeZone [-12 .. 14]
-
-instance Arbitrary ZonedTime where
-  arbitrary = ZonedTime <$> arbitrary <*> arbitrary
+    -- The day time stays below 86400 seconds, because the server normalizes
+    -- a larger value into the next day.
+    secs <- microseconds 86400
+    pure $ UTCTime day secs
 
 ----------------------------------------
 
