squeal-postgresql-0.9.2.0: src/Squeal/PostgreSQL/Expression/Parameter.hs
{-|
Module: Squeal.PostgreSQL.Expression.Parameter
Description: out-of-line parameters
Copyright: (c) Eitan Chatav, 2019
Maintainer: eitan@morphism.tech
Stability: experimental
out-of-line parameters
-}
{-# LANGUAGE
AllowAmbiguousTypes
, DataKinds
, FlexibleContexts
, FlexibleInstances
, FunctionalDependencies
, GADTs
, KindSignatures
, MultiParamTypeClasses
, OverloadedStrings
, RankNTypes
, ScopedTypeVariables
, TypeApplications
, TypeFamilies
, TypeOperators
, UndecidableInstances
#-}
module Squeal.PostgreSQL.Expression.Parameter
( -- * Parameter
HasParameter (parameter)
, param
-- * Parameter Internals
, HasParameter'
, ParamOutOfBoundsError
, ParamTypeMismatchError
) where
import Data.Kind (Constraint)
import GHC.Exts (Any)
import GHC.TypeLits
import Squeal.PostgreSQL.Expression
import Squeal.PostgreSQL.Expression.Type
import Squeal.PostgreSQL.Render
import Squeal.PostgreSQL.Type.Schema
-- $setup
-- >>> import Squeal.PostgreSQL
{- | A `HasParameter` constraint is used to indicate a value that is
supplied externally to a SQL statement.
`Squeal.PostgreSQL.Session.manipulateParams`,
`Squeal.PostgreSQL.Session.queryParams` and
`Squeal.PostgreSQL.Session.traversePrepared` support specifying data values
separately from the SQL command string, in which case `param`s are used to
refer to the out-of-line data values.
-}
class KnownNat ix => HasParameter
(ix :: Nat)
(params :: [NullType])
(ty :: NullType)
| ix params -> ty where
-- | `parameter` takes a `Nat` using type application and a `TypeExpression`.
--
-- >>> printSQL (parameter @1 int4)
-- ($1 :: int4)
parameter
:: TypeExpression db ty
-> Expression grp lat with db params from ty
parameter ty = UnsafeExpression $ parenthesized $
"$" <> renderNat @ix <+> "::"
<+> renderSQL ty
-- we could do the check for 0 in @HasParameter'@, but this way forces checking 'ix' before delegating,
-- which has the nice effect of ambiguous 'ix' errors mentioning 'HasParameter' instead of @HasParameter'@
instance {-# OVERLAPS #-} (TypeError ('Text "Tried to get the param at index 0, but params are 1-indexed"), x ~ Any) => HasParameter 0 params x
instance {-# OVERLAPS #-} (KnownNat ix, HasParameter' ix params ix params x) => HasParameter ix params x
-- | @HasParameter'@ is an implementation detail of 'HasParameter' allowing us to
-- include the full parameter list in our errors.
class KnownNat ix => HasParameter'
(originalIx :: Nat)
(allParams :: [NullType])
(ix :: Nat)
(params :: [NullType])
(ty :: NullType)
| ix params -> ty where
instance {-# OVERLAPS #-}
( params ~ (y ': xs)
, y ~ x -- having a separate 'y' type variable is required for 'ParamTypeMismatchError'
, ParamOutOfBoundsError originalIx allParams params
, ParamTypeMismatchError originalIx allParams x y
) => HasParameter' originalIx allParams 1 params x
instance {-# OVERLAPS #-}
( KnownNat ix
, HasParameter' originalIx allParams (ix-1) xs x
, params ~ (y ': xs)
, ParamOutOfBoundsError originalIx allParams params
)
=> HasParameter' originalIx allParams ix params x
-- | @ParamOutOfBoundsError@ reports a nicer error with more context when we try to do an out-of-bounds lookup successfully do a lookup but
-- find a different field than we expected, or when we find ourself out of bounds
type family ParamOutOfBoundsError (originalIx :: Nat) (allParams :: [NullType]) (params :: [NullType]) :: Constraint where
ParamOutOfBoundsError originalIx allParams '[] = TypeError
('Text "Index " ':<>: 'ShowType originalIx ':<>: 'Text " is out of bounds in 1-indexed parameter list:" ':$$: 'ShowType allParams)
ParamOutOfBoundsError _ _ _ = ()
-- | @ParamTypeMismatchError@ reports a nicer error with more context when we successfully do a lookup but
-- find a different field than we expected, or when we find ourself out of bounds
type family ParamTypeMismatchError (originalIx :: Nat) (allParams :: [NullType]) (found :: NullType) (expected :: NullType) :: Constraint where
ParamTypeMismatchError _ _ found found = ()
ParamTypeMismatchError originalIx allParams found expected = TypeError
( 'Text "Type mismatch when looking up param at index " ':<>: 'ShowType originalIx
':$$: 'Text "in 1-indexed parameter list:"
':$$: 'Text " " ':<>: 'ShowType allParams
':$$: 'Text ""
':$$: 'Text "Expected: " ':<>: 'ShowType expected
':$$: 'Text "But found: " ':<>: 'ShowType found
':$$: 'Text ""
)
-- | `param` takes a `Nat` using type application and for basic types,
-- infers a `TypeExpression`.
--
-- >>> printSQL (param @1 @('Null 'PGint4))
-- ($1 :: int4)
param
:: forall n ty lat with db params from grp
. (NullTyped db ty, HasParameter n params ty)
=> Expression grp lat with db params from ty -- ^ param
param = parameter @n (nulltype @db)