From d8a311e8de0df48565b47d7f2f5439fb9000e1fa 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 7f93ea37f5f..37fd19aaeea 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -138,6 +138,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); @@ -1396,6 +1398,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. @@ -1422,19 +1444,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 @@ -2423,6 +2432,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 69623ab49f4..02b24760297 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -9824,6 +9824,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 69df1c01fc2..0c3113cce1e 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -3701,6 +3701,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;