From 63f6cc1ee418d62b07cf09f2903479a78e95d6d5 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Thu, 17 Sep 2026 15:01:40 +0900 Subject: [PATCH v1 1/2] Fix parser failure with whole-row join alias Vars When a query has aggregates or GROUP BY, parseCheckAggregates() flattens join alias Vars in the grouping expressions, target list and HAVING clause so that aliased and unaliased references compare equal. The parser has no PlannerInfo, so this relies on every join alias expression being able to carry varnullingrels without a PHV. That doesn't hold for a whole-row Var of a join on the nullable side of an outer join: it expands to a RowExpr, and pushing the nulling bits down into its fields would turn a NULL row into ROW(NULL, ...). Such queries therefore failed with "unsupported join alias expression". To fix, leave a nulled whole-row join Var unexpanded when no root is available. The planner expands it later and wraps it in a PlaceHolderVar as before. Nothing is lost for GROUP BY matching: a nulled whole-row Var can only be equal to the same Var, never to an expression over the join's columns. This has been broken since the introduction of varnullingrels in v16. --- src/backend/optimizer/util/var.c | 13 ++++++++++++- src/test/regress/expected/join.out | 29 +++++++++++++++++++++++++++++ src/test/regress/sql/join.sql | 13 +++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/backend/optimizer/util/var.c b/src/backend/optimizer/util/var.c index aca7399cd8c..7390e8982e4 100644 --- a/src/backend/optimizer/util/var.c +++ b/src/backend/optimizer/util/var.c @@ -776,6 +776,9 @@ pull_var_clause_walker(Node *node, pull_var_clause_context *context) * PlaceHolderVar or constructed from those, we can just add the * varnullingrels bits to the existing nullingrels field(s); otherwise * we have to add a PlaceHolderVar wrapper. + * + * If root is NULL, nulled whole-row JOIN Vars are left unexpanded, so the + * result must be flattened again with a root before it can be executed. */ Node * flatten_join_alias_vars(PlannerInfo *root, Query *query, Node *node) @@ -809,7 +812,8 @@ flatten_join_alias_vars(PlannerInfo *root, Query *query, Node *node) * PlaceHolderVars. We can avoid making PlaceHolderVars in the parser's * usage because it won't be dealing with arbitrary expressions: so long as * adjust_standard_join_alias_expression can handle everything the parser - * would make as a join alias expression, we're OK. + * would make as a join alias expression, we're OK. (Nulled whole-row join + * Vars are the exception; those are left unexpanded.) * * The "node" might be part of a sub-query of the Query whose join alias * Vars are to be expanded. "sublevels_up" indicates how far below the @@ -865,6 +869,13 @@ flatten_join_alias_vars_mutator(Node *node, ListCell *lv; ListCell *ln; + /* + * A nulled whole-row expansion needs a PlaceHolderVar, which we + * can't make without a root; leave the Var unexpanded. + */ + if (context->root == NULL && var->varnullingrels != NULL) + return node; + Assert(list_length(rte->joinaliasvars) == list_length(rte->eref->colnames)); forboth(lv, rte->joinaliasvars, ln, rte->eref->colnames) { diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 75544fe6aa3..eb88fb2f616 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -4422,6 +4422,35 @@ group by t1.q2 order by 1; 4567890123456789 | 6 (4 rows) +-- nulled whole-row Var of a join alias in aggregate queries +select t1.q2, count(t23) +from int8_tbl t1 left join + (int8_tbl t2 join int4_tbl t3 on t3.f1 = 0) t23 + on (t1.q2 = t23.q1) +group by t1.q2 order by 1; + q2 | count +-------------------+------- + -4567890123456789 | 0 + 123 | 2 + 456 | 0 + 4567890123456789 | 6 +(4 rows) + +select t23, count(*) +from int8_tbl t1 left join + (int8_tbl t2 join int4_tbl t3 on t3.f1 = 0) t23 + on (t1.q2 = t23.q1) +group by t23 order by 1; + t23 | count +----------------------------------------+------- + (123,456,0) | 1 + (123,4567890123456789,0) | 1 + (4567890123456789,-4567890123456789,0) | 2 + (4567890123456789,123,0) | 2 + (4567890123456789,4567890123456789,0) | 2 + | 2 +(6 rows) + -- -- test incorrect failure to NULL pulled-up subexpressions -- diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index fb83a96e939..29eca8eda2e 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -1384,6 +1384,19 @@ from int8_tbl t1 left join on (t1.q2 = t2.q1) group by t1.q2 order by 1; +-- nulled whole-row Var of a join alias in aggregate queries +select t1.q2, count(t23) +from int8_tbl t1 left join + (int8_tbl t2 join int4_tbl t3 on t3.f1 = 0) t23 + on (t1.q2 = t23.q1) +group by t1.q2 order by 1; + +select t23, count(*) +from int8_tbl t1 left join + (int8_tbl t2 join int4_tbl t3 on t3.f1 = 0) t23 + on (t1.q2 = t23.q1) +group by t23 order by 1; + -- -- test incorrect failure to NULL pulled-up subexpressions -- -- 2.37.1 (Apple Git-137.1)