From cc1bf5e2791f73e950428311e48c203060d19ec8 Mon Sep 17 00:00:00 2001
From: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
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

