hasql-check-0.5.0.0: src/Hasql/Check/Annotate.hs
{-# LANGUAGE CPP #-}
-- |
-- Module : Hasql.Check.Annotate
-- Copyright : (c) 2026 Kronor.io
--
-- License : BSD-3-Clause
-- Maintainer : lorenzo@kronor.io
-- Stability : experimental
-- Portability : GHC
--
-- Annotate the parameters of a statement that has no annotations, as
-- hasql-th asks for.
--
-- 'Hasql.Check.Inferred.TH' annotates with the types of the Haskell values a
-- statement is used with, and the checker annotates the same way, so that it
-- checks the statement the quoter sends. Result columns are not annotated
-- here: the quoter casts them around the statement, where they cannot change
-- what it means.
module Hasql.Check.Annotate (
annotate,
typename,
placeholder,
untypedLiterals,
) where
import Data.Data (Data, cast, gmapT)
import Data.Foldable (toList)
import Data.Text (Text)
import Data.Text qualified as Text
import PostgresqlSyntax qualified as Ast
import Prelude
-- | A type, as an annotation would name it - @int8@, @public.payment_state@,
-- @text[]@ - nullable or not. An array's nullability is its elements'.
typename :: Text -> Bool -> Either String Ast.Typename
typename name nullable = case Ast.parse mempty name of
Right (Ast.Typename setof t _ Nothing) -> Right (Ast.Typename setof t nullable Nothing)
Right (Ast.Typename setof t _ (Just (dimensions, _))) -> Right (Ast.Typename setof t False (Just (dimensions, nullable)))
Left _ -> Left ("not a Postgres type name: " <> Text.unpack name)
-- | A made-up type with a plain name, to find in the rendered statement.
placeholder :: Text -> Ast.Typename
placeholder name =
Ast.Typename False (Ast.GenericTypeSimpleTypename (Ast.GenericType (Ast.UnquotedIdent name) Nothing Nothing)) False Nothing
-- | The statement with each occurrence of the @n@th parameter, counting from
-- 1, cast to the type given for it, if one is. A parameter that is
-- subscripted or has a field taken, @$1['key']@, is cast before that:
-- @($1 :: jsonb)['key']@.
annotate :: (Int -> Maybe Ast.Typename) -> Ast.PreparableStmt -> Ast.PreparableStmt
annotate param = go
where
go :: Data b => b -> b
go x = case cast x of
Just (Ast.CExprAExpr p@(Ast.ParamCExpr n Nothing)) | Just t <- param n, Just r <- cast (Ast.TypecastAExpr (Ast.CExprAExpr p) t) -> r
_ -> case cast x of
Just (Ast.CExprBExpr p@(Ast.ParamCExpr n Nothing)) | Just t <- param n, Just r <- cast (Ast.TypecastBExpr (Ast.CExprBExpr p) t) -> r
_ -> case cast x of
Just (Ast.ParamCExpr n (Just indirection))
| Just t <- param n
, Just r <- cast (Ast.InParensCExpr (Ast.TypecastAExpr (Ast.CExprAExpr (Ast.ParamCExpr n Nothing)) t) (Just indirection)) ->
r
_ -> gmapT go x
-- | The result columns, counting from 1, that are a string literal or NULL
-- with no type of its own, and the literal: Postgres reads one as the type
-- it is cast to, so whether the quoter's cast works depends only on the
-- literal. Only a plain SELECT's columns are found.
untypedLiterals :: Ast.PreparableStmt -> [(Int, Text)]
untypedLiterals = \case
Ast.SelectPreparableStmt (Ast.NoParensSelectStmt (Ast.SelectNoParens _ (Ast.SimpleSelectSelectClause (Ast.NormalSimpleSelect (Just t) _ _ _ _ _ _)) _ _ _)) ->
[(n, Ast.toText mempty e) | (n, Just e) <- zip [1 ..] (targeting t), untyped e]
_ -> []
where
targeting = \case
Ast.NormalTargeting l -> normalTargets l
Ast.AllTargeting (Just l) -> targets l
Ast.DistinctTargeting _ l -> targets l
Ast.AllTargeting Nothing -> []
untyped = \case
Ast.CExprAExpr (Ast.AexprConstCExpr (Ast.SAexprConst _)) -> True
Ast.CExprAExpr (Ast.AexprConstCExpr Ast.NullAexprConst) -> True
_ -> False
-- | Each target's expression; nothing for a @*@, which stops the counting.
targets :: Ast.TargetList -> [Maybe Ast.AExpr]
targets (Ast.TargetList els) = takeWhile (/= Nothing) (map expr (toList els))
where
expr = \case
Ast.AliasedExprTargetEl a _ -> Just a
Ast.ImplicitlyAliasedExprTargetEl a _ -> Just a
Ast.ExprTargetEl a -> Just a
Ast.AsteriskTargetEl -> Nothing
#ifdef KRONOR_SYNTAX
normalTargets :: Ast.HsTargetList -> [Maybe Ast.AExpr]
normalTargets (Ast.HsTargetList els) = takeWhile (/= Nothing) (map one (toList els))
where
one = \case
Ast.SqlTargetEl te -> case te of
Ast.AliasedExprTargetEl a _ -> Just a
Ast.ImplicitlyAliasedExprTargetEl a _ -> Just a
Ast.ExprTargetEl a -> Just a
Ast.AsteriskTargetEl -> Nothing
_ -> Nothing
#else
normalTargets :: Ast.TargetList -> [Maybe Ast.AExpr]
normalTargets = targets
#endif