From 156f52e741e4cc7bf48f243b10d916215dd6ba85 Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Sat, 19 Sep 2026 14:56:29 +0900 Subject: [PATCH v1] 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 whole-row Var of a join within the subquery can expand to an expression containing lateral references to the outer query, 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, which leads to assertion failures in distribute_qual_to_rels() or "wrong phnullingrels" errors. To fix, flatten join alias Vars in both the targetlist and the jointree quals 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. --- src/backend/optimizer/prep/prepjointree.c | 65 +++++++++++--- src/test/regress/expected/join.out | 100 ++++++++++++++++++++++ src/test/regress/sql/join.sql | 33 +++++++ 3 files changed, 185 insertions(+), 13 deletions(-) diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index dfe320beccd..424b29c8bb8 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,22 @@ 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, which could affect + * pullup_replace_vars' decisions about whether PlaceHolderVar wrappers + * are needed for tlist entries. We also flatten them in the jointree + * quals. Do this before the recheck below, so that it sees lateral + * references hidden in join alias Vars. (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); + 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 +1569,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 +2553,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, + * in place. + */ +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 75544fe6aa3..64fcfb45218 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -10690,6 +10690,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 fb83a96e939..5dbd5a612f0 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -4066,6 +4066,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; -- 2.37.1 (Apple Git-137.1)