postgres.git / summary / log / commit / refs

commit    691977d37037f30e39cc65f60e327f678f2cebe9
Author:   Richard Guo <rguo@postgresql.org>
Date:     Fri Feb 20 08:57:53 2026 +0000
First released in: 16.13, 17.9, 18.3

    Fix computation of varnullingrels when translating appendrel Var
    
    When adjust_appendrel_attrs translates a Var referencing a parent
    relation into a Var referencing a child relation, it propagates
    varnullingrels from the parent Var to the translated Var.  Previously,
    the code simply overwrote the translated Var's varnullingrels with
    those of the parent.
    
    This was incorrect because the translated Var might already possess
    nonempty varnullingrels.  This happens, for example, when a LATERAL
    subquery within a UNION ALL references a Var from the nullable side of
    an outer join.  In such cases, the translated Var correctly carries
    the outer join's relid in its varnullingrels.  Overwriting these bits
    with the parent Var's set caused the planner to lose track of the fact
    that the Var could be nulled by that outer join.
    
    In the reported case, because the underlying column had a NOT NULL
    constraint, the planner incorrectly deduced that the Var could never
    be NULL and discarded essential IS NOT NULL filters.  This led to
    incorrect query results where NULL rows were returned instead of being
    filtered out.
    
    To fix, use bms_add_members to merge the parent Var's varnullingrels
    into the translated Var's existing set, preserving both sources of
    nullability.
    
    Back-patch to v16.  Although the reported case does not seem to cause
    problems in v16, leaving incorrect varnullingrels in the tree seems
    like a trap for the unwary.
    
    Bug: #19412
    Reported-by: Sergey Shinderuk <s.shinderuk@postgrespro.ru>
    Author: Richard Guo <guofenglinux@gmail.com>
    Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
    Discussion: https://postgr.es/m/19412-1d0318089b86859e@postgresql.org
    Backpatch-through: 16


src/backend/optimizer/util/appendinfo.c | 12 +++++--- src/test/regress/expected/join.out | 51 +++++++++++++++++++++++++++++++++ src/test/regress/sql/join.sql | 24 ++++++++++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) diff --git a/src/backend/optimizer/util/appendinfo.c b/src/backend/optimizer/util/appendinfo.c index 689840d6564..eadecd0bb92 100644 --- a/src/backend/optimizer/util/appendinfo.c +++ b/src/backend/optimizer/util/appendinfo.c @@ -237,8 +237,9 @@ adjust_appendrel_attrs_mutator(Node *node, * You might think we need to adjust var->varnullingrels, but that * shouldn't need any changes. It will contain outer-join relids, * while the transformation we are making affects only baserels. - * Below, we just propagate var->varnullingrels into the translated - * Var. + * Below, we just merge var->varnullingrels into the translated Var. + * (We must merge not just copy: the child Var could have some + * nullingrel bits set already, and we mustn't drop those.) * * If var->varnullingrels isn't empty, and the translation wouldn't be * a Var, we have to fail. One could imagine wrapping the translated @@ -291,8 +292,11 @@ adjust_appendrel_attrs_mutator(Node *node, var->varattno, get_rel_name(appinfo->parent_reloid)); if (IsA(newnode, Var)) { - ((Var *) newnode)->varreturningtype = var->varreturningtype; - ((Var *) newnode)->varnullingrels = var->varnullingrels; + Var *newvar = (Var *) newnode; + + newvar->varreturningtype = var->varreturningtype; + newvar->varnullingrels = bms_add_members(newvar->varnullingrels, + var->varnullingrels); } else { diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 63d3c5d3ac8..072a7347b81 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -4486,6 +4486,57 @@ where ss.x is null; Output: 'bar'::text (12 rows) +-- Test computation of varnullingrels when translating appendrel Var +begin; +create temp table t_append (a int not null, b int); +insert into t_append values (1, 1); +insert into t_append values (2, 3); +explain (verbose, costs off) +select t1.a, s.a from t_append t1 + left join t_append t2 on t1.a = t2.b + join lateral ( + select t1.a as a union all select t2.a as a + ) s on true +where s.a is not null; + QUERY PLAN +--------------------------------------------------- + Nested Loop + Output: t1.a, (t1.a) + -> Merge Left Join + Output: t1.a, t2.a + Merge Cond: (t1.a = t2.b) + -> Sort + Output: t1.a + Sort Key: t1.a + -> Seq Scan on pg_temp.t_append t1 + Output: t1.a + -> Sort + Output: t2.a, t2.b + Sort Key: t2.b + -> Seq Scan on pg_temp.t_append t2 + Output: t2.a, t2.b + -> Append + -> Result + Output: t1.a + -> Result + Output: t2.a + One-Time Filter: (t2.a IS NOT NULL) +(21 rows) + +select t1.a, s.a from t_append t1 + left join t_append t2 on t1.a = t2.b + join lateral ( + select t1.a as a union all select t2.a as a + ) s on true +where s.a is not null; + a | a +---+--- + 1 | 1 + 1 | 1 + 2 | 2 +(3 rows) + +rollback; -- -- test inlining of immutable functions -- diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index 14cbec28766..4acd2512004 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -1508,6 +1508,30 @@ select * from int4_tbl left join ( ) ss(x) on true where ss.x is null; +-- Test computation of varnullingrels when translating appendrel Var +begin; + +create temp table t_append (a int not null, b int); +insert into t_append values (1, 1); +insert into t_append values (2, 3); + +explain (verbose, costs off) +select t1.a, s.a from t_append t1 + left join t_append t2 on t1.a = t2.b + join lateral ( + select t1.a as a union all select t2.a as a + ) s on true +where s.a is not null; + +select t1.a, s.a from t_append t1 + left join t_append t2 on t1.a = t2.b + join lateral ( + select t1.a as a union all select t2.a as a + ) s on true +where s.a is not null; + +rollback; + -- -- test inlining of immutable functions -- [parent: 0dc22fff6471]