packages feed

rel8-1.7.0.0: src/Rel8/Statement/Prepared.hs

{-# language AllowAmbiguousTypes #-}
{-# language BlockArguments #-}
{-# language FlexibleContexts #-}
{-# language MonoLocalBinds #-}
{-# language NamedFieldPuns #-}
{-# language ScopedTypeVariables #-}
{-# language TypeApplications #-}

module Rel8.Statement.Prepared (
  input,
  prepared,
) where

-- base
import Data.Functor.Const (Const (Const), getConst)
import Data.Functor.Contravariant (contramap, (>$<))
import Data.Functor.Identity (runIdentity)
import Prelude

-- hasql
import qualified Hasql.Encoders as Hasql
import qualified Hasql.Statement as Hasql

-- opaleye
import qualified Opaleye.Internal.HaskellDB.PrimQuery as Opaleye

-- rel8
import Rel8.Expr (Expr)
import Rel8.Expr.Opaleye (fromPrimExpr, scastExpr)
import Rel8.Schema.HTable (hfield, hspecs, htabulateA)
import Rel8.Schema.Null (Nullity (Null, NotNull))
import Rel8.Schema.Spec (Spec (..))
import Rel8.Statement (Statement)
import Rel8.Table (Table, fromColumns, toResult)
import Rel8.Table.Serialize (Serializable)
import Rel8.Type.Encoder (binary)
import Rel8.Type.Information (encode)

-- transformers
import Control.Monad.Trans.State.Strict (evalState, state)


{-| Given a 'Rel8.run' function that converts a 'Statement' to a
'Hasql.Statement', return a 'Rel8.run'-like function which instead takes a
/parameterized/ 'Statement' and converts it to a /preparable/
'Hasql.Statement'.

The parameters @i@ are sent to the database directly via PostgreSQL's binary
format. For large amounts of data this can be significantly more efficient
than embedding the values in the statement with 'Rel8.lit'.
-}
prepared :: forall a b i o.
  Serializable a i =>
  (Statement b -> Hasql.Statement () o) ->
  (a -> Statement b) ->
  Hasql.Statement i o
prepared run mkStatement = Hasql.Statement sql (encoder @a) decode True
  where
    Hasql.Statement sql _ decode _ = run $ mkStatement input


encoder :: forall a i. Serializable a i => Hasql.Params i
encoder =
  contramap (toResult @_ @a) $
    getConst $
      htabulateA \field ->
        case hfield hspecs field of
          Spec {nullity, info} -> Const $
            runIdentity . (`hfield` field) >$<
              case nullity of
                Null -> Hasql.param $ Hasql.nullable build
                NotNull -> Hasql.param $ Hasql.nonNullable build
              where
                build = binary (encode info)


input :: Table Expr a => a
input =
  fromColumns $
    flip (evalState @Word) 1 do
      htabulateA \field -> do
        n <- state (\n -> (n, n + 1))
        pure
          case hfield hspecs field of
            Spec {info} ->
              scastExpr info $ fromPrimExpr $
                Opaleye.ConstExpr $ Opaleye.OtherLit $ '$' : show n