hasql-dynamic-statements 0.4 → 0.5
raw patch · 8 files changed
+170/−150 lines, 8 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Hasql.DynamicStatements.Session: dynamicallyParameterizedStatement :: Snippet -> Result result -> Session result
- Hasql.DynamicStatements.Statement: dynamicallyParameterized :: Snippet -> Result result -> Statement () result
+ Hasql.DynamicStatements.Snippet: instance GHC.Internal.Base.Monoid Hasql.DynamicStatements.Snippet.Snippet
+ Hasql.DynamicStatements.Snippet: instance GHC.Internal.Base.Semigroup Hasql.DynamicStatements.Snippet.Snippet
+ Hasql.DynamicStatements.Snippet: instance GHC.Internal.Data.String.IsString Hasql.DynamicStatements.Snippet.Snippet
+ Hasql.DynamicStatements.Snippet: toPipeline :: Snippet -> Result result -> Pipeline result
+ Hasql.DynamicStatements.Snippet: toSession :: Snippet -> Result result -> Session result
+ Hasql.DynamicStatements.Snippet: toSql :: Snippet -> Text
+ Hasql.DynamicStatements.Snippet: toStatement :: Snippet -> Result result -> Statement () result
Files
- CHANGELOG.md +6/−0
- hasql-dynamic-statements.cabal +1/−4
- src/library/Hasql/DynamicStatements/Session.hs +0/−18
- src/library/Hasql/DynamicStatements/Snippet.hs +159/−1
- src/library/Hasql/DynamicStatements/Snippet/Defs.hs +0/−70
- src/library/Hasql/DynamicStatements/Statement.hs +0/−53
- src/test/Regressions/Issue2Spec.hs +2/−2
- src/test/Units/SnippetSpec.hs +2/−2
CHANGELOG.md view
@@ -1,3 +1,9 @@+# 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
hasql-dynamic-statements.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: hasql-dynamic-statements-version: 0.4+version: 0.5 synopsis: Hasql extension for dynamic construction of statements description: Library extending Hasql with an abstraction that simplifies construction of SQL statements, the structure of which depends on the parameters.@@ -81,13 +81,10 @@ 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.14 && <5,
− src/library/Hasql/DynamicStatements/Session.hs
@@ -1,18 +0,0 @@-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
@@ -1,9 +1,167 @@ module Hasql.DynamicStatements.Snippet ( Snippet,++ -- * Execution+ toSql,+ toStatement,+ toSession,+ toPipeline,++ -- * Construction param, encoderAndParam, sql, ) where -import Hasql.DynamicStatements.Snippet.Defs+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'.+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)++-- |+-- Compile a snippet into SQL text with placeholders.+toSql :: Snippet -> Text+toSql snippet = fst (compile snippet)++-- |+-- 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.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.+toStatement :: Snippet -> Decoders.Result result -> Statement.Statement () result+toStatement snippet =+ let (sql, encoder) = compile snippet+ in Statement.unpreparable sql 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/library/Hasql/DynamicStatements/Snippet/Defs.hs
@@ -1,70 +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-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
@@ -1,53 +0,0 @@-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/Regressions/Issue2Spec.hs view
@@ -2,8 +2,8 @@ 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.Session qualified as Session import Test.Hspec import Prelude @@ -13,7 +13,7 @@ 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)+ result <- Connection.use connection (Session.statement () (Snippet.toStatement snippet Decoders.noResult)) case result of Right () -> pure () _ -> expectationFailure "Statement execution failed"
src/test/Units/SnippetSpec.hs view
@@ -2,8 +2,8 @@ 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.Session qualified as Session import Test.Hspec import Prelude @@ -19,7 +19,7 @@ <> 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 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)