diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+# v0.4
+
+- Migrated to Hasql 1.10
+- (Breaking) Redefined `Snippet.sql` to use `Text` instead of `ByteString`
+- (Breaking) Dropped the preparability flags assuming that all dynamic statements should not be
+  prepared
diff --git a/hasql-dynamic-statements.cabal b/hasql-dynamic-statements.cabal
--- a/hasql-dynamic-statements.cabal
+++ b/hasql-dynamic-statements.cabal
@@ -1,12 +1,9 @@
 cabal-version: 3.0
 name: hasql-dynamic-statements
-version: 0.3.1.8
-synopsis: Toolkit for constructing Hasql statements dynamically
+version: 0.4
+synopsis: Hasql extension for dynamic construction of statements
 description:
-  This library introduces into the Hasql ecosystem a new abstraction named Snippet,
-  which makes it trivial to construct SQL, while injecting values.
-  It is intended to be used when the SQL of your statement depends on the parameters,
-  that you want to pass in.
+  Library extending Hasql with an abstraction that simplifies construction of SQL statements, the structure of which depends on the parameters.
 
 homepage: https://github.com/nikita-volkov/hasql-dynamic-statements
 bug-reports:
@@ -17,10 +14,12 @@
 copyright: (c) 2019, Nikita Volkov
 license: MIT
 license-file: LICENSE
+extra-doc-files:
+  CHANGELOG.md
 
 source-repository head
   type: git
-  location: git://github.com/nikita-volkov/hasql-dynamic-statements.git
+  location: https://github.com/nikita-volkov/hasql-dynamic-statements
 
 common base
   default-language: Haskell2010
@@ -46,6 +45,7 @@
     LiberalTypeSynonyms
     MultiParamTypeClasses
     MultiWayIf
+    NamedFieldPuns
     NoImplicitPrelude
     NoMonomorphismRestriction
     OverloadedStrings
@@ -79,7 +79,7 @@
 
 library
   import: base
-  hs-source-dirs: library
+  hs-source-dirs: src/library
   exposed-modules:
     Hasql.DynamicStatements.Session
     Hasql.DynamicStatements.Snippet
@@ -93,18 +93,27 @@
     base >=4.14 && <5,
     bytestring >=0.10 && <0.13,
     containers >=0.6 && <0.8,
-    hasql >=1.8 && <1.10,
-    hasql-implicits >=0.2 && <0.3,
-    ptr >=0.16.7.2 && <0.17,
+    hasql >=1.10 && <1.11,
+    hasql-implicits >=0.2.0.2 && <0.3,
+    text >=1 && <3,
+    text-builder >=1.0 && <1.1,
 
 test-suite test
   import: test
   type: exitcode-stdio-1.0
-  hs-source-dirs: test
+  hs-source-dirs: src/test
   main-is: Main.hs
+  other-modules:
+    Regressions.Issue2Spec
+    SpecHook
+    Units.SnippetSpec
+
+  build-tool-depends:
+    hspec-discover:hspec-discover ^>=2.11.12
+
   build-depends:
     hasql,
     hasql-dynamic-statements,
+    hspec ^>=2.11.12,
     rerebase <2,
-    tasty >=0.12 && <2,
-    tasty-hunit >=0.9 && <0.11,
+    testcontainers-postgresql >=0.2 && <0.3,
diff --git a/library/Hasql/DynamicStatements/Prelude.hs b/library/Hasql/DynamicStatements/Prelude.hs
deleted file mode 100644
--- a/library/Hasql/DynamicStatements/Prelude.hs
+++ /dev/null
@@ -1,70 +0,0 @@
-module Hasql.DynamicStatements.Prelude
-  ( module Exports,
-  )
-where
-
-import Control.Applicative as Exports
-import Control.Arrow as Exports
-import Control.Category as Exports
-import Control.Concurrent as Exports
-import Control.Exception as Exports
-import Control.Monad as Exports hiding (fail, forM, forM_, mapM, mapM_, msum, sequence, sequence_)
-import Control.Monad.Fail as Exports
-import Control.Monad.Fix as Exports hiding (fix)
-import Control.Monad.IO.Class as Exports
-import Control.Monad.ST as Exports
-import Data.Bits as Exports
-import Data.Bool as Exports
-import Data.ByteString as Exports (ByteString)
-import Data.Char as Exports
-import Data.Coerce as Exports
-import Data.Complex as Exports
-import Data.Data as Exports
-import Data.Dynamic as Exports
-import Data.Either as Exports
-import Data.Fixed as Exports
-import Data.Foldable as Exports hiding (toList)
-import Data.Function as Exports hiding (id, (.))
-import Data.Functor as Exports hiding (unzip)
-import Data.Functor.Contravariant as Exports
-import Data.Functor.Identity as Exports
-import Data.IORef as Exports
-import Data.Int as Exports
-import Data.Ix as Exports
-import Data.List as Exports hiding (all, and, any, concat, concatMap, elem, find, foldl, foldl', foldl1, foldr, foldr1, isSubsequenceOf, mapAccumL, mapAccumR, maximum, maximumBy, minimum, minimumBy, notElem, or, product, sortOn, sum, uncons)
-import Data.Maybe as Exports
-import Data.Monoid as Exports hiding (First (..), Last (..), (<>))
-import Data.Ord as Exports
-import Data.Proxy as Exports
-import Data.Ratio as Exports
-import Data.STRef as Exports
-import Data.Semigroup as Exports
-import Data.Sequence as Exports (Seq)
-import Data.String as Exports
-import Data.Traversable as Exports
-import Data.Tuple as Exports
-import Data.Unique as Exports
-import Data.Version as Exports
-import Data.Word as Exports
-import Debug.Trace as Exports
-import Foreign.ForeignPtr as Exports
-import Foreign.Ptr as Exports
-import Foreign.StablePtr as Exports
-import Foreign.Storable as Exports hiding (alignment, sizeOf)
-import GHC.Conc as Exports hiding (threadWaitRead, threadWaitReadSTM, threadWaitWrite, threadWaitWriteSTM, withMVar)
-import GHC.Exts as Exports (IsList (..), groupWith, inline, lazy, sortWith)
-import GHC.Generics as Exports (Generic, Generic1)
-import GHC.IO.Exception as Exports
-import Numeric as Exports
-import System.Environment as Exports
-import System.Exit as Exports
-import System.IO as Exports
-import System.IO.Error as Exports
-import System.IO.Unsafe as Exports
-import System.Mem as Exports
-import System.Mem.StableName as Exports
-import System.Timeout as Exports
-import Text.Printf as Exports (hPrintf, printf)
-import Text.Read as Exports (Read (..), readEither, readMaybe)
-import Unsafe.Coerce as Exports
-import Prelude as Exports hiding (all, and, any, concat, concatMap, elem, fail, foldl, foldl1, foldr, foldr1, id, mapM, mapM_, maximum, minimum, notElem, or, product, sequence, sequence_, sum, (.))
diff --git a/library/Hasql/DynamicStatements/Session.hs b/library/Hasql/DynamicStatements/Session.hs
deleted file mode 100644
--- a/library/Hasql/DynamicStatements/Session.hs
+++ /dev/null
@@ -1,19 +0,0 @@
-module Hasql.DynamicStatements.Session where
-
-import Hasql.Decoders qualified as Decoders
-import Hasql.DynamicStatements.Prelude
-import Hasql.DynamicStatements.Snippet.Defs qualified as SnippetDefs
-import Hasql.DynamicStatements.Statement qualified as Statement
-import Hasql.Session
-import Hasql.Session qualified as Session
-
--- |
--- Execute a dynamically parameterized statement, providing a result decoder.
---
--- This is merely a shortcut, which immediately embeds
--- @Hasql.DynamicStatements.Statement.'Statement.dynamicallyParameterized'@
--- in @Session@.
--- For details see the docs on that function.
-dynamicallyParameterizedStatement :: SnippetDefs.Snippet -> Decoders.Result result -> Bool -> Session result
-dynamicallyParameterizedStatement snippet decoder prepared =
-  Session.statement () (Statement.dynamicallyParameterized snippet decoder prepared)
diff --git a/library/Hasql/DynamicStatements/Snippet.hs b/library/Hasql/DynamicStatements/Snippet.hs
deleted file mode 100644
--- a/library/Hasql/DynamicStatements/Snippet.hs
+++ /dev/null
@@ -1,9 +0,0 @@
-module Hasql.DynamicStatements.Snippet
-  ( Snippet,
-    param,
-    encoderAndParam,
-    sql,
-  )
-where
-
-import Hasql.DynamicStatements.Snippet.Defs
diff --git a/library/Hasql/DynamicStatements/Snippet/Defs.hs b/library/Hasql/DynamicStatements/Snippet/Defs.hs
deleted file mode 100644
--- a/library/Hasql/DynamicStatements/Snippet/Defs.hs
+++ /dev/null
@@ -1,55 +0,0 @@
-module Hasql.DynamicStatements.Snippet.Defs where
-
-import Hasql.DynamicStatements.Prelude
-import Hasql.Encoders qualified as Encoders
-import Hasql.Implicits.Encoders qualified as Encoders
-
--- |
--- Composable SQL snippet with parameters injected.
--- Abstracts over placeholders and matching of encoders.
---
--- It has an instance of `IsString`, so having the @OverloadedStrings@ extension enabled
--- you can construct it directly from string literals.
---
--- Here's an example:
---
--- @
--- selectSubstring :: Text -> Maybe Int32 -> Maybe Int32 -> 'Snippet'
--- selectSubstring string from to =
---   "select substring(" <> 'param' string <>
---   'foldMap' (\\ x -> " from " <> 'param' x) from <>
---   'foldMap' (\\ x -> " for " <> 'param' x) to <>
---   ")"
--- @
---
--- Having a decoder you can lift it into 'Hasql.Statement.Statement' using
--- 'Hasql.DynamicStatements.Statement.dynamicallyParameterized' or directly execute it in
--- 'Hasql.Session.Session' using
--- 'Hasql.DynamicStatements.Session.dynamicallyParameterizedStatement'.
-newtype Snippet = Snippet (Seq SnippetChunk)
-
-data SnippetChunk
-  = StringSnippetChunk ByteString
-  | ParamSnippetChunk (Encoders.Params ())
-
-deriving instance Semigroup Snippet
-
-deriving instance Monoid Snippet
-
-instance IsString Snippet where
-  fromString x = Snippet (pure (StringSnippetChunk (fromString x)))
-
--- |
--- SQL chunk in ASCII.
-sql :: ByteString -> Snippet
-sql x = Snippet (pure (StringSnippetChunk x))
-
--- |
--- Parameter encoded using an implicitly derived encoder from the type.
-param :: (Encoders.DefaultParamEncoder param) => param -> Snippet
-param = encoderAndParam Encoders.defaultParam
-
--- |
--- Parameter with an explicitly defined encoder.
-encoderAndParam :: Encoders.NullableOrNot Encoders.Value param -> param -> Snippet
-encoderAndParam encoder param = Snippet (pure (ParamSnippetChunk (param >$ Encoders.param encoder)))
diff --git a/library/Hasql/DynamicStatements/Statement.hs b/library/Hasql/DynamicStatements/Statement.hs
deleted file mode 100644
--- a/library/Hasql/DynamicStatements/Statement.hs
+++ /dev/null
@@ -1,63 +0,0 @@
-module Hasql.DynamicStatements.Statement where
-
-import Hasql.Decoders qualified as Decoders
-import Hasql.DynamicStatements.Prelude
-import Hasql.DynamicStatements.Snippet.Defs qualified as SnippetDefs
-import Hasql.Statement
-import Ptr.ByteString qualified as ByteString
-import Ptr.Poking qualified as Poking
-
--- |
--- Construct a statement dynamically, specifying the parameters in-place
--- in the declaration of snippet and providing a result decoder and
--- specifying whether the statement should be prepared.
---
--- The injection of the parameters is handled automatically,
--- generating parametric statements with binary encoders under the hood.
---
--- This is useful when the SQL of your statement depends on the parameters.
--- Here's an example:
---
--- @
--- selectSubstring :: Text -> Maybe Int32 -> Maybe Int32 -> 'Statement' () Text
--- selectSubstring string from to = let
---   snippet =
---     "select substring(" <> Snippet.'SnippetDefs.param' string <>
---     foldMap (mappend " from " . Snippet.'SnippetDefs.param') from <>
---     foldMap (mappend " for " . Snippet.'SnippetDefs.param') to <>
---     ")"
---   decoder = Decoders.'Decoders.singleRow' (Decoders.'Decoders.column' (Decoders.'Decoders.nonNullable' Decoders.'Decoders.text'))
---   in 'dynamicallyParameterized' snippet decoder True
--- @
---
--- Without the Snippet API you would have had to implement the same functionality thus:
---
--- @
--- selectSubstring' :: Text -> Maybe Int32 -> Maybe Int32 -> 'Statement' () Text
--- selectSubstring' string from to = let
---   sql = case (from, to) of
---     (Just _, Just _) -> "select substring($1 from $2 to $3)"
---     (Just _, Nothing) -> "select substring($1 from $2)"
---     (Nothing, Just _) -> "select substring($1 to $2)"
---     (Nothing, Nothing) -> "select substring($1)"
---   encoder =
---     Encoders.'Encoders.param' (string '>$' Encoders.'Encoders.text') <>
---     foldMap (\\ x -> Encoders.'Encoders.param' (x '>$' Encoders.'Encoders.int8')) from <>
---     foldMap (\\ x -> Encoders.'Encoders.param' (x '>$' Encoders.'Encoders.int8')) to
---   decoder = Decoders.'Decoders.singleRow' (Decoders.'Decoders.column' (Decoders.'Decoders.nonNullable' Decoders.'Decoders.text'))
---   in Statement sql encoder decoder True
--- @
---
--- As you can see, the Snippet API abstracts over placeholders and
--- matching encoder generation, thus also protecting you from all sorts of related bugs.
-dynamicallyParameterized :: SnippetDefs.Snippet -> Decoders.Result result -> Bool -> Statement () result
-dynamicallyParameterized (SnippetDefs.Snippet chunks) decoder prepared =
-  let step (!paramId, !poking, !encoder) = \case
-        SnippetDefs.StringSnippetChunk sql -> (paramId, poking <> Poking.bytes sql, encoder)
-        SnippetDefs.ParamSnippetChunk paramEncoder ->
-          let newParamId = paramId + 1
-              newPoking = poking <> Poking.word8 36 <> Poking.asciiIntegral paramId
-              newEncoder = encoder <> paramEncoder
-           in (newParamId, newPoking, newEncoder)
-   in case foldl' step (1, mempty, mempty) chunks of
-        (_, poking, encoder) -> Statement (ByteString.poking poking) encoder decoder prepared
diff --git a/src/library/Hasql/DynamicStatements/Prelude.hs b/src/library/Hasql/DynamicStatements/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/src/library/Hasql/DynamicStatements/Prelude.hs
@@ -0,0 +1,71 @@
+module Hasql.DynamicStatements.Prelude
+  ( module Exports,
+  )
+where
+
+import Control.Applicative as Exports
+import Control.Arrow as Exports
+import Control.Category as Exports
+import Control.Concurrent as Exports
+import Control.Exception as Exports
+import Control.Monad as Exports hiding (fail, forM, forM_, mapM, mapM_, msum, sequence, sequence_)
+import Control.Monad.Fail as Exports
+import Control.Monad.Fix as Exports hiding (fix)
+import Control.Monad.IO.Class as Exports
+import Control.Monad.ST as Exports
+import Data.Bits as Exports
+import Data.Bool as Exports
+import Data.ByteString as Exports (ByteString)
+import Data.Char as Exports
+import Data.Coerce as Exports
+import Data.Complex as Exports
+import Data.Data as Exports
+import Data.Dynamic as Exports
+import Data.Either as Exports
+import Data.Fixed as Exports
+import Data.Foldable as Exports hiding (toList)
+import Data.Function as Exports hiding (id, (.))
+import Data.Functor as Exports hiding (unzip)
+import Data.Functor.Contravariant as Exports
+import Data.Functor.Identity as Exports
+import Data.IORef as Exports
+import Data.Int as Exports
+import Data.Ix as Exports
+import Data.List as Exports hiding (all, and, any, concat, concatMap, elem, find, foldl, foldl', foldl1, foldr, foldr1, isSubsequenceOf, mapAccumL, mapAccumR, maximum, maximumBy, minimum, minimumBy, notElem, or, product, sortOn, sum, uncons)
+import Data.Maybe as Exports
+import Data.Monoid as Exports hiding (First (..), Last (..), (<>))
+import Data.Ord as Exports
+import Data.Proxy as Exports
+import Data.Ratio as Exports
+import Data.STRef as Exports
+import Data.Semigroup as Exports
+import Data.Sequence as Exports (Seq)
+import Data.String as Exports
+import Data.Text as Exports (Text)
+import Data.Traversable as Exports
+import Data.Tuple as Exports
+import Data.Unique as Exports
+import Data.Version as Exports
+import Data.Word as Exports
+import Debug.Trace as Exports
+import Foreign.ForeignPtr as Exports
+import Foreign.Ptr as Exports
+import Foreign.StablePtr as Exports
+import Foreign.Storable as Exports hiding (alignment, sizeOf)
+import GHC.Conc as Exports hiding (threadWaitRead, threadWaitReadSTM, threadWaitWrite, threadWaitWriteSTM, withMVar)
+import GHC.Exts as Exports (IsList (..), groupWith, inline, lazy, sortWith)
+import GHC.Generics as Exports (Generic, Generic1)
+import GHC.IO.Exception as Exports
+import Numeric as Exports
+import System.Environment as Exports
+import System.Exit as Exports
+import System.IO as Exports
+import System.IO.Error as Exports
+import System.IO.Unsafe as Exports
+import System.Mem as Exports
+import System.Mem.StableName as Exports
+import System.Timeout as Exports
+import Text.Printf as Exports (hPrintf, printf)
+import Text.Read as Exports (Read (..), readEither, readMaybe)
+import Unsafe.Coerce as Exports
+import Prelude as Exports hiding (all, and, any, concat, concatMap, elem, fail, foldl, foldl1, foldr, foldr1, id, mapM, mapM_, maximum, minimum, notElem, or, product, sequence, sequence_, sum, (.))
diff --git a/src/library/Hasql/DynamicStatements/Session.hs b/src/library/Hasql/DynamicStatements/Session.hs
new file mode 100644
--- /dev/null
+++ b/src/library/Hasql/DynamicStatements/Session.hs
@@ -0,0 +1,18 @@
+module Hasql.DynamicStatements.Session where
+
+import Hasql.Decoders qualified as Decoders
+import Hasql.DynamicStatements.Snippet.Defs qualified as SnippetDefs
+import Hasql.DynamicStatements.Statement qualified as Statement
+import Hasql.Session
+import Hasql.Session qualified as Session
+
+-- |
+-- Execute a dynamically parameterized statement, providing a result decoder.
+--
+-- This is merely a shortcut, which immediately embeds
+-- @Hasql.DynamicStatements.Statement.'Statement.dynamicallyParameterized'@
+-- in @Session@.
+-- For details see the docs on that function.
+dynamicallyParameterizedStatement :: SnippetDefs.Snippet -> Decoders.Result result -> Session result
+dynamicallyParameterizedStatement snippet decoder =
+  Session.statement () (Statement.dynamicallyParameterized snippet decoder)
diff --git a/src/library/Hasql/DynamicStatements/Snippet.hs b/src/library/Hasql/DynamicStatements/Snippet.hs
new file mode 100644
--- /dev/null
+++ b/src/library/Hasql/DynamicStatements/Snippet.hs
@@ -0,0 +1,9 @@
+module Hasql.DynamicStatements.Snippet
+  ( Snippet,
+    param,
+    encoderAndParam,
+    sql,
+  )
+where
+
+import Hasql.DynamicStatements.Snippet.Defs
diff --git a/src/library/Hasql/DynamicStatements/Snippet/Defs.hs b/src/library/Hasql/DynamicStatements/Snippet/Defs.hs
new file mode 100644
--- /dev/null
+++ b/src/library/Hasql/DynamicStatements/Snippet/Defs.hs
@@ -0,0 +1,70 @@
+module Hasql.DynamicStatements.Snippet.Defs where
+
+import Hasql.DynamicStatements.Prelude
+import Hasql.Encoders qualified as Encoders
+import Hasql.Implicits.Encoders qualified as Encoders
+import TextBuilder qualified
+
+-- |
+-- Composable SQL snippet with parameters injected.
+-- Abstracts over placeholders and matching of encoders.
+--
+-- It has an instance of `IsString`, so having the @OverloadedStrings@ extension enabled
+-- you can construct it directly from string literals.
+--
+-- Here's an example:
+--
+-- @
+-- selectSubstring :: Text -> Maybe Int32 -> Maybe Int32 -> 'Snippet'
+-- selectSubstring string from to =
+--   "select substring(" <> 'param' string <>
+--   'foldMap' (\\ x -> " from " <> 'param' x) from <>
+--   'foldMap' (\\ x -> " for " <> 'param' x) to <>
+--   ")"
+-- @
+--
+-- Having a decoder you can lift it into 'Hasql.Statement.Statement' using
+-- 'Hasql.DynamicStatements.Statement.dynamicallyParameterized' or directly execute it in
+-- 'Hasql.Session.Session' using
+-- 'Hasql.DynamicStatements.Session.dynamicallyParameterizedStatement'.
+newtype Snippet = Snippet (Seq SnippetChunk)
+
+data SnippetChunk
+  = StringSnippetChunk TextBuilder.TextBuilder
+  | ParamSnippetChunk (Encoders.Params ())
+
+deriving instance Semigroup Snippet
+
+deriving instance Monoid Snippet
+
+instance IsString Snippet where
+  fromString x = Snippet (pure (StringSnippetChunk (fromString x)))
+
+-- |
+-- SQL chunk.
+sql :: Text -> Snippet
+sql x = Snippet (pure (StringSnippetChunk (TextBuilder.text x)))
+
+-- |
+-- Parameter encoded using an implicitly derived encoder from the type.
+param :: (Encoders.DefaultParamEncoder param) => param -> Snippet
+param = encoderAndParam Encoders.defaultParam
+
+-- |
+-- Parameter with an explicitly defined encoder.
+encoderAndParam :: Encoders.NullableOrNot Encoders.Value param -> param -> Snippet
+encoderAndParam encoder param = Snippet (pure (ParamSnippetChunk (param >$ Encoders.param encoder)))
+
+compile :: Snippet -> (Text, Encoders.Params ())
+compile (Snippet chunks) =
+  let step (!paramId, !builder, !encoder) = \case
+        StringSnippetChunk sql ->
+          (paramId, builder <> sql, encoder)
+        ParamSnippetChunk paramEncoder ->
+          let newParamId = paramId + 1
+              newPoking = builder <> "$" <> TextBuilder.decimal paramId
+              newEncoder = encoder <> paramEncoder
+           in (newParamId, newPoking, newEncoder)
+      (_, builder, encoder) = foldl' step (1 :: Int, mempty, mempty) chunks
+      sql = TextBuilder.toText builder
+   in (sql, encoder)
diff --git a/src/library/Hasql/DynamicStatements/Statement.hs b/src/library/Hasql/DynamicStatements/Statement.hs
new file mode 100644
--- /dev/null
+++ b/src/library/Hasql/DynamicStatements/Statement.hs
@@ -0,0 +1,53 @@
+module Hasql.DynamicStatements.Statement where
+
+import Hasql.Decoders qualified as Decoders
+import Hasql.DynamicStatements.Snippet.Defs qualified as SnippetDefs
+import Hasql.Statement
+
+-- |
+-- Construct a statement dynamically, specifying the parameters in-place
+-- in the declaration of snippet and providing a result decoder and
+-- specifying whether the statement should be prepared.
+--
+-- The injection of the parameters is handled automatically,
+-- generating parametric statements with binary encoders under the hood.
+--
+-- This is useful when the SQL of your statement depends on the parameters.
+-- Here's an example:
+--
+-- @
+-- selectSubstring :: Text -> Maybe Int32 -> Maybe Int32 -> 'Statement' () Text
+-- selectSubstring string from to = let
+--   snippet =
+--     "select substring(" <> Snippet.'SnippetDefs.param' string <>
+--     foldMap (mappend " from " . Snippet.'SnippetDefs.param') from <>
+--     foldMap (mappend " for " . Snippet.'SnippetDefs.param') to <>
+--     ")"
+--   decoder = Decoders.'Decoders.singleRow' (Decoders.'Decoders.column' (Decoders.'Decoders.nonNullable' Decoders.'Decoders.text'))
+--   in 'dynamicallyParameterized' snippet decoder
+-- @
+--
+-- Without the Snippet API you would have had to implement the same functionality thus:
+--
+-- @
+-- selectSubstring' :: Text -> Maybe Int32 -> Maybe Int32 -> 'Statement' () Text
+-- selectSubstring' string from to = let
+--   sql = case (from, to) of
+--     (Just _, Just _) -> "select substring($1 from $2 to $3)"
+--     (Just _, Nothing) -> "select substring($1 from $2)"
+--     (Nothing, Just _) -> "select substring($1 to $2)"
+--     (Nothing, Nothing) -> "select substring($1)"
+--   encoder =
+--     Encoders.'Encoders.param' (string '>$' Encoders.'Encoders.text') '<>'
+--     foldMap (\\ x -> Encoders.'Encoders.param' (x '>$' Encoders.'Encoders.int8')) from '<>'
+--     foldMap (\\ x -> Encoders.'Encoders.param' (x '>$' Encoders.'Encoders.int8')) to
+--   decoder = Decoders.'Decoders.singleRow' (Decoders.'Decoders.column' (Decoders.'Decoders.nonNullable' Decoders.'Decoders.text'))
+--   in Statement.'Statement.preparable' sql encoder decoder
+-- @
+--
+-- As you can see, the Snippet API abstracts over placeholders and
+-- matching encoder generation, thus also protecting you from all sorts of related bugs.
+dynamicallyParameterized :: SnippetDefs.Snippet -> Decoders.Result result -> Statement () result
+dynamicallyParameterized snippet decoder =
+  let (sql, encoder) = SnippetDefs.compile snippet
+   in unpreparable sql encoder decoder
diff --git a/src/test/Main.hs b/src/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/test/Main.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
diff --git a/src/test/Regressions/Issue2Spec.hs b/src/test/Regressions/Issue2Spec.hs
new file mode 100644
--- /dev/null
+++ b/src/test/Regressions/Issue2Spec.hs
@@ -0,0 +1,19 @@
+module Regressions.Issue2Spec where
+
+import Hasql.Connection qualified as Connection
+import Hasql.Decoders qualified as Decoders
+import Hasql.DynamicStatements.Session qualified as Session
+import Hasql.DynamicStatements.Snippet qualified as Snippet
+import Test.Hspec
+import Prelude
+
+spec :: SpecWith Connection.Connection
+spec =
+  describe "Missing $ for 1000th parameter string error" do
+    it "Doesn't happen" \connection -> do
+      let snippet =
+            "SELECT 1 " <> (foldMap @[] ("," <>) (replicate 1001 (Snippet.param (10 :: Int64))))
+      result <- Connection.use connection (Session.dynamicallyParameterizedStatement snippet Decoders.noResult)
+      case result of
+        Right () -> pure ()
+        _ -> expectationFailure "Statement execution failed"
diff --git a/src/test/SpecHook.hs b/src/test/SpecHook.hs
new file mode 100644
--- /dev/null
+++ b/src/test/SpecHook.hs
@@ -0,0 +1,45 @@
+-- Docs: https://hspec.github.io/hspec-discover.html
+module SpecHook where
+
+import Hasql.Connection qualified as Connection
+import Hasql.Connection.Settings qualified as Settings
+import Test.Hspec
+import TestcontainersPostgresql qualified
+import Prelude
+
+type HookedSpec = SpecWith Connection.Connection
+
+hook :: HookedSpec -> Spec
+hook hookedSpec = parallel do
+  byDistro "postgres:10"
+  byDistro "postgres:17"
+  where
+    byDistro tagName =
+      describe (toList tagName) do
+        aroundAll
+          ( \onConnection ->
+              TestcontainersPostgresql.run
+                TestcontainersPostgresql.Config
+                  { tagName,
+                    auth = TestcontainersPostgresql.TrustAuth,
+                    forwardLogs = False
+                  }
+                ( \(host, port) -> do
+                    let settings =
+                          mconcat
+                            [ Settings.hostAndPort host port,
+                              Settings.user "postgres",
+                              Settings.dbname "postgres"
+                            ]
+                    bracket
+                      ( do
+                          res <- Connection.acquire settings
+                          case res of
+                            Left err -> fail ("Connection failed: " <> show err)
+                            Right conn -> pure conn
+                      )
+                      Connection.release
+                      onConnection
+                )
+          )
+          (parallel hookedSpec)
diff --git a/src/test/Units/SnippetSpec.hs b/src/test/Units/SnippetSpec.hs
new file mode 100644
--- /dev/null
+++ b/src/test/Units/SnippetSpec.hs
@@ -0,0 +1,27 @@
+module Units.SnippetSpec where
+
+import Hasql.Connection qualified as Connection
+import Hasql.Decoders qualified as Decoders
+import Hasql.DynamicStatements.Session qualified as Session
+import Hasql.DynamicStatements.Snippet qualified as Snippet
+import Test.Hspec
+import Prelude
+
+spec :: SpecWith Connection.Connection
+spec = do
+  describe "Select substring" do
+    it "Works" \connection -> do
+      let sample string from to =
+            let snippet =
+                  "select substring("
+                    <> Snippet.param @Text string
+                    <> foldMap (mappend " from " . Snippet.param @Int32) from
+                    <> foldMap (mappend " for " . Snippet.param @Int32) to
+                    <> ")"
+                decoder = Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.text))
+             in Connection.use connection (Session.dynamicallyParameterizedStatement snippet decoder)
+       in do
+            shouldBe (Right "bc") =<< sample "abcd" (Just 2) (Just 2)
+            shouldBe (Right "bcd") =<< sample "abcd" (Just 2) (Just 3)
+            shouldBe (Right "abc") =<< sample "abcd" Nothing (Just 3)
+            shouldBe (Right "bcd") =<< sample "abcd" (Just 2) Nothing
diff --git a/test/Main.hs b/test/Main.hs
deleted file mode 100644
--- a/test/Main.hs
+++ /dev/null
@@ -1,75 +0,0 @@
-module Main where
-
-import Data.ByteString qualified as ByteString
-import Data.ByteString.Char8 qualified as ByteStringChar8
-import Hasql.Connection qualified as Connection
-import Hasql.Decoders qualified as Decoders
-import Hasql.DynamicStatements.Session qualified as Session
-import Hasql.DynamicStatements.Snippet qualified as Snippet
-import Hasql.DynamicStatements.Statement qualified as Statement
-import Hasql.Session qualified as Session
-import Hasql.Statement qualified as Statement
-import Test.Tasty
-import Test.Tasty.HUnit
-import Prelude hiding (assert)
-
-main :: IO ()
-main =
-  defaultMain tree
-
-tree :: TestTree
-tree =
-  testGroup
-    "All tests"
-    [ testCase "Select substring"
-        $ let sample string from to =
-                let snippet =
-                      "select substring("
-                        <> Snippet.param @Text string
-                        <> foldMap (mappend " from " . Snippet.param @Int32) from
-                        <> foldMap (mappend " for " . Snippet.param @Int32) to
-                        <> ")"
-                    decoder = Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.text))
-                 in runSession (Session.dynamicallyParameterizedStatement snippet decoder True)
-           in do
-                assertEqual "" (Right (Right "bc")) =<< sample "abcd" (Just 2) (Just 2)
-                assertEqual "" (Right (Right "bcd")) =<< sample "abcd" (Just 2) (Just 3)
-                assertEqual "" (Right (Right "abc")) =<< sample "abcd" Nothing (Just 3)
-                assertEqual "" (Right (Right "bcd")) =<< sample "abcd" (Just 2) Nothing,
-      testGroup
-        "Regression"
-        [ testCase "Missing $ for 1000th parameter string #2"
-            $ let snippet =
-                    "SELECT 1 " <> (foldMap @[] ("," <>) $ replicate 1001 $ Snippet.param (10 :: Int64))
-                  statement =
-                    Statement.dynamicallyParameterized snippet Decoders.noResult True
-                  sql =
-                    case statement of
-                      Statement.Statement x _ _ _ -> x
-               in do
-                    assertBool (ByteStringChar8.unpack sql) (ByteString.isInfixOf "$1000" sql)
-        ]
-    ]
-
-runSession :: Session.Session a -> IO (Either Connection.ConnectionError (Either Session.SessionError a))
-runSession = withConnection . Session.run
-
-withConnection :: (Connection.Connection -> IO a) -> IO (Either Connection.ConnectionError a)
-withConnection handler =
-  runExceptT $ acquire >>= \connection -> use connection <* release connection
-  where
-    acquire =
-      ExceptT $ Connection.acquire settings
-      where
-        settings =
-          Connection.settings host port user password database
-          where
-            host = "localhost"
-            port = 5432
-            user = "postgres"
-            password = "postgres"
-            database = "postgres"
-    use connection =
-      lift $ handler connection
-    release connection =
-      lift $ Connection.release connection
