From cc1bf5e2791f73e950428311e48c203060d19ec8 Mon Sep 17 00:00:00 2001 From: Ayush Tiwari Date: Mon, 27 Jul 2026 12:27:36 +0000 Subject: [PATCH v1] Fix create_gating_plan() dropping a child Result's one-time filter Commit f2bae51dfd5 refactored create_gating_plan(). When it places a gating Result node atop an existing Result, it tries to avoid stacking two Result nodes by absorbing the child. The refactor began doing this unconditionally for any Result child, whereas the previous code only discarded the child when it was trivial, i.e. it had neither a subplan nor a resconstantqual. As a result, when the child Result carried a one-time filter (resconstantqual), that filter was silently discarded, producing wrong query results. For example, a pseudoconstant qual such as "c0 <> 7" where c0 is a constant NULL becomes a one-time filter on a Result; if another gating qual is applied on top, the original filter was lost and rows that should have been rejected were returned. Restore the original behavior: only absorb the child Result when it is trivial, and otherwise keep it as the subplan so its resconstantqual (or its own subplan) is preserved. This retains the relids/result_type attribution added by f2bae51dfd5 for the trivial case, and respects the invariant that a Result carries relids only when it has no subplan. Add a regression test to join.sql. Bug: #19579 Reported-by: Viktor Leis --- src/backend/optimizer/plan/createplan.c | 15 ++++++++-- src/test/regress/expected/join.out | 37 +++++++++++++++++++++++++ src/test/regress/sql/join.sql | 20 +++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 2c696ea0268..945b7123f49 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -1040,14 +1040,23 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan, * However, we preserve the set of relids that it purports to scan and * attribute that to our replacement Result instead, and likewise for the * result_type.) + * + * We can only do this when the input Result is itself trivial, i.e. it has + * no subplan of its own and no one-time filter. If it carries a + * resconstantqual (or a subplan), that work must be preserved, so we leave + * it in place as our subplan rather than discarding it. */ if (IsA(plan, Result)) { Result *rplan = (Result *) plan; - gplan->plan.lefttree = NULL; - gplan->relids = rplan->relids; - gplan->result_type = rplan->result_type; + if (rplan->plan.lefttree == NULL && + rplan->resconstantqual == NULL) + { + gplan->plan.lefttree = NULL; + gplan->relids = rplan->relids; + gplan->result_type = rplan->result_type; + } } /* diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out index 19e2cca548b..fb8de10ce63 100644 --- a/src/test/regress/expected/join.out +++ b/src/test/regress/expected/join.out @@ -6707,6 +6707,43 @@ select p.* from One-Time Filter: false (3 rows) +-- bug #19579: a gating qual applied on top of a Result that already carries a +-- one-time filter must not discard the lower filter +select * from + (select 0 as c0 from + (select t1.c0 from + (select null::int as c0 from ((select 1) union all (select 2)) t0) t1 + full join (select 1 as c1 where false) t2 on true) t3 + where c0 <> 7) t4 + cross join (select * from (values (0::bigint)) v(x) + where now() is not null) t5; + c0 | x +----+--- +(0 rows) + +explain (costs off) +select * from + (select 0 as c0 from + (select t1.c0 from + (select null::int as c0 from ((select 1) union all (select 2)) t0) t1 + full join (select 1 as c1 where false) t2 on true) t3 + where c0 <> 7) t4 + cross join (select * from (values (0::bigint)) v(x) + where now() is not null) t5; + QUERY PLAN +----------------------------------------------------------- + Result + -> Append + -> Result + One-Time Filter: (now() IS NOT NULL) + -> Result + One-Time Filter: (NULL::integer <> 7) + -> Result + One-Time Filter: (now() IS NOT NULL) + -> Result + One-Time Filter: (NULL::integer <> 7) +(10 rows) + -- bug 5255: this is not optimizable by join removal begin; CREATE TEMP TABLE a (id int PRIMARY KEY); diff --git a/src/test/regress/sql/join.sql b/src/test/regress/sql/join.sql index 85aed7bf704..e1656eb364d 100644 --- a/src/test/regress/sql/join.sql +++ b/src/test/regress/sql/join.sql @@ -2487,6 +2487,26 @@ select p.* from (parent p left join child c on (p.k = c.k)) join parent x on p.k = x.k where p.k = 1 and p.k = 2; +-- bug #19579: a gating qual applied on top of a Result that already carries a +-- one-time filter must not discard the lower filter +select * from + (select 0 as c0 from + (select t1.c0 from + (select null::int as c0 from ((select 1) union all (select 2)) t0) t1 + full join (select 1 as c1 where false) t2 on true) t3 + where c0 <> 7) t4 + cross join (select * from (values (0::bigint)) v(x) + where now() is not null) t5; +explain (costs off) +select * from + (select 0 as c0 from + (select t1.c0 from + (select null::int as c0 from ((select 1) union all (select 2)) t0) t1 + full join (select 1 as c1 where false) t2 on true) t3 + where c0 <> 7) t4 + cross join (select * from (values (0::bigint)) v(x) + where now() is not null) t5; + -- bug 5255: this is not optimizable by join removal begin; -- 2.43.0