diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index e7c20b0710a..55fddf804c4 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -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, diff --git a/src/test/regress/expected/collate.icu.utf8.out b/src/test/regress/expected/collate.icu.utf8.out index 17876844a45..783dce6b887 100644 --- a/src/test/regress/expected/collate.icu.utf8.out +++ b/src/test/regress/expected/collate.icu.utf8.out @@ -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; diff --git a/src/test/regress/sql/collate.icu.utf8.sql b/src/test/regress/sql/collate.icu.utf8.sql index 7488624401b..724df44d9ae 100644 --- a/src/test/regress/sql/collate.icu.utf8.sql +++ b/src/test/regress/sql/collate.icu.utf8.sql @@ -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;