From d4c15309e7ed6e30e360b5911e1f4d3ca754aa2b Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Tue, 6 Oct 2026 14:40:17 +0900 Subject: [PATCH] Flatten join alias Vars before rechecking subquery pullup safety pull_up_simple_subquery() rechecks is_simple_subquery() after pulling up the subquery's own subqueries, but it flattened join alias Vars in the subquery's targetlist only after that, and never in its quals. A join alias Var within the subquery, such as a whole-row Var of a join, can expand to an expression containing lateral references to the outer query once the join's inputs have been pulled up, and until it is flattened is_simple_subquery() cannot see them. We could thus pull up a LATERAL subquery whose targetlist or quals reference rels outside the lowest outer join above it. This leads to assertion failures in distribute_qual_to_rels() or "wrong phnullingrels" errors, and in some cases to wrong query results, where rows that should have been preserved by an outer join above the subquery are discarded. To fix, flatten join alias Vars in the targetlist, and in the jointree quals if the subquery is LATERAL, before the recheck. If the recheck fails, the flattened copy is discarded along with the rest of the modified subquery, so no harm is done. Back-patch to all supported branches, as this can lead to wrong query results. Author: Richard Guo Reviewed-by: Tatsuya Kawata Discussion: https://postgr.es/m/CAMbWs48GFZ=3Bjc1ug9JRsB0Qupg+CGcb9js5vmTM6kY2OGWOg@mail.gmail.com Backpatch-through: 14 --- src/backend/optimizer/prep/prepjointree.c | 69 ++++++++++++--- src/test/regress/expected/join.out | 100 ++++++++++++++++++++++ src/test/regress/sql/join.sql | 33 +++++++ 3 files changed, 189 insertions(+), 13 deletions(-) diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index dfe320beccd..68cf6f18570 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -145,6 +145,8 @@ static bool is_safe_append_member(Query *subquery); static bool jointree_contains_lateral_outer_refs(PlannerInfo *root, Node *jtnode, bool restricted, Relids safe_upper_varnos); +static void flatten_join_alias_vars_in_jointree(PlannerInfo *root, + Node *jtnode); static void perform_pullup_replace_vars(PlannerInfo *root, pullup_replace_vars_context *rvcontext, AppendRelInfo *containing_appendrel); @@ -1525,6 +1527,26 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, */ pull_up_subqueries(subroot); + /* + * We must flatten any join alias Vars in the subquery's targetlist, + * because pulling up the subquery's subqueries might have changed their + * expansions into arbitrary expressions. That could affect + * pullup_replace_vars' decisions about whether PlaceHolderVar wrappers + * are needed for tlist entries, and if the subquery is LATERAL, the + * expansions might contain lateral references that the is_simple_subquery + * recheck below has to see. The latter also requires flattening join + * alias Vars in the jointree quals of a LATERAL subquery. (Likely it'd + * be better to do flatten_join_alias_vars on the whole query tree at some + * earlier stage, maybe even in the rewriter; but for now let's just fix + * these cases here.) + */ + subquery->targetList = (List *) + flatten_join_alias_vars(subroot, subroot->parse, + (Node *) subquery->targetList); + if (rte->lateral) + flatten_join_alias_vars_in_jointree(subroot, + (Node *) subquery->jointree); + /* * Now we must recheck whether the subquery is still simple enough to pull * up. If not, abandon processing it. @@ -1551,19 +1573,6 @@ pull_up_simple_subquery(PlannerInfo *root, Node *jtnode, RangeTblEntry *rte, return jtnode; } - /* - * We must flatten any join alias Vars in the subquery's targetlist, - * because pulling up the subquery's subqueries might have changed their - * expansions into arbitrary expressions, which could affect - * pullup_replace_vars' decisions about whether PlaceHolderVar wrappers - * are needed for tlist entries. (Likely it'd be better to do - * flatten_join_alias_vars on the whole query tree at some earlier stage, - * maybe even in the rewriter; but for now let's just fix this case here.) - */ - subquery->targetList = (List *) - flatten_join_alias_vars(subroot, subroot->parse, - (Node *) subquery->targetList); - /* * Adjust level-0 varnos in subquery so that we can append its rangetable * to upper query's. We have to fix the subquery's append_rel_list as @@ -2548,6 +2557,40 @@ jointree_contains_lateral_outer_refs(PlannerInfo *root, Node *jtnode, return false; } +/* + * flatten_join_alias_vars_in_jointree + * Apply flatten_join_alias_vars to all quals in the given jointree, + * replacing them in place. The jointree must belong to root->parse. + */ +static void +flatten_join_alias_vars_in_jointree(PlannerInfo *root, Node *jtnode) +{ + if (jtnode == NULL) + return; + if (IsA(jtnode, RangeTblRef)) + return; + else if (IsA(jtnode, FromExpr)) + { + FromExpr *f = (FromExpr *) jtnode; + ListCell *l; + + foreach(l, f->fromlist) + flatten_join_alias_vars_in_jointree(root, lfirst(l)); + f->quals = flatten_join_alias_vars(root, root->parse, f->quals); + } + else if (IsA(jtnode, JoinExpr)) + { + JoinExpr *j = (JoinExpr *) jtnode; + + flatten_join_alias_vars_in_jointree(root, j->larg); + flatten_join_alias_vars_in_jointree(root, j->rarg); + j->quals = flatten_join_alias_vars(root, root->parse, j->quals); + } + else + elog(ERROR, "unrecognized node type: %d", + (int) nodeTag(jtnode)); +} + /* * Perform pullup_replace_vars everyplace it's needed in the query tree. * diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 88d9d362d7c..25a9a39cdc1 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -10947,6 +10947,106 @@ lateral (select * from int8_tbl t1, 0 | 4567890123456789 | -4567890123456789 | 4567890123456789 | 4567890123456789 (3 rows) +-- check that lateral references hidden in a whole-row join alias Var +-- prevent pullup of a LATERAL subquery +explain (verbose, costs off) +select i4.f1, ss.x, i8.q1 +from int4_tbl i4, + lateral (select (j is null)::int + from ((select i4.f1) s left join (select 1) v on false) j) ss(x) + left join int8_tbl i8 on ss.x = i8.q1 +order by 1; + QUERY PLAN +-------------------------------------------------------------------------------- + Sort + Output: i4.f1, ((((i4.f1 IS NULL) AND ((1) IS NULL)))::integer), i8.q1 + Sort Key: i4.f1 + -> Hash Left Join + Output: i4.f1, ((((i4.f1 IS NULL) AND ((1) IS NULL)))::integer), i8.q1 + Hash Cond: (((((i4.f1 IS NULL) AND ((1) IS NULL)))::integer) = i8.q1) + -> Nested Loop + Output: i4.f1, ((((i4.f1 IS NULL) AND ((1) IS NULL)))::integer) + -> Seq Scan on public.int4_tbl i4 + Output: i4.f1 + -> Nested Loop Left Join + Output: (((i4.f1 IS NULL) AND ((1) IS NULL)))::integer + Join Filter: false + -> Result + -> Result + Output: 1 + One-Time Filter: false + -> Hash + Output: i8.q1 + -> Seq Scan on public.int8_tbl i8 + Output: i8.q1 +(21 rows) + +select i4.f1, ss.x, i8.q1 +from int4_tbl i4, + lateral (select (j is null)::int + from ((select i4.f1) s left join (select 1) v on false) j) ss(x) + left join int8_tbl i8 on ss.x = i8.q1 +order by 1; + f1 | x | q1 +-------------+---+---- + -2147483647 | 0 | + -123456 | 0 | + 0 | 0 | + 123456 | 0 | + 2147483647 | 0 | +(5 rows) + +-- same, with the lateral reference hidden in the subquery's quals +explain (verbose, costs off) +select i4.f1, i8.q1, ss.y +from int4_tbl i4, + int8_tbl i8 left join + lateral (select 1 from ((select i4.f1) s left join (select 1) v on false) j + where length(j::text) > 3) ss(y) on true +where i4.f1 = 0 +order by 1, 2; + QUERY PLAN +------------------------------------------------------------------------- + Sort + Output: i4.f1, i8.q1, (1) + Sort Key: i8.q1 + -> Nested Loop + Output: i4.f1, i8.q1, (1) + -> Seq Scan on public.int4_tbl i4 + Output: i4.f1 + Filter: (i4.f1 = 0) + -> Nested Loop Left Join + Output: i8.q1, (1) + -> Seq Scan on public.int8_tbl i8 + Output: i8.q1, i8.q2 + -> Materialize + Output: (1) + -> Nested Loop Left Join + Output: 1 + Join Filter: false + Filter: (length((ROW(i4.f1, (1)))::text) > 3) + -> Result + -> Result + Output: 1 + One-Time Filter: false +(22 rows) + +select i4.f1, i8.q1, ss.y +from int4_tbl i4, + int8_tbl i8 left join + lateral (select 1 from ((select i4.f1) s left join (select 1) v on false) j + where length(j::text) > 3) ss(y) on true +where i4.f1 = 0 +order by 1, 2; + f1 | q1 | y +----+------------------+--- + 0 | 123 | 1 + 0 | 123 | 1 + 0 | 4567890123456789 | 1 + 0 | 4567890123456789 | 1 + 0 | 4567890123456789 | 1 +(5 rows) + -- test some error cases where LATERAL should have been used but wasn't select f1,g from int4_tbl a, (select f1 as g) ss; ERROR: column "f1" does not exist diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index f6cf18b8872..7a431b926ea 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -4137,6 +4137,39 @@ lateral (select * from int8_tbl t1, and (select v.id=0)) offset 0) ss2) ss where t1.q1 = ss.q2) ss0; +-- check that lateral references hidden in a whole-row join alias Var +-- prevent pullup of a LATERAL subquery +explain (verbose, costs off) +select i4.f1, ss.x, i8.q1 +from int4_tbl i4, + lateral (select (j is null)::int + from ((select i4.f1) s left join (select 1) v on false) j) ss(x) + left join int8_tbl i8 on ss.x = i8.q1 +order by 1; +select i4.f1, ss.x, i8.q1 +from int4_tbl i4, + lateral (select (j is null)::int + from ((select i4.f1) s left join (select 1) v on false) j) ss(x) + left join int8_tbl i8 on ss.x = i8.q1 +order by 1; + +-- same, with the lateral reference hidden in the subquery's quals +explain (verbose, costs off) +select i4.f1, i8.q1, ss.y +from int4_tbl i4, + int8_tbl i8 left join + lateral (select 1 from ((select i4.f1) s left join (select 1) v on false) j + where length(j::text) > 3) ss(y) on true +where i4.f1 = 0 +order by 1, 2; +select i4.f1, i8.q1, ss.y +from int4_tbl i4, + int8_tbl i8 left join + lateral (select 1 from ((select i4.f1) s left join (select 1) v on false) j + where length(j::text) > 3) ss(y) on true +where i4.f1 = 0 +order by 1, 2; + -- test some error cases where LATERAL should have been used but wasn't select f1,g from int4_tbl a, (select f1 as g) ss; select f1,g from int4_tbl a, (select a.f1 as g) ss;