From 372761e05a62856d32fb6a9811edf1c51a286b9c Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Wed, 7 Oct 2026 14:35:01 +0900 Subject: [PATCH] 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. Author: Richard Guo Discussion: https://postgr.es/m/CAMbWs49q_gvoeOoF0+KZ-DpQaDb=nGFfSgrz2V_n1_64L4ZNVQ@mail.gmail.com Backpatch-through: 16 --- 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 ce8a2e66fe4..d14d80973e4 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 25a9a39cdc1..b0cab6a7823 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 7a431b926ea..4fc0ded779d 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 --