Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add function statement and remove the decidable instance on Params #159

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 1.7

- Added `Statement.function` for easier integration with stored procedures.
- Decidable instance on `Encoders.Params` removed. It was useless and limited the design.

# 1.6.3.1

- Moved to "postgresql-libpq-0.10"
Expand Down
2 changes: 2 additions & 0 deletions hasql.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ library
Hasql.PTI
Hasql.Session.Core
Hasql.Settings
Hasql.Statement.Function
Hasql.Statement.Function.SqlBuilder

build-depends:
, aeson >=2 && <3
Expand Down
2 changes: 1 addition & 1 deletion library/Hasql/Encoders/All.hs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import qualified Text.Builder as C
-- Female -> "female"
-- @
newtype Params a = Params (Params.Params a)
deriving (Contravariant, Divisible, Decidable, Monoid, Semigroup)
deriving (Contravariant, Divisible, Monoid, Semigroup)

-- |
-- No parameters. Same as `mempty` and `conquered`.
Expand Down
77 changes: 62 additions & 15 deletions library/Hasql/Encoders/Params.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,70 @@ import qualified Text.Builder as E

-- |
-- Encoder of some representation of a parameters product.
newtype Params a
= Params (Op (DList (A.Oid, A.Format, Bool -> Maybe ByteString, Text)) a)
deriving (Contravariant, Divisible, Decidable, Semigroup, Monoid)
data Params a = Params
{ size :: !Int,
columnsMetadata :: !(DList (A.Oid, A.Format)),
serializer :: Bool -> a -> DList (Maybe ByteString),
printer :: a -> DList Text
}

instance Contravariant Params where
contramap fn (Params size columnsMetadata oldSerializer oldPrinter) = Params {..}
where
serializer idt = oldSerializer idt . fn
printer = oldPrinter . fn

instance Divisible Params where
divide
divisor
(Params leftSize leftColumnsMetadata leftSerializer leftPrinter)
(Params rightSize rightColumnsMetadata rightSerializer rightPrinter) =
Params
{ size = leftSize + rightSize,
columnsMetadata = leftColumnsMetadata <> rightColumnsMetadata,
serializer = \idt input -> case divisor input of
(leftInput, rightInput) -> leftSerializer idt leftInput <> rightSerializer idt rightInput,
printer = \input -> case divisor input of
(leftInput, rightInput) -> leftPrinter leftInput <> rightPrinter rightInput
}
conquer =
Params
{ size = 0,
columnsMetadata = mempty,
serializer = mempty,
printer = mempty
}

instance Semigroup (Params a) where
Params leftSize leftColumnsMetadata leftSerializer leftPrinter <> Params rightSize rightColumnsMetadata rightSerializer rightPrinter =
Params
{ size = leftSize + rightSize,
columnsMetadata = leftColumnsMetadata <> rightColumnsMetadata,
serializer = \idt input -> leftSerializer idt input <> rightSerializer idt input,
printer = \input -> leftPrinter input <> rightPrinter input
}

instance Monoid (Params a) where
mempty = conquer

value :: C.Value a -> Params a
value =
contramap Just . nullableValue
value (C.Value valueOID _ serialize print) =
Params
{ size = 1,
columnsMetadata = pure (pqOid, format),
serializer = \idt -> pure . Just . B.encodingBytes . serialize idt,
printer = pure . E.run . print
}
where
D.OID _ pqOid format = valueOID

nullableValue :: C.Value a -> Params (Maybe a)
nullableValue (C.Value valueOID arrayOID encode render) =
nullableValue (C.Value valueOID _ serialize print) =
Params
$ Op
$ \input ->
let D.OID _ pqOid format =
valueOID
encoder env =
fmap (B.encodingBytes . encode env) input
rendering =
maybe "null" (E.run . render) input
in pure (pqOid, format, encoder, rendering)
{ size = 1,
columnsMetadata = pure (pqOid, format),
serializer = \idt -> pure . fmap (B.encodingBytes . serialize idt),
printer = pure . maybe "null" (E.run . print)
}
where
D.OID _ pqOid format = valueOID
32 changes: 18 additions & 14 deletions library/Hasql/IO.hs
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,17 @@ sendPreparedParametricStatement ::
ParamsEncoders.Params a ->
a ->
IO (Either CommandError ())
sendPreparedParametricStatement connection registry integerDatetimes template (ParamsEncoders.Params (Op encoderOp)) input =
let (oidList, valueAndFormatList) =
let step (oid, format, encoder, _) ~(oidList, bytesAndFormatList) =
(,)
(oid : oidList)
(fmap (\bytes -> (bytes, format)) (encoder integerDatetimes) : bytesAndFormatList)
in foldr step ([], []) (encoderOp input)
in runExceptT $ do
key <- ExceptT $ getPreparedStatementKey connection registry template oidList
ExceptT $ checkedSend connection $ LibPQ.sendQueryPrepared connection key valueAndFormatList LibPQ.Binary
sendPreparedParametricStatement connection registry integerDatetimes template (ParamsEncoders.Params size columnsMetadata serializer _) input =
runExceptT $ do
key <- ExceptT $ getPreparedStatementKey connection registry template oidList
ExceptT $ checkedSend connection $ LibPQ.sendQueryPrepared connection key valueAndFormatList LibPQ.Binary
where
(oidList, formatList) =
columnsMetadata & toList & unzip
valueAndFormatList =
serializer integerDatetimes input
& toList
& zipWith (\format encoding -> (,format) <$> encoding) formatList

{-# INLINE sendUnpreparedParametricStatement #-}
sendUnpreparedParametricStatement ::
Expand All @@ -133,11 +134,14 @@ sendUnpreparedParametricStatement ::
ParamsEncoders.Params a ->
a ->
IO (Either CommandError ())
sendUnpreparedParametricStatement connection integerDatetimes template (ParamsEncoders.Params (Op encoderOp)) input =
sendUnpreparedParametricStatement connection integerDatetimes template (ParamsEncoders.Params _ columnsMetadata serializer printer) input =
let params =
let step (oid, format, encoder, _) acc =
((,,) <$> pure oid <*> encoder integerDatetimes <*> pure format) : acc
in foldr step [] (encoderOp input)
zipWith
( \(oid, format) encoding ->
(,,) <$> pure oid <*> encoding <*> pure format
)
(toList columnsMetadata)
(toList (serializer integerDatetimes input))
in checkedSend connection $ LibPQ.sendQueryParams connection template params LibPQ.Binary

{-# INLINE sendParametricStatement #-}
Expand Down
8 changes: 3 additions & 5 deletions library/Hasql/Session/Core.hs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ sql sql =
-- |
-- Parameters and a specification of a parametric single-statement query to apply them to.
statement :: params -> Statement.Statement params result -> Session result
statement input (Statement.Statement template (Encoders.Params paramsEncoder) decoder preparable) =
statement input (Statement.Statement template (Encoders.Params paramsEncoder@(Encoders.Params.Params _ _ _ printer)) decoder preparable) =
Session
$ ReaderT
$ \(Connection.Connection pqConnectionRef integerDatetimes registry) ->
Expand All @@ -59,7 +59,5 @@ statement input (Statement.Statement template (Encoders.Params paramsEncoder) de
return $ r1 *> r2
where
inputReps =
let Encoders.Params.Params (Op encoderOp) = paramsEncoder
step (_, _, _, rendering) acc =
rendering : acc
in foldr step [] (encoderOp input)
printer input
& toList
23 changes: 23 additions & 0 deletions library/Hasql/Statement.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Hasql.Statement
( Statement (..),
function,
refineResult,

-- * Recipies
Expand All @@ -16,6 +17,7 @@ import qualified Hasql.Decoders as Decoders
import qualified Hasql.Decoders.All as Decoders
import qualified Hasql.Encoders as Encoders
import Hasql.Prelude
import qualified Hasql.Statement.Function as Function

-- |
-- Specification of a strictly single-statement query, which can be parameterized and prepared.
Expand Down Expand Up @@ -117,3 +119,24 @@ refineResult refiner (Statement template encoder decoder preparable) =
--
-- For details see
-- <https://www.postgresql.org/docs/9.6/static/functions-comparisons.html#AEN20944 the Postgresql docs>.

-- |
-- Utility for execution of stored procedures and functions, which automates the SQL generation.
--
-- Produces SQL like the following:
--
-- > SELECT function_name($1, $2, ..)
--
-- with the amount of parameter placeholders derived from the encoder.
function ::
-- | Function name.
--
-- Will get automatically escaped for injection-safety.
Text ->
Encoders.Params a ->
Decoders.Result b ->
-- | Whether the statement should be prepared.
Bool ->
Statement a b
function functionName encoders =
Statement (Function.sql functionName encoders) encoders
13 changes: 13 additions & 0 deletions library/Hasql/Statement/Function.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Hasql.Statement.Function where

import qualified ByteString.StrictBuilder as Builder
import qualified Hasql.Encoders.All as Encoders
import qualified Hasql.Encoders.Params as Encoders.Params
import Hasql.Prelude
import qualified Hasql.Statement.Function.SqlBuilder as SqlBuilder

sql :: Text -> Encoders.Params a -> ByteString
sql name (Encoders.Params (Encoders.Params.Params size _ _ _)) =
Builder.builderBytes
$ SqlBuilder.sql name
$ size
25 changes: 25 additions & 0 deletions library/Hasql/Statement/Function/SqlBuilder.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module Hasql.Statement.Function.SqlBuilder where

import ByteString.StrictBuilder
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Hasql.Prelude

sql :: Text -> Int -> Builder
sql name size =
"select " <> functionName name <> "(" <> arguments size <> ")"

functionName :: Text -> Builder
functionName =
mappend "\"" . flip mappend "\"" . bytes . Text.encodeUtf8 . Text.replace "\"" ""

arguments :: Int -> Builder
arguments size =
[1 .. size]
& fmap argument
& intersperse ", "
& mconcat

argument :: Int -> Builder
argument num =
"$" <> asciiIntegral num
26 changes: 0 additions & 26 deletions tasty/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -372,32 +372,6 @@ tree =
Encoders.param (Encoders.nonNullable (Encoders.unknown))
in DSL.statement "ok" statement
in actualIO >>= assertEqual "" (Right True),
testCase "Textual Unknown"
$ let actualIO =
DSL.session $ do
let statement =
Statement.Statement sql mempty Decoders.noResult True
where
sql =
"create or replace function overloaded(a int, b int) returns int as $$ select a + b $$ language sql;"
in DSL.statement () statement
let statement =
Statement.Statement sql mempty Decoders.noResult True
where
sql =
"create or replace function overloaded(a text, b text, c text) returns text as $$ select a || b || c $$ language sql;"
in DSL.statement () statement
let statement =
Statement.Statement sql encoder decoder True
where
sql =
"select overloaded($1, $2) || overloaded($3, $4, $5)"
decoder =
(Decoders.singleRow ((Decoders.column . Decoders.nonNullable) (Decoders.text)))
encoder =
contramany (Encoders.param (Encoders.nonNullable (Encoders.unknown)))
in DSL.statement ["1", "2", "4", "5", "6"] statement
in actualIO >>= assertEqual "" (Right "3456"),
testCase "Enum"
$ let actualIO =
DSL.session $ do
Expand Down
Loading