1
0
mirror of https://github.com/postgres/postgres.git synced 2026-10-23 13:29:09 +03:00

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 <guofenglinux@gmail.com>
Reviewed-by: Tatsuya Kawata <kawatatatsuya0913@gmail.com>
Discussion: https://postgr.es/m/CAMbWs48GFZ=3Bjc1ug9JRsB0Qupg+CGcb9js5vmTM6kY2OGWOg@mail.gmail.com
Backpatch-through: 14
This commit is contained in:
Richard Guo
2026-10-06 14:44:39 +09:00
parent 658100f85b
commit d236e2241c
3 changed files with 188 additions and 12 deletions

View File

@@ -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.
*

View File

@@ -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

View File

@@ -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;