hasql-dynamic-statements 0.3.1 → 0.5.1
raw patch · 16 files changed
Files
- CHANGELOG.md +12/−0
- README.md +72/−0
- Setup.hs +0/−2
- hasql-dynamic-statements.cabal +112/−31
- library/Hasql/DynamicStatements/Prelude.hs +0/−82
- library/Hasql/DynamicStatements/Session.hs +0/−20
- library/Hasql/DynamicStatements/Snippet.hs +0/−10
- library/Hasql/DynamicStatements/Snippet/Defs.hs +0/−59
- library/Hasql/DynamicStatements/Statement.hs +0/−66
- src/library/Hasql/DynamicStatements/Prelude.hs +71/−0
- src/library/Hasql/DynamicStatements/Snippet.hs +170/−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/−78
+ 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++[](https://hackage.haskell.org/package/hasql-dynamic-statements)+[](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