diff --git a/src/backend/optimizer/prep/prepjointree.c b/src/backend/optimizer/prep/prepjointree.c index a91c5c1e361..6248f9caaaa 100644 --- a/src/backend/optimizer/prep/prepjointree.c +++ b/src/backend/optimizer/prep/prepjointree.c @@ -100,6 +100,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, JoinExpr *lowest_nulling_outer_join, @@ -1064,6 +1066,25 @@ 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->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. @@ -1090,18 +1111,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->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 @@ -2116,6 +2125,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->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->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 4830ac25c81..c8129292d34 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -6108,6 +6108,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 f73de38f515..4822021ee5b 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -2097,6 +2097,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;