hasql-interpolate-1.0.0.0: lib/Hasql/Interpolate/Internal/EncodeRow.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MonoLocalBinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Hasql.Interpolate.Internal.EncodeRow
( EncodeRow (..),
GEncodeRow (..),
toTable,
)
where
import Control.Monad
import Data.Functor.Contravariant
import Data.List (intersperse)
import Data.Monoid
import GHC.Generics
import qualified Hasql.Encoders as E
import Hasql.Interpolate.Internal.EncodeRow.TH
import Hasql.Interpolate.Internal.Encoder
import Hasql.Interpolate.Internal.Sql
import Hasql.Interpolate.Internal.TH (addParam)
class EncodeRow a where
-- | The continuation @(forall x. (a -> x -> x) -> x -> E.Params x
-- -> Int -> r)@ is given cons @(a -> x -> x)@ and nil @(x)@ for some
-- existential type @x@ and an encoder (@'E.Params' x@) for @x@. An
-- Int is also given to tally up how many sql fields are in the
-- unzipped structure.
--
-- ==== __Example__
--
-- Consider the following manually written instance:
--
-- @
-- data Blerg = Blerg Int64 Bool Text Char
--
-- instance EncodeRow Blerg where
-- unzipWithEncoder k = k cons nil enc 4
-- where
-- cons (Blerg a b c d) ~(as, bs, cs, ds) =
-- (a : as, b : bs, c : cs, d : ds)
-- nil = ([], [], [], [])
-- enc =
-- ((\(x, _, _, _) -> x) >$< param encodeField)
-- <> ((\(_, x, _, _) -> x) >$< param encodeField)
-- <> ((\(_, _, x, _) -> x) >$< param encodeField)
-- <> ((\(_, _, _, x) -> x) >$< param encodeField)
-- @
--
-- We chose @([Int64], [Bool], [Text], [Char])@ as our existential
-- type. If we instead use the default instance based on
-- 'GEncodeRow' then we would produce the same code as the
-- instance below:
--
-- @
-- instance EncodeRow Blerg where
-- unzipWithEncoder k = k cons nil enc 4
-- where
-- cons (Blerg a b c d) ~(~(as, bs), ~(cs, ds)) =
-- ((a : as, b : bs), (c : cs, d : ds))
-- nil = (([], []), ([], []))
-- enc =
-- ((\((x, _), _) -> x) >$< param encodeField)
-- <> ((\((_, x), _) -> x) >$< param encodeField)
-- <> ((\(_ , (x, _)) -> x) >$< param encodeField)
-- <> ((\(_ , (_, x)) -> x) >$< param encodeField)
-- @
--
-- The notable difference being we don't produce a flat tuple, but
-- instead produce a balanced tree of tuples isomorphic to the
-- balanced tree of @':*:'@ from the generic 'Rep' of @Blerg@.
unzipWithEncoder :: (forall x. (a -> x -> x) -> x -> E.Params x -> Int -> r) -> r
default unzipWithEncoder ::
(Generic a, GEncodeRow (Rep a)) =>
(forall x. (a -> x -> x) -> x -> E.Params x -> Int -> r) ->
r
unzipWithEncoder k = gUnzipWithEncoder \cons nil enc fc ->
k (cons . from) nil enc fc
{-# INLINE unzipWithEncoder #-}
class GEncodeRow a where
gUnzipWithEncoder :: (forall x. (a p -> x -> x) -> x -> E.Params x -> Int -> r) -> r
-- | 'toTable' takes some list of products into the corresponding
-- relation in sql. It is applying the @unnest@ based technique
-- described [in the hasql
-- documentation](https://hackage.haskell.org/package/hasql-1.4.5.1/docs/Hasql-Statement.html#g:2).
--
-- ==== __Example__
--
-- Here is a small example that takes a haskell list and inserts it
-- into a table @blerg@ which has columns @x@, @y@, and @z@ of type
-- @int8@, @boolean@, and @text@ respectively.
--
-- @
-- toTableExample :: [(Int64, Bool, Text)] -> Statement () ()
-- toTableExample rowsToInsert =
-- interp [sql| insert into blerg (x, y, z) select * from ^{toTable rowsToInsert} |]
-- @
--
-- This is driven by the 'EncodeRow' type class that has a
-- default implementation for product types that are an instance of
-- 'Generic'. So the following also works:
--
-- @
-- data Blerg
-- = Blerg Int64 Bool Text
-- deriving stock (Generic)
-- deriving anyclass (EncodeRow)
--
-- toTableExample :: [Blerg] -> Statement () ()
-- toTableExample blergs =
-- interp [sql| insert into blerg (x, y, z) select * from ^{toTable blergs} |]
-- @
toTable :: EncodeRow a => [a] -> Sql
toTable xs = unzipWithEncoder \cons nil enc i ->
let unzippedEncoder = foldr cons nil xs >$ enc
queryString = getAp $ pure "unnest(" <> (mconcat . intersperse ", " <$> Ap (replicateM i addParam)) <> pure ")"
in Sql queryString unzippedEncoder
{-# INLINE toTable #-}
instance GEncodeRow x => GEncodeRow (M1 t i x) where
gUnzipWithEncoder k = gUnzipWithEncoder \cons nil enc i ->
k (\(M1 a) -> cons a) nil enc i
{-# INLINE gUnzipWithEncoder #-}
instance (GEncodeRow a, GEncodeRow b) => GEncodeRow (a :*: b) where
gUnzipWithEncoder k = gUnzipWithEncoder \consa nila enca ia -> gUnzipWithEncoder \consb nilb encb ib ->
k
( \(a :*: b) ~(as, bs) ->
(consa a as, consb b bs)
)
(nila, nilb)
(contramap fst enca <> contramap snd encb)
(ia + ib)
{-# INLINE gUnzipWithEncoder #-}
instance EncodeField a => GEncodeRow (K1 i a) where
gUnzipWithEncoder k =
k (\(K1 a) b -> a : b) [] (E.param (E.nonNullable (E.foldableArray encodeField))) 1
{-# INLINE gUnzipWithEncoder #-}
$(traverse genEncodeRowInstance [2 .. 16])