From a612a22486486eed0511de5fb72762596d3d6209 Mon Sep 17 00:00:00 2001 From: Matheus Alcantara Date: Fri, 23 Oct 2026 12:16:50 -0300 Subject: [PATCH v2] Don't replace Var-free subexpressions with grouped Vars Since f5050f795ae, substitute_grouped_columns_mutator() checks every subexpression against the GROUP BY items with equal() before accepting Consts and Params, so that a constant grouping item is marked nullable by grouping sets. But that also replaces any other Const that merely happens to be equal to a constant grouping item. Such a Const may be in a place where Vars are not allowed, such as the elemexpr of an ArrayCoerceExpr, where the isExplicit flag of a length-coercion function is a boolean Const. For example: SELECT ARRAY[]::varchar(500)[] GROUP BY CAST('true' AS boolean); crashed the planner, because constant-folding evaluated a Var without a tuple slot. A grouping expression that contains no Vars of the current query level is not a reference to any column of the input, so an equal expression elsewhere in the query is not a reference to that grouping column. Treat such grouping expressions as referring only to the targetlist entry that GROUP BY identified: replace that entry as a whole, and leave them out of the equal() matching altogether. This covers constants, Params, outer references and Var-free expressions such as 1+2. With that, a Const or Param node can never match a grouping item, so accept them at the top of the mutator again, as before f5050f795ae. Author: Andrey Rachitskiy Co-authored-by: Matheus Alcantara --- src/backend/parser/parse_agg.c | 74 +++++++++++++++++++--- src/test/regress/expected/groupingsets.out | 56 ++++++++++++++++ src/test/regress/sql/groupingsets.sql | 20 ++++++ 3 files changed, 140 insertions(+), 10 deletions(-) diff --git a/src/backend/parser/parse_agg.c b/src/backend/parser/parse_agg.c index a5424c9d24c..af54d6d576a 100644 --- a/src/backend/parser/parse_agg.c +++ b/src/backend/parser/parse_agg.c @@ -53,6 +53,8 @@ typedef struct List *groupClauseSubLevels; /* list of lists of TargetEntry */ List *gset_common; /* integer list of sortgrouprefs */ bool have_non_var_grouping; + Bitmapset *varfree_grouping; /* groupClauses items with no level-zero + * Vars, by position */ List **func_grouped_rels; int sublevels_up; bool in_agg_direct_args; @@ -69,6 +71,7 @@ static Node *substitute_grouped_columns(Node *node, ParseState *pstate, Query *q List *groupClauses, List *groupClauseCommonVars, List *gset_common, bool have_non_var_grouping, + Bitmapset *varfree_grouping, List **func_grouped_rels); static Node *substitute_grouped_columns_mutator(Node *node, substitute_grouped_columns_context *context); @@ -1142,8 +1145,10 @@ parseCheckAggregates(ParseState *pstate, Query *qry) List *groupClauses = NIL; List *groupClauseCommonVars = NIL; bool have_non_var_grouping; + Bitmapset *varfree_grouping = NULL; List *func_grouped_rels = NIL; ListCell *l; + int attnum; bool hasJoinRTEs; bool hasSelfRefRTEs; Node *clause; @@ -1267,12 +1272,22 @@ parseCheckAggregates(ParseState *pstate, Query *qry) * Track Vars that are included in all grouping sets separately in * groupClauseCommonVars, since these are the only ones we can use to * check for functional dependencies. + * + * Also remember which grouping expressions contain no Vars of this query + * level (constants, Params, outer references and the like). Those can + * only be referenced through the targetlist entry that GROUP BY + * identified; see substitute_grouped_columns_mutator. */ have_non_var_grouping = false; + attnum = 0; foreach(l, groupClauses) { TargetEntry *tle = lfirst(l); + attnum++; + if (!contain_vars_of_level((Node *) tle->expr, 0)) + varfree_grouping = bms_add_member(varfree_grouping, attnum); + if (!IsA(tle->expr, Var)) { have_non_var_grouping = true; @@ -1308,6 +1323,7 @@ parseCheckAggregates(ParseState *pstate, Query *qry) groupClauses, groupClauseCommonVars, gset_common, have_non_var_grouping, + varfree_grouping, &func_grouped_rels); clause = (Node *) qry->havingQual; @@ -1321,6 +1337,7 @@ parseCheckAggregates(ParseState *pstate, Query *qry) groupClauses, groupClauseCommonVars, gset_common, have_non_var_grouping, + varfree_grouping, &func_grouped_rels); /* @@ -1351,6 +1368,7 @@ substitute_grouped_columns(Node *node, ParseState *pstate, Query *qry, List *groupClauses, List *groupClauseCommonVars, List *gset_common, bool have_non_var_grouping, + Bitmapset *varfree_grouping, List **func_grouped_rels) { substitute_grouped_columns_context context; @@ -1363,6 +1381,7 @@ substitute_grouped_columns(Node *node, ParseState *pstate, Query *qry, context.groupClauseSubLevels = NIL; context.gset_common = gset_common; context.have_non_var_grouping = have_non_var_grouping; + context.varfree_grouping = varfree_grouping; context.func_grouped_rels = func_grouped_rels; context.sublevels_up = 0; context.in_agg_direct_args = false; @@ -1377,6 +1396,9 @@ substitute_grouped_columns_mutator(Node *node, if (node == NULL) return NULL; + if (IsA(node, Const) || + IsA(node, Param)) + return node; /* constants are always acceptable */ if (IsA(node, Aggref)) { @@ -1424,6 +1446,42 @@ substitute_grouped_columns_mutator(Node *node, return node; } + /* + * A grouping expression that contains no Vars of this query level, such + * as a constant, refers only to the targetlist entry that GROUP BY + * identified. Other occurrences of an equal expression are not + * references to that grouping column, so they are not candidates for the + * equal() matching below. Replacing them would be wrong anyway: they can + * appear in places where Vars are not allowed, such as the elemexpr of an + * ArrayCoerceExpr. + */ + if (IsA(node, TargetEntry) && + ((TargetEntry *) node)->ressortgroupref != 0 && + context->have_non_var_grouping && + context->sublevels_up == 0) + { + TargetEntry *tle = (TargetEntry *) node; + int attnum = 0; + + foreach(gl, context->groupClauses) + { + TargetEntry *grouptle = (TargetEntry *) lfirst(gl); + + attnum++; + if (grouptle->ressortgroupref != tle->ressortgroupref) + continue; + if (bms_is_member(attnum, context->varfree_grouping)) + { + tle = flatCopyTargetEntry(tle); + tle->expr = (Expr *) buildGroupedVar(attnum, + tle->ressortgroupref, + context); + return (Node *) tle; + } + break; + } + } + /* * If we have any GROUP BY items that are not simple Vars, check to see if * subexpression as a whole matches any GROUP BY item. We need to do this @@ -1448,6 +1506,11 @@ substitute_grouped_columns_mutator(Node *node, TargetEntry *tle = (TargetEntry *) lfirst(gl); attnum++; + + /* Var-free items are matched only by targetlist entry, above */ + if (bms_is_member(attnum, context->varfree_grouping)) + continue; + if (equal(node, tle->expr)) { /* acceptable, replace it with a GROUP Var */ @@ -1458,16 +1521,6 @@ substitute_grouped_columns_mutator(Node *node, } } - /* - * Constants are always acceptable. We have to do this after we checked - * the subexpression as a whole for a match, because it is possible that - * we have GROUP BY items that are constants, and the constants would - * become not so constant after the grouping step. - */ - if (IsA(node, Const) || - IsA(node, Param)) - return node; - /* * If we have an ungrouped Var of the original query level, we have a * failure. Vars below the original query level are not a problem, and @@ -1624,6 +1677,7 @@ finalize_grouping_exprs(Node *node, ParseState *pstate, Query *qry, context.groupClauseSubLevels = NIL; context.gset_common = NIL; context.have_non_var_grouping = have_non_var_grouping; + context.varfree_grouping = NULL; context.func_grouped_rels = NULL; context.sublevels_up = 0; context.in_agg_direct_args = false; diff --git a/src/test/regress/expected/groupingsets.out b/src/test/regress/expected/groupingsets.out index 08d5ce3156d..7725dec0a59 100644 --- a/src/test/regress/expected/groupingsets.out +++ b/src/test/regress/expected/groupingsets.out @@ -2667,6 +2667,62 @@ select 1 as one group by rollup(one) order by one nulls first; 1 (2 rows) +-- A grouping expression containing no Vars of the query level refers only +-- to the targetlist entry GROUP BY identified; equal expressions elsewhere +-- must not be replaced by grouped Vars (this used to crash) +select array[]::varchar(500)[] as a group by cast('true' as boolean); + a +---- + {} +(1 row) + +select array[]::varchar(500)[] as a group by 504::int4; + a +---- + {} +(1 row) + +select (select array[]::varchar(500)[]) as a +group by rollup(cast('true' as boolean)); + a +---- + {} + {} +(2 rows) + +explain (verbose, costs off) +select 1 as a, 1 as b, 1+2 as c, (1+2)*10 as d +group by rollup(a, c); + QUERY PLAN +--------------------------- + MixedAggregate + Output: (1), 1, (3), 30 + Hash Key: 1, 3 + Hash Key: 1 + Group Key: () + -> Result + Output: 1, 3 +(7 rows) + +select 1 as a, 1 as b, 1+2 as c, (1+2)*10 as d +group by rollup(a, c) +order by a nulls first, c nulls first; + a | b | c | d +---+---+---+---- + | 1 | | 30 + 1 | 1 | | 30 + 1 | 1 | 3 | 30 +(3 rows) + +-- likewise for an outer reference used as a grouping expression +select array(select o.a as k from (values (1)) as i (x) + group by rollup(o.a, x+0) order by 1 nulls first) +from (values (10)) as o (a); + array +-------------- + {NULL,10,10} +(1 row) + explain (costs off) select a, b, row_number() over (order by a, b nulls first) from (values (1, 1), (2, 2)) as t (a, b) where a = b diff --git a/src/test/regress/sql/groupingsets.sql b/src/test/regress/sql/groupingsets.sql index c5d4ed2eb57..22b8c517feb 100644 --- a/src/test/regress/sql/groupingsets.sql +++ b/src/test/regress/sql/groupingsets.sql @@ -771,6 +771,26 @@ explain (costs off) select 1 as one group by rollup(one) order by one nulls first; select 1 as one group by rollup(one) order by one nulls first; +-- A grouping expression containing no Vars of the query level refers only +-- to the targetlist entry GROUP BY identified; equal expressions elsewhere +-- must not be replaced by grouped Vars (this used to crash) +select array[]::varchar(500)[] as a group by cast('true' as boolean); +select array[]::varchar(500)[] as a group by 504::int4; +select (select array[]::varchar(500)[]) as a +group by rollup(cast('true' as boolean)); + +explain (verbose, costs off) +select 1 as a, 1 as b, 1+2 as c, (1+2)*10 as d +group by rollup(a, c); +select 1 as a, 1 as b, 1+2 as c, (1+2)*10 as d +group by rollup(a, c) +order by a nulls first, c nulls first; + +-- likewise for an outer reference used as a grouping expression +select array(select o.a as k from (values (1)) as i (x) + group by rollup(o.a, x+0) order by 1 nulls first) +from (values (10)) as o (a); + explain (costs off) select a, b, row_number() over (order by a, b nulls first) from (values (1, 1), (2, 2)) as t (a, b) where a = b -- 2.50.1 (Apple Git-155)