postgresql-syntax-0.5.0.0: library-internal/PostgresqlSyntax/Ast/AExpr.hs
module PostgresqlSyntax.Ast.AExpr
( AExpr (..),
filteredParser,
isBoundedAExprOperand,
safeAExprOperand,
selectWithParensAExpr,
refineToSelectWithParens,
canonicalize,
)
where
import qualified HeadedMegaparsec as Parser
import PostgresqlSyntax.Ast.AExprReversableOp
import PostgresqlSyntax.Ast.AnyName hiding (filteredParser)
import PostgresqlSyntax.Ast.CExpr (CExpr)
import qualified PostgresqlSyntax.Ast.CExpr as CExpr
import PostgresqlSyntax.Ast.Ident
import PostgresqlSyntax.Ast.QualOp
import PostgresqlSyntax.Ast.Row
import {-# SOURCE #-} PostgresqlSyntax.Ast.SelectWithParens (SelectWithParens)
import PostgresqlSyntax.Ast.SubType
import PostgresqlSyntax.Ast.SubqueryOp
import PostgresqlSyntax.Ast.SymbolicExprBinOp
import PostgresqlSyntax.Ast.Typename
import PostgresqlSyntax.Ast.VerbalExprBinOp
import qualified PostgresqlSyntax.Helpers.Gens as Gens
import qualified PostgresqlSyntax.Helpers.Parsers as Parsers
import qualified PostgresqlSyntax.Helpers.TextBuilders as TextBuilders
import PostgresqlSyntax.IsAst
import PostgresqlSyntax.Prelude
import PostgresqlSyntax.Settings (Settings)
import qualified Test.QuickCheck as Qc
-- |
-- ==== References
-- @
-- a_expr:
-- | c_expr
-- | a_expr TYPECAST Typename
-- | a_expr COLLATE any_name
-- | a_expr AT TIME ZONE a_expr
-- | '+' a_expr
-- | '-' a_expr
-- | a_expr '+' a_expr
-- | a_expr '-' a_expr
-- | a_expr '*' a_expr
-- | a_expr '/' a_expr
-- | a_expr '%' a_expr
-- | a_expr '^' a_expr
-- | a_expr '<' a_expr
-- | a_expr '>' a_expr
-- | a_expr '=' a_expr
-- | a_expr LESS_EQUALS a_expr
-- | a_expr GREATER_EQUALS a_expr
-- | a_expr NOT_EQUALS a_expr
-- | a_expr qual_Op a_expr
-- | qual_Op a_expr
-- | a_expr AND a_expr
-- | a_expr OR a_expr
-- | NOT a_expr
-- | NOT_LA a_expr
-- | a_expr LIKE a_expr
-- | a_expr LIKE a_expr ESCAPE a_expr
-- | a_expr NOT_LA LIKE a_expr
-- | a_expr NOT_LA LIKE a_expr ESCAPE a_expr
-- | a_expr ILIKE a_expr
-- | a_expr ILIKE a_expr ESCAPE a_expr
-- | a_expr NOT_LA ILIKE a_expr
-- | a_expr NOT_LA ILIKE a_expr ESCAPE a_expr
-- | a_expr SIMILAR TO a_expr
-- | a_expr SIMILAR TO a_expr ESCAPE a_expr
-- | a_expr NOT_LA SIMILAR TO a_expr
-- | a_expr NOT_LA SIMILAR TO a_expr ESCAPE a_expr
-- | a_expr IS NULL_P
-- | a_expr ISNULL
-- | a_expr IS NOT NULL_P
-- | a_expr NOTNULL
-- | row OVERLAPS row
-- | a_expr IS TRUE_P
-- | a_expr IS NOT TRUE_P
-- | a_expr IS FALSE_P
-- | a_expr IS NOT FALSE_P
-- | a_expr IS UNKNOWN
-- | a_expr IS NOT UNKNOWN
-- | a_expr IS DISTINCT FROM a_expr
-- | a_expr IS NOT DISTINCT FROM a_expr
-- | a_expr IS OF '(' type_list ')'
-- | a_expr IS NOT OF '(' type_list ')'
-- | a_expr BETWEEN opt_asymmetric b_expr AND a_expr
-- | a_expr NOT_LA BETWEEN opt_asymmetric b_expr AND a_expr
-- | a_expr BETWEEN SYMMETRIC b_expr AND a_expr
-- | a_expr NOT_LA BETWEEN SYMMETRIC b_expr AND a_expr
-- | a_expr IN_P in_expr
-- | a_expr NOT_LA IN_P in_expr
-- | a_expr subquery_Op sub_type select_with_parens
-- | a_expr subquery_Op sub_type '(' a_expr ')'
-- | UNIQUE select_with_parens
-- | a_expr IS DOCUMENT_P
-- | a_expr IS NOT DOCUMENT_P
-- | DEFAULT
-- @
data AExpr
= CExprAExpr CExpr
| TypecastAExpr AExpr Typename
| CollateAExpr AExpr AnyName
| AtTimeZoneAExpr AExpr AExpr
| PlusAExpr AExpr
| MinusAExpr AExpr
| SymbolicBinOpAExpr AExpr SymbolicExprBinOp AExpr
| PrefixQualOpAExpr QualOp AExpr
| AndAExpr AExpr AExpr
| OrAExpr AExpr AExpr
| NotAExpr AExpr
| VerbalExprBinOpAExpr AExpr Bool VerbalExprBinOp AExpr (Maybe AExpr)
| ReversableOpAExpr AExpr Bool AExprReversableOp
| IsnullAExpr AExpr
| NotnullAExpr AExpr
| OverlapsAExpr Row Row
| SubqueryAExpr AExpr SubqueryOp SubType (Either SelectWithParens AExpr)
| UniqueAExpr SelectWithParens
| DefaultAExpr
deriving (Show, Generic, Eq, Ord, Data)
instance IsAst AExpr where
toTextBuilder settings = \case
CExprAExpr a -> toTextBuilder settings a
TypecastAExpr a b -> renderOperand a <> " :: " <> toTextBuilder settings b
CollateAExpr a b -> renderOperand a <> " COLLATE " <> toTextBuilder settings b
AtTimeZoneAExpr a b -> renderOperand a <> " AT TIME ZONE " <> toTextBuilder settings b
PlusAExpr a -> "+ " <> toTextBuilder settings a
MinusAExpr a -> "- " <> toTextBuilder settings a
SymbolicBinOpAExpr a b c -> renderOperand a <> " " <> toTextBuilder settings b <> " " <> toTextBuilder settings c
PrefixQualOpAExpr a b -> toTextBuilder settings a <> " " <> toTextBuilder settings b
AndAExpr a b -> renderOperand a <> " AND " <> toTextBuilder settings b
OrAExpr a b -> renderOperand a <> " OR " <> toTextBuilder settings b
NotAExpr a -> "NOT " <> toTextBuilder settings a
VerbalExprBinOpAExpr a b c d e -> renderOperand a <> " " <> bool "" "NOT " b <> toTextBuilder settings c <> " " <> renderVerbalRhs e d <> foldMap (mappend " ESCAPE " . toTextBuilder settings) e
ReversableOpAExpr a b c -> renderOperand a <> " " <> renderAExprReversableOp b c
IsnullAExpr a -> renderOperand a <> " ISNULL"
NotnullAExpr a -> renderOperand a <> " NOTNULL"
OverlapsAExpr a b -> toTextBuilder settings a <> " OVERLAPS " <> toTextBuilder settings b
SubqueryAExpr a b c d -> renderOperand a <> " " <> toTextBuilder settings b <> " " <> toTextBuilder settings c <> " " <> either (toTextBuilder settings) (TextBuilders.renderInParens . toTextBuilder settings) d
UniqueAExpr a -> "UNIQUE " <> toTextBuilder settings a
DefaultAExpr -> "DEFAULT"
where
-- Renders an operand sitting in the left\/accumulator position of a
-- suffix production (the @a@ that 'customizedParser'\'s @suffixRec@
-- threads through). Every alternative in @suffix@ parses its
-- right-hand operand via an unrestricted recursive @a_expr@ call, so
-- rendering it plainly always reconstructs the same shape on reparse.
-- The *left* position is different: control only returns to
-- @suffixRec@'s loop after a __bounded__ production — one that
-- doesn't itself end in an unrestricted recursive @a_expr@. A value
-- shaped like 'PlusAExpr'\/'MinusAExpr'\/'NotAExpr'\/
-- 'PrefixQualOpAExpr'\/'AtTimeZoneAExpr'\/'SymbolicBinOpAExpr'\/
-- 'AndAExpr'\/'OrAExpr'\/'VerbalExprBinOpAExpr', or a 'ReversableOpAExpr'
-- whose 'PostgresqlSyntax.Ast.AExprReversableOp' ends in one too, is
-- unbounded — placed there bare, it would greedily re-absorb whatever
-- follows on reparse (e.g. rendering @SymbolicBinOpAExpr (NotAExpr x)
-- op y@ plainly as @NOT x op y@ reparses as @NotAExpr (SymbolicBinOpAExpr
-- x op y)@). Parenthesizing it, via the existing @'(' a_expr ')'@
-- 'PostgresqlSyntax.Ast.CExpr' production, keeps rendering correct for
-- any 'AExpr' value, whether or not the parser itself could have
-- produced it in that position.
renderOperand a
| isBoundedAExprOperand a = toTextBuilder settings a
| otherwise = TextBuilders.renderInParens (toTextBuilder settings a)
-- The @d@ operand of a @LIKE@\/@ILIKE@\/@SIMILAR TO@ production, when
-- it has a trailing @ESCAPE@ clause of its own (@e@). Since @d@ is
-- parsed via an unrestricted recursive @a_expr@, rendering it bare
-- when it's unbounded (see 'isBoundedAExprOperand') — e.g. a dangling
-- operator, or an escape-less 'VerbalExprBinOpAExpr' whose own greedy
-- @optional escape@ check would otherwise swallow the text meant for
-- \*this* production's @ESCAPE@ — reparses differently. Bounded shapes
-- (a column reference, a constant, an already-parenthesized
-- expression, DEFAULT, …) always terminate cleanly and never need the
-- extra parens, exactly like @renderOperand@'s left position.
renderVerbalRhs e d = case e of
Nothing -> toTextBuilder settings d
Just _
| isBoundedAExprOperand d -> toTextBuilder settings d
| otherwise -> TextBuilders.renderInParens (toTextBuilder settings d)
-- Distinct from 'PostgresqlSyntax.Ast.AExprReversableOp'\'s own
-- @toTextBuilder@ (which bakes in the "positive" @IS@\/@BETWEEN@\/@IN@
-- Parsers.keyword but not the negation) — this one threads the external
-- @Bool@ (@NOT@) in, mirroring the pre-extraction top-level
-- @aExprReversableOp@ renderer.
renderAExprReversableOp a = \case
NullAExprReversableOp -> bool "IS " "IS NOT " a <> "NULL"
TrueAExprReversableOp -> bool "IS " "IS NOT " a <> "TRUE"
FalseAExprReversableOp -> bool "IS " "IS NOT " a <> "FALSE"
UnknownAExprReversableOp -> bool "IS " "IS NOT " a <> "UNKNOWN"
DistinctFromAExprReversableOp b -> bool "IS " "IS NOT " a <> "DISTINCT FROM " <> toTextBuilder settings b
OfAExprReversableOp b -> bool "IS " "IS NOT " a <> "OF " <> TextBuilders.renderInParens (toTextBuilder settings b)
BetweenAExprReversableOp b c d -> bool "" "NOT " a <> bool "BETWEEN " "BETWEEN ASYMMETRIC " b <> toTextBuilder settings c <> " AND " <> toTextBuilder settings d
BetweenSymmetricAExprReversableOp b c -> bool "" "NOT " a <> "BETWEEN SYMMETRIC " <> toTextBuilder settings b <> " AND " <> toTextBuilder settings c
InAExprReversableOp b -> bool "" "NOT " a <> "IN " <> toTextBuilder settings b
DocumentAExprReversableOp -> bool "IS " "IS NOT " a <> "DOCUMENT"
parser settings = customizedParser settings (parser settings)
-- |
-- Parameterized over the 'PostgresqlSyntax.Ast.CExpr' parser embedded at the
-- base case — the only axis 'filteredParser' needs to customize (via
-- 'PostgresqlSyntax.Ast.CExpr.customizedParser'). Every other occurrence of
-- @a_expr@\/@b_expr@\/@select_with_parens@ in the grammar below uses the
-- ordinary, unfiltered parsers, exactly as the pre-extraction
-- @customizedAExpr@ did (its @bExpr@\/@selectWithParens@ references were
-- never threaded through the @cExpr@ parameter either).
customizedParser :: Settings -> Parser CExpr -> Parser AExpr
customizedParser settings cExpr = suffixRec base suffix
where
aExpr = customizedParser settings cExpr
base =
asum
[ DefaultAExpr <$ Parsers.keyword "default",
UniqueAExpr <$> (Parsers.keyword "unique" *> Parsers.space1 *> parser settings),
Parsers.qualOpExpr settings aExpr PrefixQualOpAExpr,
PlusAExpr <$> Parsers.plusedExpr aExpr,
MinusAExpr <$> Parsers.minusedExpr aExpr,
NotAExpr <$> (Parsers.keyword "not" *> Parsers.space1 *> aExpr),
CExprAExpr <$> cExpr
]
suffix a =
asum
[ overlapsSuffix settings a,
do
Parsers.space1
b <- Parser.wrapToHead (parser settings)
Parsers.space1
c <- Parser.wrapToHead (parser settings)
Parsers.space
d <- Left <$> Parser.wrapToHead (parser settings) <|> Right <$> Parsers.inParens aExpr
return (SubqueryAExpr a b c d),
Parsers.typecastExpr settings a TypecastAExpr,
CollateAExpr a <$> (Parsers.space1 *> Parsers.keyword "collate" *> Parsers.space1 *> Parser.endHead *> parser settings),
AtTimeZoneAExpr a <$> (Parsers.space1 *> Parsers.keyphrase "at time zone" *> Parsers.space1 *> Parser.endHead *> aExpr),
Parsers.symbolicBinOpExpr settings a aExpr SymbolicBinOpAExpr,
AndAExpr a <$> (Parsers.space1 *> Parsers.keyword "and" *> Parsers.space1 *> Parser.endHead *> aExpr),
OrAExpr a <$> (Parsers.space1 *> Parsers.keyword "or" *> Parsers.space1 *> Parser.endHead *> aExpr),
do
Parsers.space1
b <- Parsers.trueIfPresent (Parsers.keyword "not" *> Parsers.space1)
c <- parser settings
Parsers.space1
Parser.endHead
d <- aExpr
e <- optional (Parsers.space1 *> Parsers.keyword "escape" *> Parsers.space1 *> Parser.endHead *> aExpr)
return (VerbalExprBinOpAExpr a b c d e),
do
Parsers.space1
Parsers.keyword "is"
Parsers.space1
Parser.endHead
b <- Parsers.trueIfPresent (Parsers.keyword "not" *> Parsers.space1)
c <-
asum
[ NullAExprReversableOp <$ Parsers.keyword "null",
TrueAExprReversableOp <$ Parsers.keyword "true",
FalseAExprReversableOp <$ Parsers.keyword "false",
UnknownAExprReversableOp <$ Parsers.keyword "unknown",
DistinctFromAExprReversableOp <$> (Parsers.keyword "distinct" *> Parsers.space1 *> Parsers.keyword "from" *> Parsers.space1 *> Parser.endHead *> aExpr),
OfAExprReversableOp <$> (Parsers.keyword "of" *> Parsers.space1 *> Parser.endHead *> Parsers.inParens (parser settings)),
DocumentAExprReversableOp <$ Parsers.keyword "document"
]
return (ReversableOpAExpr a b c),
do
Parsers.space1
b <- Parsers.trueIfPresent (Parsers.keyword "not" *> Parsers.space1)
Parsers.keyword "between"
Parsers.space1
Parser.endHead
c <-
asum
[ BetweenSymmetricAExprReversableOp <$ (Parsers.keyword "symmetric" *> Parsers.space1),
BetweenAExprReversableOp True <$ (Parsers.keyword "asymmetric" *> Parsers.space1),
pure (BetweenAExprReversableOp False)
]
d <- parser settings
Parsers.space1
Parsers.keyword "and"
Parsers.space1
e <- aExpr
return (ReversableOpAExpr a b (c d e)),
do
Parsers.space1
b <- Parsers.trueIfPresent (Parsers.keyword "not" *> Parsers.space1)
Parsers.keyword "in"
Parsers.space
c <- InAExprReversableOp <$> parser settings
return (ReversableOpAExpr a b c),
IsnullAExpr a <$ (Parsers.space1 *> Parsers.keyword "isnull"),
NotnullAExpr a <$ (Parsers.space1 *> Parsers.keyword "notnull")
]
-- |
-- The @OVERLAPS@ operator, as a suffix of an already parsed left operand.
-- Reinterprets the already-parsed base 'AExpr' as a 'Row' rather than
-- speculatively parsing a @row@ on top of it — see the original
-- @overlapsSuffix@ in the pre-extraction @PostgresqlSyntax.Parsing@ for the
-- exponential-blowup rationale this avoids.
overlapsSuffix :: Settings -> AExpr -> Parser AExpr
overlapsSuffix settings a = do
b <- maybe empty pure (aExprRow a)
Parsers.space1
Parsers.keyword "overlaps"
Parser.endHead
Parsers.space1
c <- parser settings
return (OverlapsAExpr b c)
where
aExprRow = \case
CExprAExpr (CExpr.ExplicitRowCExpr x) -> Just (ExplicitRowRow x)
CExprAExpr (CExpr.ImplicitRowCExpr x) -> Just (ImplicitRowRow x)
_ -> Nothing
-- | 'parser', but rejecting the given words when they'd otherwise be
-- accepted as a trailing bare column-reference identifier. Needed by
-- "PostgresqlSyntax.Ast.SortBy", which must not let @a_expr@ swallow a
-- Parsers.keyword (@USING@\/@ASC@\/@DESC@\/@NULLS@) that is meant to terminate it.
filteredParser :: Settings -> [Text] -> Parser AExpr
filteredParser settings excluded = customizedParser settings (CExpr.customizedParser settings (Parsers.filteredColIdLike UnquotedIdent (parser settings) excluded))
-- |
-- Whether the given 'AExpr' is safe to place in the left\/accumulator
-- position of a suffix production without parenthesizing it — see
-- @renderOperand@ in the 'IsAst' instance above for why that position is
-- special. A shape is bounded when parsing it can never end in an
-- unrestricted recursive @a_expr@ call, i.e. control is guaranteed to
-- return to @suffixRec@'s loop once it's done.
--
-- Note this predicate is purely about operator precedence and
-- associativity: an unbounded shape is one whose own rendering ends in an
-- unrestricted recursive @a_expr@, so placed bare in the left position it
-- would re-absorb the suffix that follows (e.g. rendering
-- @SymbolicBinOpAExpr (NotAExpr x) op y@ plainly as @NOT x op y@ reparses
-- as @NotAExpr (SymbolicBinOpAExpr x op y)@). It is /not/ a general
-- terminator-keyword guard — it isn't a general-purpose mechanism against
-- an expression swallowing a keyword that terminates an enclosing
-- production; the only shape that ever created that hazard by construction
-- was the postfix @a_expr qual_Op@ production, which no longer exists here
-- or in @references/gram.y@ (see gram.y:15985,15987; Postgres removed
-- postfix operators in v14). It does incidentally get reused for that
-- purpose in "PostgresqlSyntax.Ast.TargetEl" (to stop an @OrAExpr@'s right
-- operand from absorbing a following implicit alias) — that's just one
-- call site's use of it, not evidence of general applicability.
isBoundedAExprOperand :: AExpr -> Bool
isBoundedAExprOperand = \case
PlusAExpr {} -> False
MinusAExpr {} -> False
NotAExpr {} -> False
PrefixQualOpAExpr {} -> False
AtTimeZoneAExpr {} -> False
SymbolicBinOpAExpr {} -> False
AndAExpr {} -> False
OrAExpr {} -> False
VerbalExprBinOpAExpr {} -> False
ReversableOpAExpr _ _ c -> case c of
DistinctFromAExprReversableOp {} -> False
BetweenAExprReversableOp {} -> False
BetweenSymmetricAExprReversableOp {} -> False
_ -> True
_ -> True
-- |
-- A generator for the left\/accumulator position of a suffix production
-- (see 'isBoundedAExprOperand'): whatever the given generator produces gets
-- wrapped in an explicit @'(' a_expr ')'@ (via 'CExpr.InParensCExpr') when
-- it wouldn't otherwise be reachable there, so the result always round-trips
-- through 'parser'.
safeAExprOperand :: Qc.Gen AExpr -> Qc.Gen AExpr
safeAExprOperand gen = do
a <- gen
pure $
if isBoundedAExprOperand a
then a
else CExprAExpr (CExpr.InParensCExpr a Nothing)
-- |
-- Smart constructor wrapping a bare 'SelectWithParens' as an 'AExpr', for
-- modules that can only see 'AExpr' via its @hs-boot@ (which keeps it
-- abstract to break an import cycle) — see "PostgresqlSyntax.Ast.InExpr",
-- which needs it to canonicalize a @select_with_parens@\/@expr_list@
-- ambiguity.
selectWithParensAExpr :: SelectWithParens -> AExpr
selectWithParensAExpr a = CExprAExpr (CExpr.SelectWithParensCExpr a Nothing)
-- |
-- If an 'AExpr' is a bare, indirection-less 'PostgresqlSyntax.Ast.CExpr.SelectWithParensCExpr'
-- wrapping, returns the wrapped 'SelectWithParens'. The inverse of
-- 'selectWithParensAExpr', exposed for the same @hs-boot@-abstraction reason
-- — see "PostgresqlSyntax.Ast.CExpr", which needs it to canonicalize an
-- @'(' a_expr ')'@\/@select_with_parens@ ambiguity analogous to the one
-- described there.
refineToSelectWithParens :: AExpr -> Maybe SelectWithParens
refineToSelectWithParens = \case
CExprAExpr (CExpr.SelectWithParensCExpr a Nothing) -> Just a
_ -> Nothing
-- |
-- Collapses the non-canonical @Right@-wrapping-a-bare-@select_with_parens@
-- shape of 'SubqueryAExpr'\'s final field to the @Left@ shape the parser
-- actually produces for it.
--
-- @a_expr subquery_Op sub_type select_with_parens@ (i.e. @Left@) and
-- @a_expr subquery_Op sub_type '(' a_expr ')'@ (i.e. @Right@) overlap
-- whenever the parenthesized @a_expr@ is itself nothing but a bare,
-- indirection-less @select_with_parens@: both parse @x = ANY ((select 1))@.
-- @suffix@ tries the @select_with_parens@ alternative before the
-- parenthesized @a_expr@ one (see the @d <- Left <$> ... <|> Right <$> ...@
-- line above), so that's always what the parser returns — never @Right@ —
-- making the latter non-canonical for this shape. Both 'arbitrary' and
-- 'shrink' can otherwise construct it, which renders fine but parses back to
-- a different, canonical value and so breaks the roundtrip property.
canonicalize :: AExpr -> AExpr
canonicalize = \case
SubqueryAExpr a b c (Right d)
| Just inner <- refineToSelectWithParens d -> SubqueryAExpr a b c (Left inner)
other -> other
instance Qc.Arbitrary AExpr where
shrink = fmap canonicalize . Qc.genericShrink
arbitrary =
fmap canonicalize $ Qc.sized $ \n ->
if n <= 1
then pure DefaultAExpr
else
Qc.oneof
[ CExprAExpr <$> Qc.arbitrary,
pure DefaultAExpr,
TypecastAExpr <$> safeAExprOperand (Gens.downscale Qc.arbitrary) <*> Qc.arbitrary,
CollateAExpr <$> safeAExprOperand (Gens.downscale Qc.arbitrary) <*> Qc.arbitrary,
AtTimeZoneAExpr <$> safeAExprOperand (Gens.downscale Qc.arbitrary) <*> Gens.downscale Qc.arbitrary,
PlusAExpr <$> Gens.downscale Qc.arbitrary,
MinusAExpr <$> Gens.downscale Qc.arbitrary,
SymbolicBinOpAExpr <$> safeAExprOperand (Gens.downscale Qc.arbitrary) <*> Qc.arbitrary <*> Gens.downscale Qc.arbitrary,
PrefixQualOpAExpr <$> Qc.arbitrary <*> Gens.downscale Qc.arbitrary,
AndAExpr <$> safeAExprOperand (Gens.downscale Qc.arbitrary) <*> Gens.downscale Qc.arbitrary,
OrAExpr <$> safeAExprOperand (Gens.downscale Qc.arbitrary) <*> Gens.downscale Qc.arbitrary,
NotAExpr <$> Gens.downscale Qc.arbitrary,
( do
a <- safeAExprOperand (Gens.downscale Qc.arbitrary)
b <- Qc.arbitrary
c <- Qc.arbitrary
e <- Gens.terminatingMaybe (Gens.downscale Qc.arbitrary)
-- See @renderVerbalRhs@ in the 'IsAst' instance above:
-- whenever there's an escape clause, an unbounded @d@ must
-- be pre-wrapped in parens so the generated value already
-- matches what parsing the (necessarily parenthesized)
-- rendering reconstructs — the same rule 'safeAExprOperand'
-- applies for the left/accumulator position.
d <- case e of
Nothing -> Gens.downscale Qc.arbitrary
Just _ -> safeAExprOperand (Gens.downscale Qc.arbitrary)
pure (VerbalExprBinOpAExpr a b c d e)
),
ReversableOpAExpr <$> safeAExprOperand (Gens.downscale Qc.arbitrary) <*> Qc.arbitrary <*> Qc.arbitrary,
IsnullAExpr <$> safeAExprOperand (Gens.downscale Qc.arbitrary),
NotnullAExpr <$> safeAExprOperand (Gens.downscale Qc.arbitrary),
OverlapsAExpr <$> Qc.arbitrary <*> Qc.arbitrary,
SubqueryAExpr <$> safeAExprOperand (Gens.downscale Qc.arbitrary) <*> Qc.arbitrary <*> Qc.arbitrary <*> Gens.downscale Qc.arbitrary,
UniqueAExpr <$> Gens.downscale Qc.arbitrary
]