hasql-dynamic-statements 0.5 → 0.5.0.1
raw patch · 3 files changed
+120/−30 lines, 3 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- README.md +72/−0
- hasql-dynamic-statements.cabal +24/−2
- src/library/Hasql/DynamicStatements/Snippet.hs +24/−28
+ 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`
hasql-dynamic-statements.cabal view
@@ -1,10 +1,31 @@ cabal-version: 3.0 name: hasql-dynamic-statements-version: 0.5+version: 0.5.0.1 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.+ 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@@ -16,6 +37,7 @@ license-file: LICENSE extra-doc-files: CHANGELOG.md+ README.md source-repository head type: git
src/library/Hasql/DynamicStatements/Snippet.hs view
@@ -45,23 +45,29 @@ -- '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 ())+data Snippet+ = Snippet+ (Int -> TextBuilder.TextBuilder)+ Int+ (Encoders.Params ()) -deriving instance Semigroup Snippet+instance Semigroup Snippet where+ Snippet sql1 paramCount1 encoder1 <> Snippet sql2 paramCount2 encoder2 =+ Snippet+ (\paramId -> sql1 paramId <> sql2 (paramId + paramCount1))+ (paramCount1 + paramCount2)+ (encoder1 <> encoder2) -deriving instance Monoid Snippet+instance Monoid Snippet where+ mempty = Snippet (\_ -> mempty) 0 mempty instance IsString Snippet where- fromString x = Snippet (pure (StringSnippetChunk (fromString x)))+ fromString = sql . fromString -- | -- SQL chunk. sql :: Text -> Snippet-sql x = Snippet (pure (StringSnippetChunk (TextBuilder.text x)))+sql x = Snippet (const (TextBuilder.text x)) 0 mempty -- | -- Parameter encoded using an implicitly derived encoder from the type.@@ -71,26 +77,17 @@ -- | -- 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)+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 = fst (compile snippet)+toSql (Snippet sql _ _) =+ TextBuilder.toText (sql 1) -- | -- Construct a statement dynamically, specifying the parameters in-place@@ -136,9 +133,8 @@ -- 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+toStatement (Snippet sql _ encoder) =+ Statement.unpreparable (TextBuilder.toText (sql 1)) encoder -- | -- Execute in @Session.Session@, providing a result decoder.