From: Richard Guo Date: Fri, 8 May 2026 01:57:50 +0000 (+0900) Subject: Fix HAVING-to-WHERE pushdown for simple-CASE form X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ba82de48e62c633f5ad5e9dc9e6f2f3e8455a09f;p=thirdparty%2Fpostgresql.git 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 Reviewed-by: Richard Guo Discussion: https://postgr.es/m/CAHg+QDcqPdd=2V0PQ_oNYj50OUeqSqznqFaYtP3RdokLBDXBqw@mail.gmail.com Backpatch-through: 18 --- diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index 933dcbf5004..f4689e7c9f8 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -1586,10 +1586,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) @@ -1659,6 +1671,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, diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out index 8c3a369e212..9c4b8ccff78 100644 --- a/src/test/regress/expected/collate.icu.utf8.out +++ b/src/test/regress/expected/collate.icu.utf8.out @@ -2249,6 +2249,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; diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql index fdcdb2094f8..a8e34017e07 100644 --- a/src/test/regress/sql/collate.icu.utf8.sql +++ b/src/test/regress/sql/collate.icu.utf8.sql @@ -797,6 +797,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;