packages feed

hasql-dynamic-statements-0.3.1.3: library/Hasql/DynamicStatements/Statement.hs

module Hasql.DynamicStatements.Statement where

import qualified Hasql.Decoders as Decoders
import Hasql.DynamicStatements.Prelude
import qualified Hasql.DynamicStatements.Snippet.Defs as SnippetDefs
import qualified Hasql.Encoders as Encoders
import Hasql.Statement
import qualified Ptr.ByteString as ByteString
import qualified Ptr.Poking 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.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