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 +6/−0
- hasql-dynamic-statements.cabal +23/−14
- library/Hasql/DynamicStatements/Prelude.hs +0/−70
- library/Hasql/DynamicStatements/Session.hs +0/−19
- library/Hasql/DynamicStatements/Snippet.hs +0/−9
- library/Hasql/DynamicStatements/Snippet/Defs.hs +0/−55
- library/Hasql/DynamicStatements/Statement.hs +0/−63
- src/library/Hasql/DynamicStatements/Prelude.hs +71/−0
- src/library/Hasql/DynamicStatements/Session.hs +18/−0
- src/library/Hasql/DynamicStatements/Snippet.hs +9/−0
- src/library/Hasql/DynamicStatements/Snippet/Defs.hs +70/−0
- src/library/Hasql/DynamicStatements/Statement.hs +53/−0
- src/test/Main.hs +1/−0
- src/test/Regressions/Issue2Spec.hs +19/−0
- src/test/SpecHook.hs +45/−0
- src/test/Units/SnippetSpec.hs +27/−0
- test/Main.hs +0/−75
+ 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