packages feed

hasql-check-0.5.0.0: src/Hasql/Check/Inferred.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE UndecidableInstances #-}
-- 'statement' and 'resultless' name their Postgres types through equalities
-- they do not need themselves: with them, GHC works the names out where a
-- statement is used, which is what puts them in the .hie files.
{-# OPTIONS_GHC -Wno-redundant-constraints #-}

-- |
-- Module      : Hasql.Check.Inferred
-- Copyright   : (c) 2026 Kronor.io
--
-- License     : BSD-3-Clause
-- Maintainer  : lorenzo@kronor.io
-- Stability   : experimental
-- Portability : GHC
--
-- Statements whose encoders and decoders are chosen by the Haskell types they
-- are used with, for the quoters in "Hasql.Check.Inferred.TH".
--
-- Each Haskell type stands for one Postgres type, named at the type level by
-- 'PgType', and the statement sends the annotations hasql-th would ask for:
-- @select email from customer@ used as a @Statement Int64 Text@ is sent as
-- @select (email)::text from customer@. A @varchar@ column is then read as
-- 'Text', and an @int4@ one as 'Int64', where an exact decoder would fail.
--
-- Because the names are types, GHC works them out where a statement is used,
-- and records them in its @.hie@ files, where @hasql-check-hie@ reads them:
-- a @newtype@ deriving 'Value' is checked as the type it wraps, with no list
-- of types to keep in step.
module Hasql.Check.Inferred (
    -- * Types
    Value (..),
    typeName,
    Domain (..),
    PgEnum (..),
    GEnum (..),
    qualifiedName,
    Column (..),
    Params (..),
    Row (..),

    -- * Postgres types at the type level
    PgColumnType,
    PgColumnLabels,
    PgParamLabels,
    PgColumnLabelLists,
    PostgresParamLabels,
    PostgresColumnLabels,
    PgParamTypes,
    PgColumnTypes,
    KnownSymbols (..),
    PostgresParams,
    PostgresColumns,

    -- * Statements
    Template (..),
    Wrapped (..),
    Piece (..),
    statement,
    resultless,
    render,
) where

import Data.Aeson qualified as Aeson
import Data.ByteString (ByteString)
import Data.Foldable (toList)
import Data.Functor.Contravariant ((>$<))
import Data.Int (Int16, Int32, Int64)
import Data.Kind (Type)
import Data.Proxy (Proxy (..))
import Data.Scientific (Scientific)
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Time (Day, LocalTime, TimeOfDay, UTCTime)
import Data.UUID (UUID)
import Data.Vector (Vector)
import GHC.Generics (C1, D1, Generic (..), M1 (..), Meta (..), U1 (..), (:+:) (..))
import GHC.TypeLits (AppendSymbol, KnownSymbol, Symbol, symbolVal)
import Hasql.Decoders qualified as D
import Hasql.Encoders qualified as E
import Hasql.Statement (Statement)
import Hasql.Statement qualified as Statement
import Language.Haskell.TH.Syntax (Lift)
import PostgresqlSyntax qualified as Ast
import Prelude


-- | A Haskell type, and the Postgres type it is sent and read as.
--
-- A @newtype@ can derive the instance of the type it wraps:
--
-- > newtype Email = Email Text
-- >     deriving newtype (Value)
class Value a where
    -- | The Postgres type's name, as an annotation would give it: @"int8"@,
    -- @"public.payment_state"@.
    type PgType a :: Symbol


    -- | The labels of the Postgres enum it is, if it is one, for the checker.
    type PgLabels a :: [Symbol]


    type PgLabels a = '[]


    encodeValue :: E.Value a
    decodeValue :: D.Value a


-- | The name of a type's Postgres type.
typeName :: forall a. KnownSymbol (PgType a) => Text
typeName = Text.pack (symbolVal (Proxy @(PgType a)))


instance Value Bool where
    type PgType Bool = "bool"
    encodeValue = E.bool
    decodeValue = D.bool


instance Value Int16 where
    type PgType Int16 = "int2"
    encodeValue = E.int2
    decodeValue = D.int2


instance Value Int32 where
    type PgType Int32 = "int4"
    encodeValue = E.int4
    decodeValue = D.int4


instance Value Int64 where
    type PgType Int64 = "int8"
    encodeValue = E.int8
    decodeValue = D.int8


instance Value Float where
    type PgType Float = "float4"
    encodeValue = E.float4
    decodeValue = D.float4


instance Value Double where
    type PgType Double = "float8"
    encodeValue = E.float8
    decodeValue = D.float8


instance Value Scientific where
    type PgType Scientific = "numeric"
    encodeValue = E.numeric
    decodeValue = D.numeric


instance Value Text where
    type PgType Text = "text"
    encodeValue = E.text
    decodeValue = D.text


instance Value ByteString where
    type PgType ByteString = "bytea"
    encodeValue = E.bytea
    decodeValue = D.bytea


instance Value UUID where
    type PgType UUID = "uuid"
    encodeValue = E.uuid
    decodeValue = D.uuid


instance Value Day where
    type PgType Day = "date"
    encodeValue = E.date
    decodeValue = D.date


instance Value LocalTime where
    type PgType LocalTime = "timestamp"
    encodeValue = E.timestamp
    decodeValue = D.timestamp


instance Value UTCTime where
    type PgType UTCTime = "timestamptz"
    encodeValue = E.timestamptz
    decodeValue = D.timestamptz


instance Value TimeOfDay where
    type PgType TimeOfDay = "time"
    encodeValue = E.time
    decodeValue = D.time


instance Value Aeson.Value where
    type PgType Aeson.Value = "jsonb"
    encodeValue = E.jsonb
    decodeValue = D.jsonb


-- | A type sent and read as the one it wraps, under the name of a Postgres
-- domain over it, so that Postgres checks the domain's constraints:
--
-- > newtype Cents = Cents Int64
-- >     deriving (Value) via Domain "cents" Int64
newtype Domain (name :: Symbol) a = Domain a


instance Value a => Value (Domain name a) where
    type PgType (Domain name a) = name
    type PgLabels (Domain name a) = PgLabels a
    encodeValue = (\(Domain a) -> a) >$< encodeValue
    decodeValue = Domain <$> decodeValue


-- | A Haskell enumeration sent and read as a Postgres enum, by the enum's
-- name: each constructor, which takes no fields, as the label of the same
-- name.
--
-- > data PaymentState = PENDING | PAID
-- >     deriving stock (Generic)
-- >     deriving (Value) via PgEnum "payment_state" PaymentState
--
-- The checker compares the labels with the enum's in the database.
newtype PgEnum (name :: Symbol) a = PgEnum a


instance (KnownSymbol name, Generic a, GEnum (Rep a)) => Value (PgEnum name a) where
    type PgType (PgEnum name a) = name
    type PgLabels (PgEnum name a) = ConstructorNames (Rep a)
    encodeValue = uncurry E.enum (qualifiedName (Text.pack (symbolVal (Proxy @name)))) (\(PgEnum a) -> glabel (from a))
    decodeValue = uncurry D.enum (qualifiedName (Text.pack (symbolVal (Proxy @name)))) (\t -> PgEnum . to <$> lookup t gvalues)


-- | A type's schema, if its name has one, and its own name, as the
-- identifiers they are: the name is SQL, as a cast takes it, so a quoted
-- part is taken as it is and an unquoted one in lower case, as Postgres
-- takes them.
qualifiedName :: Text -> (Maybe Text, Text)
qualifiedName n = case Ast.parse mempty n of
    Right (Ast.Typename _ (Ast.GenericTypeSimpleTypename (Ast.GenericType i attrs _)) _ _) ->
        -- The last part is the type, and the one before it its schema; a
        -- part before that is the database, which must be the one connected
        -- to.
        case reverse (i : maybe [] (\(Ast.Attrs as) -> toList as) attrs) of
            own : schema : _ -> (Just (ident schema), ident own)
            own : _ -> (Nothing, ident own)
            [] -> (Nothing, n)
    _ -> (Nothing, n)
  where
    ident = \case
        Ast.QuotedIdent t -> t
        Ast.UnquotedIdent t -> t


-- | The constructors of an enumeration, by name.
class GEnum f where
    gvalues :: [(Text, f p)]
    glabel :: f p -> Text


instance GEnum f => GEnum (D1 m f) where
    gvalues = [(l, M1 x) | (l, x) <- gvalues]
    glabel (M1 x) = glabel x


instance (GEnum f, GEnum g) => GEnum (f :+: g) where
    gvalues = [(l, L1 x) | (l, x) <- gvalues] <> [(l, R1 x) | (l, x) <- gvalues]
    glabel = \case
        L1 x -> glabel x
        R1 x -> glabel x


instance KnownSymbol label => GEnum (C1 ('MetaCons label fixity strict) U1) where
    gvalues = [(Text.pack (symbolVal (Proxy @label)), M1 U1)]
    glabel _ = Text.pack (symbolVal (Proxy @label))


-- | An enumeration's constructor names.
type family ConstructorNames (f :: Type -> Type) :: [Symbol] where
    ConstructorNames (D1 m f) = ConstructorNames f
    ConstructorNames (f :+: g) = Append (ConstructorNames f) (ConstructorNames g)
    ConstructorNames (C1 ('MetaCons label fixity strict) U1) = '[label]


type family Append (xs :: [k]) (ys :: [k]) :: [k] where
    Append '[] ys = ys
    Append (x ': xs) ys = x ': Append xs ys


-- | A one-dimensional array, whose elements may be NULL when they are
-- 'Maybe's: a @Vector Int64@ is an @int8[]@.
instance Column a => Value (Vector a) where
    type PgType (Vector a) = AppendSymbol (PgColumnType a) "[]"
    type PgLabels (Vector a) = PgColumnLabels a
    encodeValue = E.foldableArray encodeColumn
    decodeValue = D.vectorArray decodeColumn


-- | A one-dimensional array, as a list. A 'String' is not one: 'Char' has no
-- Postgres type; use 'Text'.
instance Column a => Value [a] where
    type PgType [a] = AppendSymbol (PgColumnType a) "[]"
    type PgLabels [a] = PgColumnLabels a
    encodeValue = E.foldableArray encodeColumn
    decodeValue = D.listArray decodeColumn


-- | A column or parameter, which may be NULL when it is a 'Maybe'.
class Column a where
    encodeColumn :: E.NullableOrNot E.Value a
    decodeColumn :: D.NullableOrNot D.Value a


instance {-# OVERLAPPABLE #-} Value a => Column a where
    encodeColumn = E.nonNullable encodeValue
    decodeColumn = D.nonNullable decodeValue


instance Value a => Column (Maybe a) where
    encodeColumn = E.nullable encodeValue
    decodeColumn = D.nullable decodeValue


-- | A column's enum labels, if it is one.
type family PgColumnLabels (a :: Type) :: [Symbol] where
    PgColumnLabels (Maybe a) = PgLabels a
    PgColumnLabels a = PgLabels a


-- | A column's Postgres type.
type family PgColumnType (a :: Type) :: Symbol where
    PgColumnType (Maybe a) = PgType a
    PgColumnType a = PgType a


-- | A statement's parameters: none, one, or a tuple of them.
class Params a where
    params :: E.Params a


instance Params () where
    params = E.noParams


instance {-# OVERLAPPABLE #-} Column a => Params a where
    params = E.param encodeColumn


instance (Column a, Column b) => Params (a, b) where
    params =
        (fst >$< E.param encodeColumn)
            <> (snd >$< E.param encodeColumn)


instance (Column a, Column b, Column c) => Params (a, b, c) where
    params =
        ((\(x, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, x, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, x) -> x) >$< E.param encodeColumn)


instance (Column a, Column b, Column c, Column d) => Params (a, b, c, d) where
    params =
        ((\(x, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, x, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, x, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, x) -> x) >$< E.param encodeColumn)


instance (Column a, Column b, Column c, Column d, Column e) => Params (a, b, c, d, e) where
    params =
        ((\(x, _, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, x, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, x, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, x, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, _, x) -> x) >$< E.param encodeColumn)


instance (Column a, Column b, Column c, Column d, Column e, Column f) => Params (a, b, c, d, e, f) where
    params =
        ((\(x, _, _, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, x, _, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, x, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, x, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, _, x, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, _, _, x) -> x) >$< E.param encodeColumn)


instance (Column a, Column b, Column c, Column d, Column e, Column f, Column g) => Params (a, b, c, d, e, f, g) where
    params =
        ((\(x, _, _, _, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, x, _, _, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, x, _, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, x, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, _, x, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, _, _, x, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, _, _, _, x) -> x) >$< E.param encodeColumn)


instance (Column a, Column b, Column c, Column d, Column e, Column f, Column g, Column h) => Params (a, b, c, d, e, f, g, h) where
    params =
        ((\(x, _, _, _, _, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, x, _, _, _, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, x, _, _, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, x, _, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, _, x, _, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, _, _, x, _, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, _, _, _, x, _) -> x) >$< E.param encodeColumn)
            <> ((\(_, _, _, _, _, _, _, x) -> x) >$< E.param encodeColumn)


-- | The Postgres types of a statement's parameters.
type family PgParamTypes (p :: Type) :: [Symbol] where
    PgParamTypes () = '[]
    PgParamTypes (a, b) = '[PgColumnType a, PgColumnType b]
    PgParamTypes (a, b, c) = '[PgColumnType a, PgColumnType b, PgColumnType c]
    PgParamTypes (a, b, c, d) = '[PgColumnType a, PgColumnType b, PgColumnType c, PgColumnType d]
    PgParamTypes (a, b, c, d, e) = '[PgColumnType a, PgColumnType b, PgColumnType c, PgColumnType d, PgColumnType e]
    PgParamTypes (a, b, c, d, e, f) = '[PgColumnType a, PgColumnType b, PgColumnType c, PgColumnType d, PgColumnType e, PgColumnType f]
    PgParamTypes (a, b, c, d, e, f, g) = '[PgColumnType a, PgColumnType b, PgColumnType c, PgColumnType d, PgColumnType e, PgColumnType f, PgColumnType g]
    PgParamTypes (a, b, c, d, e, f, g, h) = '[PgColumnType a, PgColumnType b, PgColumnType c, PgColumnType d, PgColumnType e, PgColumnType f, PgColumnType g, PgColumnType h]
    PgParamTypes a = '[PgColumnType a]


-- | The enum labels of a statement's parameters, one list each.
type family PgParamLabels (p :: Type) :: [[Symbol]] where
    PgParamLabels () = '[]
    PgParamLabels (a, b) = '[PgColumnLabels a, PgColumnLabels b]
    PgParamLabels (a, b, c) = '[PgColumnLabels a, PgColumnLabels b, PgColumnLabels c]
    PgParamLabels (a, b, c, d) = '[PgColumnLabels a, PgColumnLabels b, PgColumnLabels c, PgColumnLabels d]
    PgParamLabels (a, b, c, d, e) = '[PgColumnLabels a, PgColumnLabels b, PgColumnLabels c, PgColumnLabels d, PgColumnLabels e]
    PgParamLabels (a, b, c, d, e, f) = '[PgColumnLabels a, PgColumnLabels b, PgColumnLabels c, PgColumnLabels d, PgColumnLabels e, PgColumnLabels f]
    PgParamLabels (a, b, c, d, e, f, g) = '[PgColumnLabels a, PgColumnLabels b, PgColumnLabels c, PgColumnLabels d, PgColumnLabels e, PgColumnLabels f, PgColumnLabels g]
    PgParamLabels (a, b, c, d, e, f, g, h) = '[PgColumnLabels a, PgColumnLabels b, PgColumnLabels c, PgColumnLabels d, PgColumnLabels e, PgColumnLabels f, PgColumnLabels g, PgColumnLabels h]
    PgParamLabels a = '[PgColumnLabels a]


-- | A result row: one column, or a tuple of them.
class Row a where
    row :: D.Row a


instance {-# OVERLAPPABLE #-} Column a => Row a where
    row = D.column decodeColumn


instance (Column a, Column b) => Row (a, b) where
    row = (,) <$> D.column decodeColumn <*> D.column decodeColumn


instance (Column a, Column b, Column c) => Row (a, b, c) where
    row = (,,) <$> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn


instance (Column a, Column b, Column c, Column d) => Row (a, b, c, d) where
    row = (,,,) <$> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn


instance (Column a, Column b, Column c, Column d, Column e) => Row (a, b, c, d, e) where
    row = (,,,,) <$> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn


instance (Column a, Column b, Column c, Column d, Column e, Column f) => Row (a, b, c, d, e, f) where
    row = (,,,,,) <$> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn


instance (Column a, Column b, Column c, Column d, Column e, Column f, Column g) => Row (a, b, c, d, e, f, g) where
    row = (,,,,,,) <$> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn


instance (Column a, Column b, Column c, Column d, Column e, Column f, Column g, Column h) => Row (a, b, c, d, e, f, g, h) where
    row = (,,,,,,,) <$> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn <*> D.column decodeColumn


-- | The Postgres types of a statement's result columns.
type family PgColumnTypes (r :: Type) :: [Symbol] where
    PgColumnTypes (a, b) = '[PgColumnType a, PgColumnType b]
    PgColumnTypes (a, b, c) = '[PgColumnType a, PgColumnType b, PgColumnType c]
    PgColumnTypes (a, b, c, d) = '[PgColumnType a, PgColumnType b, PgColumnType c, PgColumnType d]
    PgColumnTypes (a, b, c, d, e) = '[PgColumnType a, PgColumnType b, PgColumnType c, PgColumnType d, PgColumnType e]
    PgColumnTypes (a, b, c, d, e, f) = '[PgColumnType a, PgColumnType b, PgColumnType c, PgColumnType d, PgColumnType e, PgColumnType f]
    PgColumnTypes (a, b, c, d, e, f, g) = '[PgColumnType a, PgColumnType b, PgColumnType c, PgColumnType d, PgColumnType e, PgColumnType f, PgColumnType g]
    PgColumnTypes (a, b, c, d, e, f, g, h) = '[PgColumnType a, PgColumnType b, PgColumnType c, PgColumnType d, PgColumnType e, PgColumnType f, PgColumnType g, PgColumnType h]
    PgColumnTypes a = '[PgColumnType a]


-- | The enum labels of a statement's result columns, one list each.
type family PgColumnLabelLists (r :: Type) :: [[Symbol]] where
    PgColumnLabelLists (a, b) = '[PgColumnLabels a, PgColumnLabels b]
    PgColumnLabelLists (a, b, c) = '[PgColumnLabels a, PgColumnLabels b, PgColumnLabels c]
    PgColumnLabelLists (a, b, c, d) = '[PgColumnLabels a, PgColumnLabels b, PgColumnLabels c, PgColumnLabels d]
    PgColumnLabelLists (a, b, c, d, e) = '[PgColumnLabels a, PgColumnLabels b, PgColumnLabels c, PgColumnLabels d, PgColumnLabels e]
    PgColumnLabelLists (a, b, c, d, e, f) = '[PgColumnLabels a, PgColumnLabels b, PgColumnLabels c, PgColumnLabels d, PgColumnLabels e, PgColumnLabels f]
    PgColumnLabelLists (a, b, c, d, e, f, g) = '[PgColumnLabels a, PgColumnLabels b, PgColumnLabels c, PgColumnLabels d, PgColumnLabels e, PgColumnLabels f, PgColumnLabels g]
    PgColumnLabelLists (a, b, c, d, e, f, g, h) = '[PgColumnLabels a, PgColumnLabels b, PgColumnLabels c, PgColumnLabels d, PgColumnLabels e, PgColumnLabels f, PgColumnLabels g, PgColumnLabels h]
    PgColumnLabelLists a = '[PgColumnLabels a]


-- | Type-level names, as text.
class KnownSymbols (ss :: [Symbol]) where
    symbolTexts :: [Text]


instance KnownSymbols '[] where
    symbolTexts = []


instance (KnownSymbol s, KnownSymbols ss) => KnownSymbols (s ': ss) where
    symbolTexts = Text.pack (symbolVal (Proxy @s)) : symbolTexts @ss


-- | The Postgres types of a statement's parameters, as GHC worked them out
-- where it is used. A class of its own, rather than 'KnownSymbols' itself,
-- so that @hasql-check-hie@ finds the whole list in a @.hie@ file, and not
-- only the tails 'KnownSymbols' is built from.
class KnownSymbols ps => PostgresParams ps


instance KnownSymbols ps => PostgresParams ps


-- | The Postgres types of a statement's result columns; see 'PostgresParams'.
class KnownSymbols cs => PostgresColumns cs


instance KnownSymbols cs => PostgresColumns cs


-- | The enum labels of a statement's parameters, as GHC worked them out where
-- it is used, for @hasql-check-hie@ to find; see 'PostgresParams'.
class PostgresParamLabels (ls :: [[Symbol]])


instance PostgresParamLabels ls


-- | The enum labels of a statement's result columns; see 'PostgresParams'.
class PostgresColumnLabels (ls :: [[Symbol]])


instance PostgresColumnLabels ls


-- | A statement's text, as the quoter cut it.
--
-- Its result columns are cast in a projection around it,
--
-- > WITH <its own WITH list,> r (c1, c2) AS (<the statement>) SELECT c1 :: int8, c2 :: text FROM r
--
-- rather than where they are selected, so that the statement means what it
-- did: a cast column is another expression to DISTINCT ON, ORDER BY and
-- GROUP BY, and no longer the primary key other columns depend on. The
-- statement's own WITH list goes first, since one with a data-modifying
-- statement must stay at the top.
data Template = Template
    { whole :: [Piece]
    -- ^ The statement, for one returning no rows.
    , wrapped :: Maybe Wrapped
    -- ^ Its parts, unless it cannot be put in a WITH list: a CALL.
    }
    deriving stock (Eq, Show, Lift)


data Wrapped = Wrapped
    { with :: Maybe [Piece]
    -- ^ Its own WITH list, with the WITH.
    , body :: [Piece]
    -- ^ The rest of it.
    , name :: Text
    -- ^ A name for it that it does not use itself.
    }
    deriving stock (Eq, Show, Lift)


-- | Some SQL, or the place of a parameter's annotation.
data Piece
    = Sql Text
    | -- | The annotation of the nth parameter, counting from 1.
      ParamType Int
    deriving stock (Eq, Show, Lift)


-- | The statement's text, given the Postgres types of its parameters and
-- result columns.
render :: [Text] -> [Text] -> Template -> Text
render paramTypes' columnTypes' t = case (columnTypes', t.wrapped) of
    (_ : _, Just w) ->
        mconcat
            [ maybe "WITH " (\with -> fill with <> ", ") w.with
            , w.name
            , " ("
            , Text.intercalate ", " columns
            , ") AS ("
            , fill w.body
            , ") SELECT "
            , Text.intercalate ", " (zipWith (\c ty -> c <> " :: " <> ty) columns columnTypes')
            , " FROM "
            , w.name
            ]
    _ -> fill t.whole
  where
    columns = ["c" <> Text.pack (show i) | i <- [1 .. length columnTypes']]
    fill = foldMap \case
        Sql sql -> sql
        ParamType n -> case drop (n - 1) paramTypes' of
            ty : _ | n >= 1 -> " :: " <> ty
            _ -> ""


-- | A statement returning rows, given what to make of them.
statement ::
    forall p r a ps cs pl cl.
    ( Params p
    , Row r
    , ps ~ PgParamTypes p
    , cs ~ PgColumnTypes r
    , PostgresParams ps
    , PostgresColumns cs
    , pl ~ PgParamLabels p
    , cl ~ PgColumnLabelLists r
    , PostgresParamLabels pl
    , PostgresColumnLabels cl
    ) =>
    Template ->
    (D.Row r -> D.Result a) ->
    Statement p a
statement template result =
    Statement.preparable (render (symbolTexts @ps) (symbolTexts @cs) template) params (result row)


-- | A statement returning no rows.
resultless ::
    forall p ps pl.
    (Params p, ps ~ PgParamTypes p, PostgresParams ps, pl ~ PgParamLabels p, PostgresParamLabels pl) =>
    Template ->
    Statement p ()
resultless template = Statement.preparable (render (symbolTexts @ps) [] template) params D.noResult