packages feed

hasql-dynamic-statements 0.3.1 → 0.5.1

raw patch · 16 files changed

Files

+ CHANGELOG.md view
@@ -0,0 +1,12 @@+# v0.5++- (Breaking) Removed `Hasql.DynamicStatements.Session` module+- (Breaking) Removed `Hasql.DynamicStatements.Statement` module+- Added `toStatement`, `toSession`, `toPipeline` functions to the `Hasql.DynamicStatements.Snippet` module instead++# 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
+ README.md view
@@ -0,0 +1,72 @@+# hasql-dynamic-statements++[![Hackage](https://img.shields.io/hackage/v/hasql-dynamic-statements.svg)](https://hackage.haskell.org/package/hasql-dynamic-statements)+[![Continuous Haddock](https://img.shields.io/badge/haddock-master-blue)](https://nikita-volkov.github.io/hasql-dynamic-statements/)++Hasql extension for composable, dynamic construction of SQL statements.++## Overview++This library provides a **Snippet** API that simplifies building SQL statements where the structure depends on runtime parameters. It abstracts over placeholder management and encoder matching, eliminating boilerplate and reducing bugs.++## Key Features++- **Composable**: Build statements using `Semigroup`/`Monoid` operations+- **Type-safe**: Automatic parameter encoding with type-driven encoder derivation+- **Dynamic**: Construct SQL conditionally based on parameters+- **Clean API**: No manual placeholder numbering or encoder alignment++## Quick Example++```haskell+import Hasql.DynamicStatements.Snippet++-- Build a dynamic substring query+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 <>+  ")"++-- Execute in a session+result <- toSession (selectSubstring "hello" (Just 2) Nothing) +  (singleRow $ column $ nonNullable text)+```++## Without Snippet API++Compare the above to manual construction:++```haskell+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.param (string >$ Encoders.text) <>+        foldMap (\x -> Encoders.param (x >$ Encoders.int8)) from <>+        foldMap (\x -> Encoders.param (x >$ Encoders.int8)) to+      decoder = singleRow $ column $ nonNullable text+  in Statement.preparable sql encoder decoder+```++The Snippet API eliminates placeholder numbering, pattern matching on parameter presence, and manual encoder sequencing.++## Core API++### Construction++- **`sql`** - Raw SQL text+- **`param`** - Parameter with implicit encoder+- **`encoderAndParam`** - Parameter with explicit encoder++### Execution++- **`toSql`** - Compile to SQL text with placeholders+- **`toStatement`** - Create a `Statement`+- **`toSession`** - Execute directly in `Session`+- **`toPipeline`** - Execute in `Pipeline`
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
hasql-dynamic-statements.cabal view
@@ -1,57 +1,138 @@+cabal-version: 3.0 name: hasql-dynamic-statements-version: 0.3.1-synopsis: Toolkit for constructing Hasql statements dynamically+version: 0.5.1+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 a composable API for building SQL statements dynamically.++  The core abstraction is 'Snippet', which allows you to construct SQL statements with parameters+  injected in-place, automatically handling placeholders and encoder matching. This is particularly+  useful when the structure of your SQL depends on runtime parameters.++  Key features:++  * Composable SQL snippets using 'Semigroup' and 'Monoid' instances+  * Automatic placeholder generation and parameter encoding+  * Type-safe parameter injection with implicit encoder derivation+  * Protection against common SQL construction bugs+  * Support for conditional SQL structure based on parameters++  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 <>+  >   ")"+ homepage: https://github.com/nikita-volkov/hasql-dynamic-statements-bug-reports: https://github.com/nikita-volkov/hasql-dynamic-statements/issues+bug-reports:+  https://github.com/nikita-volkov/hasql-dynamic-statements/issues+ author: Nikita Volkov <nikita.y.volkov@mail.ru> maintainer: Nikita Volkov <nikita.y.volkov@mail.ru> copyright: (c) 2019, Nikita Volkov license: MIT license-file: LICENSE-build-type: Simple-cabal-version: >=1.10+extra-doc-files:+  CHANGELOG.md+  README.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 -library-  hs-source-dirs: library-  default-extensions: BangPatterns, DeriveDataTypeable, DeriveGeneric, DeriveFunctor, DeriveTraversable, GeneralizedNewtypeDeriving, FlexibleContexts, FlexibleInstances, LambdaCase, NoImplicitPrelude, OverloadedStrings, RankNTypes, ScopedTypeVariables, StandaloneDeriving, TypeApplications, TypeFamilies-  ghc-options: -funbox-strict-fields+common base   default-language: Haskell2010+  default-extensions:+    ApplicativeDo+    BangPatterns+    BlockArguments+    DefaultSignatures+    DeriveDataTypeable+    DeriveFoldable+    DeriveFunctor+    DeriveGeneric+    DeriveTraversable+    DerivingVia+    EmptyDataDecls+    FlexibleContexts+    FlexibleInstances+    FunctionalDependencies+    GADTs+    GeneralizedNewtypeDeriving+    ImportQualifiedPost+    LambdaCase+    LiberalTypeSynonyms+    MultiParamTypeClasses+    MultiWayIf+    NamedFieldPuns+    NoImplicitPrelude+    NoMonomorphismRestriction+    OverloadedStrings+    PatternGuards+    QuasiQuotes+    RankNTypes+    RecordWildCards+    RoleAnnotations+    ScopedTypeVariables+    StandaloneDeriving+    StrictData+    TupleSections+    TypeApplications+    TypeFamilies+    TypeOperators++common executable+  import: base+  ghc-options:+    -O2+    -threaded+    -with-rtsopts=-N+    -rtsopts+    -funbox-strict-fields++common test+  import: base+  ghc-options:+    -threaded+    -with-rtsopts=-N++library+  import: base+  hs-source-dirs: src/library   exposed-modules:-    Hasql.DynamicStatements.Session     Hasql.DynamicStatements.Snippet-    Hasql.DynamicStatements.Statement+   other-modules:     Hasql.DynamicStatements.Prelude-    Hasql.DynamicStatements.Snippet.Defs+   build-depends:-    base >=4.12 && <5,-    bytestring >=0.10 && <0.12,-    containers >=0.6 && <0.7,-    hasql >=1.4 && <1.5,-    hasql-implicits >=0.1 && <0.2,-    ptr >=0.16.7.2 && <0.17+    base >=4.14 && <5,+    bytestring >=0.10 && <0.13,+    containers >=0.6 && <0.8,+    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-  default-extensions: Arrows, BangPatterns, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFoldable, DeriveFunctor, DeriveGeneric, DeriveTraversable, EmptyDataDecls, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, LambdaCase, LiberalTypeSynonyms, MagicHash, MultiParamTypeClasses, MultiWayIf, NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, PatternGuards, ParallelListComp, QuasiQuotes, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, TemplateHaskell, TupleSections, TypeApplications, TypeFamilies, TypeOperators, UnboxedTuples-  default-language: Haskell2010+  other-modules:+    Regressions.Issue2Spec+    SpecHook+    Units.SnippetSpec++  build-tool-depends:+    hspec-discover:hspec-discover ^>=2.11.12+   build-depends:     hasql,     hasql-dynamic-statements,-    QuickCheck >=2.8.1 && <3,-    quickcheck-instances >=0.3.11 && <0.4,+    hspec ^>=2.11.12,     rerebase <2,-    tasty >=0.12 && <2,-    tasty-hunit >=0.9 && <0.11,-    tasty-quickcheck >=0.9 && <0.11+    testcontainers-postgresql >=0.2 && <0.3,
− library/Hasql/DynamicStatements/Prelude.hs
@@ -1,82 +0,0 @@-module Hasql.DynamicStatements.Prelude-(-  module Exports,-)-where----- base---------------------------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, mapM_, sequence_, forM_, msum, mapM, sequence, forM)-import Control.Monad.IO.Class as Exports-import Control.Monad.Fail as Exports-import Control.Monad.Fix as Exports hiding (fix)-import Control.Monad.ST as Exports-import Data.Bits as Exports-import Data.Bool as Exports-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-import Data.Functor.Contravariant as Exports-import Data.Functor.Identity as Exports-import Data.Int as Exports-import Data.IORef as Exports-import Data.Ix as Exports-import Data.List as Exports hiding (sortOn, isSubsequenceOf, uncons, concat, foldr, foldl1, maximum, minimum, product, sum, all, and, any, concatMap, elem, foldl, foldr1, notElem, or, find, maximumBy, minimumBy, mapAccumL, mapAccumR, foldl')-import Data.Maybe as Exports-import Data.Monoid as Exports hiding (Last(..), First(..), (<>))-import Data.Ord as Exports-import Data.Proxy as Exports-import Data.Ratio as Exports-import Data.Semigroup as Exports-import Data.STRef as Exports-import Data.String as Exports-import Data.Traversable as Exports-import Data.Tuple as Exports-import Data.Unique as Exports-import Data.Version as Exports-import Data.Word as Exports-import Debug.Trace as Exports-import Foreign.ForeignPtr as Exports-import Foreign.Ptr as Exports-import Foreign.StablePtr as Exports-import Foreign.Storable as Exports hiding (sizeOf, alignment)-import GHC.Conc as Exports hiding (withMVar, threadWaitWriteSTM, threadWaitWrite, threadWaitReadSTM, threadWaitRead)-import GHC.Exts as Exports (lazy, inline, sortWith, groupWith, IsList(..))-import GHC.Generics as Exports (Generic, Generic1)-import GHC.IO.Exception as Exports-import Numeric as Exports-import Prelude as Exports hiding (fail, concat, foldr, mapM_, sequence_, foldl1, maximum, minimum, product, sum, all, and, any, concatMap, elem, foldl, foldr1, notElem, or, mapM, sequence, id, (.))-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.ParserCombinators.ReadP as Exports (ReadP, ReadS, readP_to_S, readS_to_P)-import Text.ParserCombinators.ReadPrec as Exports (ReadPrec, readPrec_to_P, readP_to_Prec, readPrec_to_S, readS_to_Prec)-import Text.Printf as Exports (printf, hPrintf)-import Text.Read as Exports (Read(..), readMaybe, readEither)-import Unsafe.Coerce as Exports---- containers---------------------------import Data.Sequence as Exports (Seq)---- bytestring---------------------------import Data.ByteString as Exports (ByteString)
− library/Hasql/DynamicStatements/Session.hs
@@ -1,20 +0,0 @@-module Hasql.DynamicStatements.Session where--import Hasql.DynamicStatements.Prelude-import Hasql.Session-import qualified Hasql.DynamicStatements.Snippet.Defs as SnippetDefs-import qualified Hasql.DynamicStatements.Statement as Statement-import qualified Hasql.Decoders as Decoders-import qualified Hasql.Session 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,10 +0,0 @@-module Hasql.DynamicStatements.Snippet-(-  Snippet,-  param,-  encoderAndParam,-  sql,-)-where--import Hasql.DynamicStatements.Snippet.Defs
− library/Hasql/DynamicStatements/Snippet/Defs.hs
@@ -1,59 +0,0 @@-module Hasql.DynamicStatements.Snippet.Defs where--import Hasql.DynamicStatements.Prelude-import qualified Hasql.Encoders as Encoders-import qualified Hasql.Implicits.Encoders 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,66 +0,0 @@-module Hasql.DynamicStatements.Statement where--import Hasql.DynamicStatements.Prelude-import Hasql.Statement-import qualified Hasql.DynamicStatements.Snippet.Defs as SnippetDefs-import qualified Hasql.Decoders as Decoders-import qualified Hasql.Encoders as Encoders-import qualified Ptr.Poking as Poking-import qualified Ptr.ByteString as ByteString---{-|-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.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/Snippet.hs view
@@ -0,0 +1,170 @@+module Hasql.DynamicStatements.Snippet+  ( Snippet,++    -- * Execution+    toSql,+    toStatement,+    toPreparableStatement,+    toSession,+    toPipeline,++    -- * Construction+    param,+    encoderAndParam,+    sql,+  )+where++import Hasql.Decoders qualified as Decoders+import Hasql.DynamicStatements.Prelude+import Hasql.Encoders qualified as Encoders+import Hasql.Implicits.Encoders qualified as Encoders+import Hasql.Pipeline qualified as Pipeline+import Hasql.Session qualified as Session+import Hasql.Statement qualified as Statement+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.Snippet.toStatement' or directly execute it in+-- 'Hasql.Session.Session' using+-- 'Hasql.DynamicStatements.Snippet.toSession'.+data Snippet+  = Snippet+      (Int -> TextBuilder.TextBuilder)+      Int+      (Encoders.Params ())++instance Semigroup Snippet where+  Snippet sql1 paramCount1 encoder1 <> Snippet sql2 paramCount2 encoder2 =+    Snippet+      (\paramId -> sql1 paramId <> sql2 (paramId + paramCount1))+      (paramCount1 + paramCount2)+      (encoder1 <> encoder2)++instance Monoid Snippet where+  mempty = Snippet (\_ -> mempty) 0 mempty++instance IsString Snippet where+  fromString = sql . fromString++-- |+-- SQL chunk.+sql :: Text -> Snippet+sql x = Snippet (const (TextBuilder.text x)) 0 mempty++-- |+-- 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+    (\paramId -> "$" <> TextBuilder.decimal paramId)+    1+    (param >$ Encoders.param encoder)++-- |+-- Compile a snippet into SQL text with placeholders.+toSql :: Snippet -> Text+toSql (Snippet sql _ _) =+  TextBuilder.toText (sql 1)++-- |+-- 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.'param' string <>+--     foldMap (mappend " from " . Snippet.'param') from <>+--     foldMap (mappend " for " . Snippet.'param') to <>+--     ")"+--   decoder = Decoders.'Decoders.singleRow' (Decoders.'Decoders.column' (Decoders.'Decoders.nonNullable' Decoders.'Decoders.text'))+--   in 'toStatement' 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.unpreparable' 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.+toStatement :: Snippet -> Decoders.Result result -> Statement.Statement () result+toStatement = toStatement' Statement.unpreparable++toPreparableStatement :: Snippet -> Decoders.Result result -> Statement.Statement () result+toPreparableStatement = toStatement' Statement.preparable++toStatement' :: (Text -> Encoders.Params () -> t) -> Snippet -> t+toStatement' stmt (Snippet sql _ encoder) =+  stmt (TextBuilder.toText (sql 1)) encoder++-- |+-- Execute in @Session.Session@, providing a result decoder.+--+-- This is merely a shortcut defined thus:+--+-- @+-- toSession snippet decoder =+--   Session.statement () (Snippet.toStatement snippet decoder)+-- @+toSession :: Snippet -> Decoders.Result result -> Session.Session result+toSession snippet decoder =+  Session.statement () (toStatement snippet decoder)++-- |+-- Execute in @Pipeline.Pipeline@, providing a result decoder.+--+-- This is merely a shortcut defined thus:+--+-- @+-- toPipeline snippet decoder =+--   Pipeline.statement () (Snippet.toStatement snippet decoder)+-- @+toPipeline :: Snippet -> Decoders.Result result -> Pipeline.Pipeline result+toPipeline snippet decoder =+  Pipeline.statement () (toStatement snippet 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.Snippet qualified as Snippet+import Hasql.Session qualified as Session+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.statement () (Snippet.toStatement 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.Snippet qualified as Snippet+import Hasql.Session qualified as Session+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.statement () (Snippet.toStatement 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,78 +0,0 @@-module Main where--import Prelude hiding (assert)-import Test.QuickCheck.Instances-import Test.Tasty-import Test.Tasty.Runners-import Test.Tasty.HUnit-import Test.Tasty.QuickCheck-import qualified Test.QuickCheck as QuickCheck-import qualified Hasql.Statement as Statement-import qualified Hasql.Encoders as Encoders-import qualified Hasql.Decoders as Decoders-import qualified Hasql.Session as Session-import qualified Hasql.Connection as Connection-import qualified Hasql.DynamicStatements.Snippet as Snippet-import qualified Hasql.DynamicStatements.Session as Session-import qualified Hasql.DynamicStatements.Statement as Statement-import qualified Data.ByteString as ByteString-import qualified Data.ByteString.Char8 as ByteStringChar8---main =-  defaultMain tree--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.QueryError 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 = ""-            database = "postgres"-    use connection =-      lift $ handler connection-    release connection =-      lift $ Connection.release connection