From aa4d8f39ee0022797d416da0008c2a28ef8fc92b Mon Sep 17 00:00:00 2001
From: Ayush Tiwari <ayushtiwari.slg01@gmail.com>
Date: Tue, 28 Jul 2026 05:42:13 +0000
Subject: [PATCH v2] 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 IS NOT
NULL" 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 | 21 +++++++++++----------
 src/test/regress/expected/join.out      | 10 ++++++++++
 src/test/regress/sql/join.sql           |  7 +++++++
 3 files changed, 28 insertions(+), 10 deletions(-)

diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 2c696ea0268..ab392ef60fe 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -1033,21 +1033,22 @@ create_gating_plan(PlannerInfo *root, Path *path, Plan *plan,
 							   (Node *) gating_quals, plan);
 
 	/*
-	 * We might have had a trivial Result plan already.  Stacking one Result
-	 * atop another is silly, so if that applies, just discard the input plan.
-	 * (We're assuming its targetlist is uninteresting; it should be either
-	 * the same as the result of build_path_tlist, or a simplified version.
-	 * 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.)
+	 * See if we can reduce down stacked Result nodes to a single node.  This
+	 * is only possible when the nested Result has no subplan and no gating
+	 * qual.  If we do remove the nested Result, we maintain the relids and
+	 * result_type for EXPLAIN.
 	 */
 	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..a7ce08f7263 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -6707,6 +6707,16 @@ select p.* from
    One-Time Filter: false
 (3 rows)
 
+select * from (
+  select c0 from
+    (select null::int as c0 from ((select 1) union all (select 2))) t1
+  full join (select 1) t2 on true
+  where c0 is not null
+) t3 where now() is not null;
+ c0 
+----
+(0 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..836d4b3db07 100644
--- a/src/test/regress/sql/join.sql
+++ b/src/test/regress/sql/join.sql
@@ -2487,6 +2487,13 @@ 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;
 
+select * from (
+  select c0 from
+    (select null::int as c0 from ((select 1) union all (select 2))) t1
+  full join (select 1) t2 on true
+  where c0 is not null
+) t3 where now() is not null;
+
 -- bug 5255: this is not optimizable by join removal
 begin;
 
-- 
2.43.0

