Fix HAVING-to-WHERE pushdown for simple-CASE form

Commit f76686ce7 added a walker that detects when a HAVING clause uses
a collation that conflicts with the GROUP BY's nondeterministic
collation, keeping such clauses in HAVING.  The walker uses
exprInputCollation() to identify each ancestor's comparison collation,
but missed the simple-CASE case: parse analysis builds each WHEN as
OpExpr(CaseTestExpr op val), where CaseTestExpr is a placeholder for
the arg, while the actual arg expression sits at cexpr->arg, outside
the OpExpr that carries the comparison's inputcollid.  A GROUP Var at
cexpr->arg was therefore visited with the WHEN's inputcollid absent
from the ancestor stack, the conflict went undetected, and the clause
was wrongly pushed to WHERE.

Fix by handling simple CASE explicitly: before walking cexpr->arg,
push every WHEN's inputcollid onto the ancestor stack so a GROUP Var
at the arg is checked against the same collations the WHEN comparisons
would apply.  Then walk the WHEN bodies and defresult under the
unchanged stack, where their own collation contexts are picked up by
the default path.

Back-patch to v18 only; this fix extends the walker added by commit
f76686ce7 and inherits its dependency on the v18 RTE_GROUP mechanism.

Author: SATYANARAYANA NARLAPURAM <satyanarlapuram@gmail.com>
Reviewed-by: Richard Guo <guofenglinux@gmail.com>
Discussion: https://postgr.es/m/CAHg+QDcqPdd=2V0PQ_oNYj50OUeqSqznqFaYtP3RdokLBDXBqw@mail.gmail.com
Backpatch-through: 18
This commit is contained in:
Richard Guo 2026-05-08 10:57:50 +09:00
parent 936d8974c3
commit 1132af22cf
3 changed files with 124 additions and 4 deletions

View file

@ -1487,10 +1487,22 @@ find_having_collation_conflicts(Query *parse, Index group_rtindex)
* by collation-aware ancestors. At each GROUP Var with a nondeterministic
* varcollid, the clause has a conflict if any ancestor's inputcollid differs
* from the GROUP Var's varcollid. Most collation-aware nodes expose their
* inputcollid through exprInputCollation(); RowCompareExpr is the exception,
* as it carries one inputcollid per column in inputcollids[], so we descend
* into its (largs[i], rargs[i]) pairs explicitly with the matching collation
* pushed onto the stack.
* inputcollid through exprInputCollation(). Two structural exceptions need
* special handling:
*
* - RowCompareExpr carries one inputcollid per column in inputcollids[], so we
* descend into its (largs[i], rargs[i]) pairs explicitly with the matching
* collation pushed onto the stack.
*
* - A simple CASE (CaseExpr with a non-NULL arg) holds the arg outside the
* WHEN's OpExpr, even though the WHEN's OpExpr is the place where the
* comparison's inputcollid lives. Parse analysis builds each WHEN as
* "OpExpr(CaseTestExpr op val)" -- the CaseTestExpr is a placeholder for
* the arg. Before walking cexpr->arg we therefore push every WHEN's
* inputcollid onto the ancestor stack, so a GROUP Var at the arg is
* checked against the same collations the WHEN comparisons would apply.
* The WHEN bodies and defresult are then walked under the unchanged stack
* so their own collation contexts are picked up by the default path.
*/
static bool
having_collation_conflict_walker(Node *node, having_collation_ctx *ctx)
@ -1560,6 +1572,48 @@ having_collation_conflict_walker(Node *node, having_collation_ctx *ctx)
return false;
}
if (IsA(node, CaseExpr) && ((CaseExpr *) node)->arg != NULL)
{
CaseExpr *cexpr = (CaseExpr *) node;
int saved_len = list_length(ctx->ancestor_collids);
bool found;
/*
* Push every WHEN's inputcollid before walking cexpr->arg, since each
* WHEN implicitly compares the arg under that inputcollid.
*/
foreach_node(CaseWhen, cw, cexpr->args)
{
Oid collid = exprInputCollation((Node *) cw->expr);
if (OidIsValid(collid))
ctx->ancestor_collids = lappend_oid(ctx->ancestor_collids,
collid);
}
found = having_collation_conflict_walker((Node *) cexpr->arg, ctx);
ctx->ancestor_collids = list_truncate(ctx->ancestor_collids,
saved_len);
if (found)
return true;
/*
* Walk the WHEN bodies and defresult under the unchanged ancestor
* stack; any inputcollids inside them are picked up by the default
* path.
*/
foreach_node(CaseWhen, cw, cexpr->args)
{
if (having_collation_conflict_walker((Node *) cw->expr, ctx) ||
having_collation_conflict_walker((Node *) cw->result, ctx))
return true;
}
return having_collation_conflict_walker((Node *) cexpr->defresult,
ctx);
}
this_collid = exprInputCollation(node);
if (OidIsValid(this_collid))
ctx->ancestor_collids = lappend_oid(ctx->ancestor_collids,

View file

@ -2239,6 +2239,57 @@ SELECT x, count(*) FROM test3ci GROUP BY x HAVING ROW(x, 1) < ROW('ABC' COLLATE
abc | 2
(1 row)
-- Negative: simple-CASE form with conflicting WHEN comparison collation
EXPLAIN (COSTS OFF)
SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE x WHEN 'abc' COLLATE case_sensitive THEN true ELSE false END);
QUERY PLAN
-----------------------------------------------------------------------------------
HashAggregate
Group Key: x
Filter: CASE x WHEN 'abc'::text COLLATE case_sensitive THEN true ELSE false END
-> Seq Scan on test3ci
(4 rows)
SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE x WHEN 'abc' COLLATE case_sensitive THEN true ELSE false END);
x | count
-----+-------
abc | 2
(1 row)
-- Positive: simple-CASE form with matching collation, safe to push
EXPLAIN (COSTS OFF)
SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE x WHEN 'abc' COLLATE case_insensitive THEN true ELSE false END);
QUERY PLAN
-------------------------------------------------------------------------------------------
HashAggregate
Group Key: x
-> Seq Scan on test3ci
Filter: CASE x WHEN 'abc'::text COLLATE case_insensitive THEN true ELSE false END
(4 rows)
SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE x WHEN 'abc' COLLATE case_insensitive THEN true ELSE false END);
x | count
-----+-------
abc | 2
(1 row)
-- Negative: nested CASE with collation conflict
EXPLAIN (COSTS OFF)
SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE WHEN (CASE x WHEN 'abc' COLLATE case_sensitive THEN 1 ELSE 0 END) = 1 THEN true ELSE false END);
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------
HashAggregate
Group Key: x
Filter: CASE WHEN (CASE x WHEN 'abc'::text COLLATE case_sensitive THEN 1 ELSE 0 END = 1) THEN true ELSE false END
-> Seq Scan on test3ci
(4 rows)
SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE WHEN (CASE x WHEN 'abc' COLLATE case_sensitive THEN 1 ELSE 0 END) = 1 THEN true ELSE false END);
x | count
-----+-------
abc | 2
(1 row)
-- Positive: conflicting collation but no grouping expression reference
EXPLAIN (COSTS OFF)
SELECT x, count(*) FROM test3ci GROUP BY x HAVING current_setting('server_version') = 'abc' COLLATE case_sensitive;

View file

@ -793,6 +793,21 @@ EXPLAIN (COSTS OFF)
SELECT x, count(*) FROM test3ci GROUP BY x HAVING ROW(x, 1) < ROW('ABC' COLLATE case_sensitive, 1) ORDER BY 1;
SELECT x, count(*) FROM test3ci GROUP BY x HAVING ROW(x, 1) < ROW('ABC' COLLATE case_sensitive, 1) ORDER BY 1;
-- Negative: simple-CASE form with conflicting WHEN comparison collation
EXPLAIN (COSTS OFF)
SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE x WHEN 'abc' COLLATE case_sensitive THEN true ELSE false END);
SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE x WHEN 'abc' COLLATE case_sensitive THEN true ELSE false END);
-- Positive: simple-CASE form with matching collation, safe to push
EXPLAIN (COSTS OFF)
SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE x WHEN 'abc' COLLATE case_insensitive THEN true ELSE false END);
SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE x WHEN 'abc' COLLATE case_insensitive THEN true ELSE false END);
-- Negative: nested CASE with collation conflict
EXPLAIN (COSTS OFF)
SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE WHEN (CASE x WHEN 'abc' COLLATE case_sensitive THEN 1 ELSE 0 END) = 1 THEN true ELSE false END);
SELECT x, count(*) FROM test3ci GROUP BY x HAVING (CASE WHEN (CASE x WHEN 'abc' COLLATE case_sensitive THEN 1 ELSE 0 END) = 1 THEN true ELSE false END);
-- Positive: conflicting collation but no grouping expression reference
EXPLAIN (COSTS OFF)
SELECT x, count(*) FROM test3ci GROUP BY x HAVING current_setting('server_version') = 'abc' COLLATE case_sensitive;