From 1a12f86a428b251933dd14601397eb28cf08163d Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Fri, 6 Nov 2026 18:22:38 +0900 Subject: [PATCH v1] Fix nullingrels of whole-row references to JOIN USING aliases A whole-row reference to a JOIN USING alias is expanded into a RowExpr over the join's merged columns. transformWholeRowRef() built the fields with expandRTE(), which does not mark the Vars as nullable by outer joins above the JOIN USING. As a result, the planner could evaluate a qual such as "x IS NULL" below an outer join that nulls x, producing wrong results, or fail with "wrong varnullingrels" errors. To fix, build the fields with expandNSItemVars(), which yields just the columns of the alias and marks them with markNullableIfNeeded(), as scanNSItemForColumn() does for a plain reference such as x.i. Stored views containing such a reference keep the old parse tree, so they must be recreated to get correct results. Back-patch to v16, where outer-join-aware Vars were introduced. --- src/backend/parser/parse_expr.c | 17 ++++++----------- src/test/regress/expected/join.out | 22 ++++++++++++++++++++++ src/test/regress/sql/join.sql | 7 +++++++ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 728392f436f..c810c5c26c4 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -2681,26 +2681,21 @@ transformWholeRowRef(ParseState *pstate, ParseNamespaceItem *nsitem, else { RowExpr *rowexpr; - List *fields; /* - * We want only as many columns as are listed in p_names->colnames, - * and we should use those names not whatever possibly-aliased names - * are in the RTE. We needn't worry about marking the RTE for SELECT - * access, as the common columns are surely so marked already. + * We want only the columns listed in p_names->colnames, and we should + * use those names not whatever possibly-aliased names are in the RTE. + * We needn't worry about marking the RTE for SELECT access, as the + * common columns are surely so marked already. */ - expandRTE(nsitem->p_rte, nsitem->p_rtindex, sublevels_up, - nsitem->p_returning_type, location, false, NULL, &fields); rowexpr = makeNode(RowExpr); - rowexpr->args = list_truncate(fields, - list_length(nsitem->p_names->colnames)); + rowexpr->args = expandNSItemVars(pstate, nsitem, sublevels_up, + location, NULL); rowexpr->row_typeid = RECORDOID; rowexpr->row_format = COERCE_IMPLICIT_CAST; rowexpr->colnames = copyObject(nsitem->p_names->colnames); rowexpr->location = location; - /* XXX we ought to mark the row as possibly nullable */ - return (Node *) rowexpr; } } diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 7773852e61b..0e674259fc3 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -1673,6 +1673,28 @@ SELECT row_to_json(x.*) FROM J1_TBL JOIN J2_TBL USING (i) AS x WHERE J1_TBL.t = {"i":1} (1 row) +-- whole-row reference to a join using alias below an outer join +SELECT t1.i, t1.t FROM J1_TBL t1 + LEFT JOIN (J1_TBL JOIN J2_TBL USING (i) AS x) ON t1.i = x.i + WHERE x IS NULL ORDER BY 1, 2; + i | t +---+------- + 4 | four + 6 | six + 7 | seven + 8 | eight + | null + | zero +(6 rows) + +SELECT count(*) FROM J1_TBL t1 + LEFT JOIN (J1_TBL JOIN J2_TBL USING (i) AS x) ON true + LEFT JOIN J2_TBL t2 ON x IS NULL; + count +------- + 77 +(1 row) + -- -- NATURAL JOIN -- Inner equi-join on all columns with the same name diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index a49d343ec32..3fcec04e248 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -137,6 +137,13 @@ SELECT * FROM J1_TBL a1 JOIN J2_TBL a2 USING (i) AS a1; -- error SELECT x.* FROM J1_TBL JOIN J2_TBL USING (i) AS x WHERE J1_TBL.t = 'one'; SELECT ROW(x.*) FROM J1_TBL JOIN J2_TBL USING (i) AS x WHERE J1_TBL.t = 'one'; SELECT row_to_json(x.*) FROM J1_TBL JOIN J2_TBL USING (i) AS x WHERE J1_TBL.t = 'one'; +-- whole-row reference to a join using alias below an outer join +SELECT t1.i, t1.t FROM J1_TBL t1 + LEFT JOIN (J1_TBL JOIN J2_TBL USING (i) AS x) ON t1.i = x.i + WHERE x IS NULL ORDER BY 1, 2; +SELECT count(*) FROM J1_TBL t1 + LEFT JOIN (J1_TBL JOIN J2_TBL USING (i) AS x) ON true + LEFT JOIN J2_TBL t2 ON x IS NULL; -- -- NATURAL JOIN -- 2.37.1 (Apple Git-137.1)