From d174f31bdced5664569528cb12f38991e9543c05 Mon Sep 17 00:00:00 2001
From: Richard Guo <guofenglinux@gmail.com>
Date: Thu, 17 Sep 2026 15:26:08 +0900
Subject: [PATCH v1 2/2] Fix planner failure with zero-column join alias Vars

When a whole-row Var of a join on the nullable side of an outer join
is expanded, the resulting RowExpr is wrapped in a PlaceHolderVar to
carry the Var's nullingrels.  If the join has no columns, the RowExpr
contains no Vars, so the PlaceHolderVar is instead evaluated at the
join's input relations.  add_nullingrels_if_needed() refused to do
that when the Var is an outer reference from a subquery, and raised
"unsupported join alias expression".

That restriction is unnecessary.  Every planner caller passes
root->parse as the query, so the Var always belongs to root->parse
regardless of its varlevelsup, and get_relids_for_join() on it returns
relids at the correct level.  This is the same level the non-empty
case already gets from pull_varnos_of_level().  Remove the check, and
add an assertion that the query is root->parse.
---
 src/backend/optimizer/util/var.c   |  5 +++--
 src/test/regress/expected/join.out | 16 ++++++++++++++++
 src/test/regress/sql/join.sql      |  8 ++++++++
 3 files changed, 27 insertions(+), 2 deletions(-)

diff --git a/src/backend/optimizer/util/var.c b/src/backend/optimizer/util/var.c
index 7390e8982e4..4cfea8d1cb9 100644
--- a/src/backend/optimizer/util/var.c
+++ b/src/backend/optimizer/util/var.c
@@ -791,6 +791,8 @@ flatten_join_alias_vars(PlannerInfo *root, Query *query, Node *node)
 	 * it's okay to immediately increment sublevels_up.
 	 */
 	Assert(node != (Node *) query);
+	/* add_nullingrels_if_needed relies on this */
+	Assert(root == NULL || query == root->parse);
 
 	context.root = root;
 	context.query = query;
@@ -1257,8 +1259,7 @@ add_nullingrels_if_needed(PlannerInfo *root, Node *newnode, Var *oldvar)
 
 		if (bms_is_empty(phrels))	/* variable-free? */
 		{
-			if (levelsup != 0)	/* this won't work otherwise */
-				elog(ERROR, "unsupported join alias expression");
+			/* oldvar belongs to root->parse even when levelsup > 0 */
 			phrels = get_relids_for_join(root->parse, oldvar->varno);
 			/* If it's an outer join, eval below not above the join */
 			phrels = bms_del_member(phrels, oldvar->varno);
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index eb88fb2f616..f5d05bceb8f 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4451,6 +4451,22 @@ group by t23 order by 1;
                                         |     2
 (6 rows)
 
+-- nulled whole-row Var of a zero-column join, referenced from a subquery
+select t1.q1, t1.q2, (select t23::text)
+from int8_tbl t1 left join
+  ((select from int4_tbl where f1 = 0) t2
+   cross join (select from int4_tbl where f1 = 0) t3) t23
+  on (t1.q1 = 123)
+order by 1, 2;
+        q1        |        q2         | t23 
+------------------+-------------------+-----
+              123 |               456 | ()
+              123 |  4567890123456789 | ()
+ 4567890123456789 | -4567890123456789 | 
+ 4567890123456789 |               123 | 
+ 4567890123456789 |  4567890123456789 | 
+(5 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 29eca8eda2e..4577e7c54f2 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -1397,6 +1397,14 @@ from int8_tbl t1 left join
   on (t1.q2 = t23.q1)
 group by t23 order by 1;
 
+-- nulled whole-row Var of a zero-column join, referenced from a subquery
+select t1.q1, t1.q2, (select t23::text)
+from int8_tbl t1 left join
+  ((select from int4_tbl where f1 = 0) t2
+   cross join (select from int4_tbl where f1 = 0) t3) t23
+  on (t1.q1 = 123)
+order by 1, 2;
+
 --
 -- test incorrect failure to NULL pulled-up subexpressions
 --
-- 
2.37.1 (Apple Git-137.1)

