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

Fix PlaceHolderVar placement for join alias expressions

When a join alias Var with nonempty varnullingrels expands to an
expression that cannot carry the nullingrels itself, such as the
RowExpr of a whole-row Var, add_nullingrels_if_needed() wraps it in a
PlaceHolderVar.  The choice of the PHV's phrels had several problems.

First, phrels was taken from all Vars in the expression.  If a LATERAL
subquery within the join has been pulled up, the expression can
contain lateral references to rels outside the join, so the PHV could
be evaluated above the outer join that is supposed to null it.  This
gave wrong results, or "wrong phnullingrels" errors.

Second, for a variable-free expression we fell back to the join's
relids but removed the join's own outer-join relid.  That leaves a set
spanning both sides of the outer join without the join itself, which
can make the PHV look nullable by that join and cause "wrong
phnullingrels" errors.

Third, that fallback raised "unsupported join alias expression" when
the Var is an outer reference from a subquery.  The restriction is
unnecessary: every planner caller passes root->parse as the query, so
the Var always belongs to root->parse regardless of its varlevelsup.

To fix, restrict phrels to the rels within the join, and if nothing is
left, evaluate the PHV at the join including its outer-join relid, at
any varlevelsup.  Add an assertion that the query is root->parse.

Author: Richard Guo <guofenglinux@gmail.com>
Discussion: https://postgr.es/m/CAMbWs49q_gvoeOoF0+KZ-DpQaDb=nGFfSgrz2V_n1_64L4ZNVQ@mail.gmail.com
Backpatch-through: 16
This commit is contained in:
Richard Guo
2026-10-07 14:36:34 +09:00
parent 6181300283
commit 5f49c6db11
3 changed files with 350 additions and 10 deletions

View File

@@ -791,6 +791,8 @@ flatten_join_alias_vars(PlannerInfo *root, Query *query, Node *node)
* it's okay to immediately increment sublevels_up.
*/
Assert(node != (Node *) query);
/* add_nullingrels_if_needed relies on this */
Assert(root == NULL || query == root->parse);
context.root = root;
context.query = query;
@@ -1251,21 +1253,23 @@ add_nullingrels_if_needed(PlannerInfo *root, Node *newnode, Var *oldvar)
* We can insert a PlaceHolderVar to carry the nullingrels. However,
* deciding where to evaluate the PHV is slightly tricky. We first
* try to evaluate it at the natural semantic level of the new
* expression; but if that expression is variable-free, fall back to
* evaluating it at the join that the oldvar is an alias Var for.
* expression, ignoring any lateral references to rels outside the
* join; but if that leaves nothing, fall back to evaluating it at the
* join that the oldvar is an alias Var for.
*/
PlaceHolderVar *newphv;
Index levelsup = oldvar->varlevelsup;
Relids phrels = pull_varnos_of_level(root, newnode, levelsup);
Relids joinrelids;
Relids phrels;
if (bms_is_empty(phrels)) /* variable-free? */
/* oldvar belongs to root->parse even when levelsup > 0 */
joinrelids = get_relids_for_join(root->parse, oldvar->varno);
phrels = pull_varnos_of_level(root, newnode, levelsup);
phrels = bms_int_members(phrels, joinrelids);
if (bms_is_empty(phrels))
{
if (levelsup != 0) /* this won't work otherwise */
elog(ERROR, "unsupported join alias expression");
phrels = get_relids_for_join(root->parse, oldvar->varno);
/* If it's an outer join, eval below not above the join */
phrels = bms_del_member(phrels, oldvar->varno);
Assert(!bms_is_empty(phrels));
/* Keep the join's own OJ relid: this set spans both its sides */
phrels = joinrelids;
}
newphv = make_placeholder_expr(root, (Expr *) newnode, phrels);
/* newphv has zero phlevelsup and NULL phnullingrels; fix it */

View File

@@ -3678,6 +3678,262 @@ group by t23 order by 1;
| 2
(6 rows)
-- nulled whole-row Var of a zero-column join, referenced from a subquery
explain (verbose, costs off)
select t1.q1, t1.q2, (select t23::text)
from int8_tbl t1 left join
((select from int4_tbl where f1 = 0) t2
cross join (select from int4_tbl where f1 = 0) t3) t23
on (t1.q1 = 123)
order by 1, 2;
QUERY PLAN
----------------------------------------------------------------
Sort
Output: t1.q1, t1.q2, ((SubPlan expr_1))
Sort Key: t1.q1, t1.q2
-> Nested Loop Left Join
Output: t1.q1, t1.q2, (SubPlan expr_1)
Join Filter: (t1.q1 = 123)
-> Seq Scan on public.int8_tbl t1
Output: t1.q1, t1.q2
-> Materialize
Output: ('()'::record)
-> Nested Loop
Output: '()'::record
-> Seq Scan on public.int4_tbl
Output: int4_tbl.f1
Filter: (int4_tbl.f1 = 0)
-> Seq Scan on public.int4_tbl int4_tbl_1
Output: int4_tbl_1.f1
Filter: (int4_tbl_1.f1 = 0)
SubPlan expr_1
-> Result
Output: (('()'::record))::text
(21 rows)
select t1.q1, t1.q2, (select t23::text)
from int8_tbl t1 left join
((select from int4_tbl where f1 = 0) t2
cross join (select from int4_tbl where f1 = 0) t3) t23
on (t1.q1 = 123)
order by 1, 2;
q1 | q2 | t23
------------------+-------------------+-----
123 | 456 | ()
123 | 4567890123456789 | ()
4567890123456789 | -4567890123456789 |
4567890123456789 | 123 |
4567890123456789 | 4567890123456789 |
(5 rows)
-- nulled whole-row Var of a zero-column outer join
explain (verbose, costs off)
select t1.q1, t1.q2, f
from int8_tbl t1 left join
((select from int4_tbl where f1 = 0) t2
left join (select from int4_tbl where f1 = 0) t3 on true) t23
on (t1.q1 = 123),
length(t23::text) f
order by 1, 2;
QUERY PLAN
----------------------------------------------------------------------
Sort
Output: t1.q1, t1.q2, f.f
Sort Key: t1.q1, t1.q2
-> Nested Loop
Output: t1.q1, t1.q2, f.f
-> Nested Loop Left Join
Output: t1.q1, t1.q2, ('()'::record)
Join Filter: (t1.q1 = 123)
-> Seq Scan on public.int8_tbl t1
Output: t1.q1, t1.q2
-> Materialize
Output: ('()'::record)
-> Nested Loop Left Join
Output: '()'::record
-> Seq Scan on public.int4_tbl
Output: int4_tbl.f1
Filter: (int4_tbl.f1 = 0)
-> Seq Scan on public.int4_tbl int4_tbl_1
Output: int4_tbl_1.f1
Filter: (int4_tbl_1.f1 = 0)
-> Function Scan on pg_catalog.length f
Output: f.f
Function Call: length((('()'::record))::text)
(23 rows)
select t1.q1, t1.q2, f
from int8_tbl t1 left join
((select from int4_tbl where f1 = 0) t2
left join (select from int4_tbl where f1 = 0) t3 on true) t23
on (t1.q1 = 123),
length(t23::text) f
order by 1, 2;
q1 | q2 | f
------------------+-------------------+---
123 | 456 | 2
123 | 4567890123456789 | 2
4567890123456789 | -4567890123456789 |
4567890123456789 | 123 |
4567890123456789 | 4567890123456789 |
(5 rows)
-- nulled whole-row Var of a join with a variable-free merged column
explain (verbose, costs off)
select 1 from (unnest(array[1, (select sum(f1) from int4_tbl)]) as u(b)
right join (values (1)) as v(b) using (b)
full join int8_tbl i8 on true) as j,
length(j::text) f;
QUERY PLAN
---------------------------------------------------------------------------------------
Nested Loop
Output: 1
InitPlan expr_1
-> Aggregate
Output: sum(int4_tbl.f1)
-> Seq Scan on public.int4_tbl
Output: int4_tbl.f1
-> Merge Full Join
Output: ('1'::bigint), i8.q1, i8.q2
-> Nested Loop Left Join
Output: '1'::bigint
-> Result
-> Function Scan on pg_catalog.unnest u
Output: u.b
Function Call: unnest(ARRAY['1'::bigint, (InitPlan expr_1).col1])
Filter: (u.b = 1)
-> Materialize
Output: i8.q1, i8.q2
-> Seq Scan on public.int8_tbl i8
Output: i8.q1, i8.q2
-> Function Scan on pg_catalog.length f
Output: f.f
Function Call: length((ROW(('1'::bigint), i8.q1, i8.q2))::text)
(23 rows)
select 1 from (unnest(array[1, (select sum(f1) from int4_tbl)]) as u(b)
right join (values (1)) as v(b) using (b)
full join int8_tbl i8 on true) as j,
length(j::text) f;
?column?
----------
1
1
1
1
1
(5 rows)
-- nulled whole-row Var of a join containing a lateral reference
explain (verbose, costs off)
select (j is null) from int4_tbl i4
left join (int8_tbl i8 join lateral (select i4.f1 from (values (3)) v) ss(x) on true) as j
on false;
QUERY PLAN
------------------------------------------------
Nested Loop Left Join
Output: ((ROW(i8.q1, i8.q2, i4.f1)) IS NULL)
Join Filter: false
-> Seq Scan on public.int4_tbl i4
Output: i4.f1
-> Result
Output: ROW(i8.q1, i8.q2, i4.f1)
Replaces: Scan on i8
One-Time Filter: false
(9 rows)
select (j is null) from int4_tbl i4
left join (int8_tbl i8 join lateral (select i4.f1 from (values (3)) v) ss(x) on true) as j
on false;
?column?
----------
t
t
t
t
t
(5 rows)
-- same, but the PHV must be passed up through another join
explain (verbose, costs off)
select j from int4_tbl i4
left join (lateral (values (i4.f1)) as v(a) cross join int8_tbl i8) as j
on (j.q1 = 123 and j.q2 = 456),
int4_tbl i4b
where i4.f1 = 0
order by 1;
QUERY PLAN
---------------------------------------------------------------
Sort
Output: (ROW(i4.f1, i8.q1, i8.q2))
Sort Key: (ROW(i4.f1, i8.q1, i8.q2))
-> Nested Loop
Output: (ROW(i4.f1, i8.q1, i8.q2))
-> Nested Loop Left Join
Output: (ROW(i4.f1, i8.q1, i8.q2))
-> Seq Scan on public.int4_tbl i4
Output: i4.f1
Filter: (i4.f1 = 0)
-> Seq Scan on public.int8_tbl i8
Output: ROW(i4.f1, i8.q1, i8.q2)
Filter: ((i8.q1 = 123) AND (i8.q2 = 456))
-> Seq Scan on public.int4_tbl i4b
Output: i4b.f1
(15 rows)
select j from int4_tbl i4
left join (lateral (values (i4.f1)) as v(a) cross join int8_tbl i8) as j
on (j.q1 = 123 and j.q2 = 456),
int4_tbl i4b
where i4.f1 = 0
order by 1;
j
-------------
(0,123,456)
(0,123,456)
(0,123,456)
(0,123,456)
(0,123,456)
(5 rows)
-- same, referenced from a subquery, with only lateral references and constants
explain (verbose, costs off)
select i4.f1, (select j::text)
from int4_tbl i4
left join (lateral (select i4.f1) ss(x) cross join (select 1) s2(y)) j
on (i4.f1 = 0)
order by 1;
QUERY PLAN
-------------------------------------------------
Sort
Output: i4.f1, ((SubPlan expr_1))
Sort Key: i4.f1
-> Nested Loop Left Join
Output: i4.f1, (SubPlan expr_1)
Join Filter: (i4.f1 = 0)
-> Seq Scan on public.int4_tbl i4
Output: i4.f1
-> Result
Output: ROW(i4.f1, 1)
SubPlan expr_1
-> Result
Output: ((ROW(i4.f1, 1)))::text
(13 rows)
select i4.f1, (select j::text)
from int4_tbl i4
left join (lateral (select i4.f1) ss(x) cross join (select 1) s2(y)) j
on (i4.f1 = 0)
order by 1;
f1 | j
-------------+-------
-2147483647 |
-123456 |
0 | (0,1)
123456 |
2147483647 |
(5 rows)
--
-- test incorrect failure to NULL pulled-up subexpressions
--

View File

@@ -1108,6 +1108,86 @@ from int8_tbl t1 left join
on (t1.q2 = t23.q1)
group by t23 order by 1;
-- nulled whole-row Var of a zero-column join, referenced from a subquery
explain (verbose, costs off)
select t1.q1, t1.q2, (select t23::text)
from int8_tbl t1 left join
((select from int4_tbl where f1 = 0) t2
cross join (select from int4_tbl where f1 = 0) t3) t23
on (t1.q1 = 123)
order by 1, 2;
select t1.q1, t1.q2, (select t23::text)
from int8_tbl t1 left join
((select from int4_tbl where f1 = 0) t2
cross join (select from int4_tbl where f1 = 0) t3) t23
on (t1.q1 = 123)
order by 1, 2;
-- nulled whole-row Var of a zero-column outer join
explain (verbose, costs off)
select t1.q1, t1.q2, f
from int8_tbl t1 left join
((select from int4_tbl where f1 = 0) t2
left join (select from int4_tbl where f1 = 0) t3 on true) t23
on (t1.q1 = 123),
length(t23::text) f
order by 1, 2;
select t1.q1, t1.q2, f
from int8_tbl t1 left join
((select from int4_tbl where f1 = 0) t2
left join (select from int4_tbl where f1 = 0) t3 on true) t23
on (t1.q1 = 123),
length(t23::text) f
order by 1, 2;
-- nulled whole-row Var of a join with a variable-free merged column
explain (verbose, costs off)
select 1 from (unnest(array[1, (select sum(f1) from int4_tbl)]) as u(b)
right join (values (1)) as v(b) using (b)
full join int8_tbl i8 on true) as j,
length(j::text) f;
select 1 from (unnest(array[1, (select sum(f1) from int4_tbl)]) as u(b)
right join (values (1)) as v(b) using (b)
full join int8_tbl i8 on true) as j,
length(j::text) f;
-- nulled whole-row Var of a join containing a lateral reference
explain (verbose, costs off)
select (j is null) from int4_tbl i4
left join (int8_tbl i8 join lateral (select i4.f1 from (values (3)) v) ss(x) on true) as j
on false;
select (j is null) from int4_tbl i4
left join (int8_tbl i8 join lateral (select i4.f1 from (values (3)) v) ss(x) on true) as j
on false;
-- same, but the PHV must be passed up through another join
explain (verbose, costs off)
select j from int4_tbl i4
left join (lateral (values (i4.f1)) as v(a) cross join int8_tbl i8) as j
on (j.q1 = 123 and j.q2 = 456),
int4_tbl i4b
where i4.f1 = 0
order by 1;
select j from int4_tbl i4
left join (lateral (values (i4.f1)) as v(a) cross join int8_tbl i8) as j
on (j.q1 = 123 and j.q2 = 456),
int4_tbl i4b
where i4.f1 = 0
order by 1;
-- same, referenced from a subquery, with only lateral references and constants
explain (verbose, costs off)
select i4.f1, (select j::text)
from int4_tbl i4
left join (lateral (select i4.f1) ss(x) cross join (select 1) s2(y)) j
on (i4.f1 = 0)
order by 1;
select i4.f1, (select j::text)
from int4_tbl i4
left join (lateral (select i4.f1) ss(x) cross join (select 1) s2(y)) j
on (i4.f1 = 0)
order by 1;
--
-- test incorrect failure to NULL pulled-up subexpressions
--