packages feed

hasql-dynamic-statements 0.3.1.8 → 0.4

raw patch · 17 files changed

+342/−305 lines, 17 filesdep +hspecdep +testcontainers-postgresqldep +textdep −ptrdep −tastydep −tasty-hunitdep ~hasqldep ~hasql-implicitsPVP ok

version bump matches the API change (PVP)

Dependencies added: hspec, testcontainers-postgresql, text, text-builder

Dependencies removed: ptr, tasty, tasty-hunit

Dependency ranges changed: hasql, hasql-implicits

API changes (from Hackage documentation)

- Hasql.DynamicStatements.Session: dynamicallyParameterizedStatement :: Snippet -> Result result -> Bool -> Session result
+ Hasql.DynamicStatements.Session: dynamicallyParameterizedStatement :: Snippet -> Result result -> Session result
- Hasql.DynamicStatements.Snippet: sql :: ByteString -> Snippet
+ Hasql.DynamicStatements.Snippet: sql :: Text -> Snippet
- Hasql.DynamicStatements.Statement: dynamicallyParameterized :: Snippet -> Result result -> Bool -> Statement () result
+ Hasql.DynamicStatements.Statement: dynamicallyParameterized :: Snippet -> Result result -> Statement () result

Files

+ CHANGELOG.md view
@@ -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
hasql-dynamic-statements.cabal view
@@ -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,
− library/Hasql/DynamicStatements/Prelude.hs
@@ -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, (.))
− library/Hasql/DynamicStatements/Session.hs
@@ -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)
− library/Hasql/DynamicStatements/Snippet.hs
@@ -1,9 +0,0 @@-module Hasql.DynamicStatements.Snippet-  ( Snippet,-    param,-    encoderAndParam,-    sql,-  )-where--import Hasql.DynamicStatements.Snippet.Defs
− library/Hasql/DynamicStatements/Snippet/Defs.hs
@@ -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)))
− library/Hasql/DynamicStatements/Statement.hs
@@ -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
+ src/library/Hasql/DynamicStatements/Prelude.hs view
@@ -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, (.))
+ src/library/Hasql/DynamicStatements/Session.hs view
@@ -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)
+ src/library/Hasql/DynamicStatements/Snippet.hs view
@@ -0,0 +1,9 @@+module Hasql.DynamicStatements.Snippet+  ( Snippet,+    param,+    encoderAndParam,+    sql,+  )+where++import Hasql.DynamicStatements.Snippet.Defs
+ src/library/Hasql/DynamicStatements/Snippet/Defs.hs view
@@ -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)
+ src/library/Hasql/DynamicStatements/Statement.hs view
@@ -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
+ src/test/Main.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ src/test/Regressions/Issue2Spec.hs view
@@ -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"
+ src/test/SpecHook.hs view
@@ -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)
+ src/test/Units/SnippetSpec.hs view
@@ -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
− test/Main.hs
@@ -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