]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Fix HAVING-to-WHERE pushdown for simple-CASE form
authorRichard Guo <rguo@postgresql.org>
Fri, 8 May 2026 01:57:50 +0000 (10:57 +0900)
committerRichard Guo <rguo@postgresql.org>
Fri, 8 May 2026 01:57:50 +0000 (10:57 +0900)
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

src/backend/optimizer/plan/planner.c
src/test/regress/expected/collate.icu.utf8.out
src/test/regress/sql/collate.icu.utf8.sql

index 933dcbf5004ce6fade7a38f35520d6f30170d79d..f4689e7c9f8bfc7c1a8e628ad8d91c602c3b1473 100644 (file)
@@ -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,
index 8c3a369e212ea41a7990b6aaff93172c575e5f6d..9c4b8ccff78f5b8d0715dd4148f8e152ef220c11 100644 (file)
@@ -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;
index fdcdb2094f8a98ecda48a968a1dc76194cea8e8e..a8e34017e07f076772b58288e8a44b9056131595 100644 (file)
@@ -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;