From 3c0c827dff2c3e2dfab51daa72c1d226c484ab13 Mon Sep 17 00:00:00 2001
From: David Rowley <dgrowley@gmail.com>
Date: Thu, 30 Jul 2026 14:17:04 +1200
Subject: [PATCH v1] Fix issue with RANGE partition pruning being too
 aggressive

Partition pruning for RANGE-partitioned tables could mistakenly prune
the DEFAULT partition in some cases when it was not valid to do so.
This could lead to result sets not containing rows that should have
matched the query's WHERE clause.

The only known cases where this could happen is when combining pruning
steps from an IS NOT NULL clause with other steps that matched to the
DEFAULT partition.  This could occur due to RANGE partitioned tables
having two separate internal representations for marking if the DEFAULT
partition should be scanned or not.  The IS NOT NULL steps would mark
the "scan_default" boolean, but other steps would mark a bound_offset
bit, which would ultimately translate into also scanning the default
partition.  This could all fail after multiple steps were combined with
a combine intersect operator, as it will intersect the bound_offset bits
and only set scan_default if all pruning steps have that flag set.

Here, we fix this by having the IS NOT NULL pruning result mark the
bound_offsets so that it uses both representations to mark that the
DEFAULT partition must be scanned.

Reported-by: Jacob Brazeal <jacob.brazeal@gmail.com>
Discussion: https://postgr.es/m/CA+COZaDXrfTaBjLE=Z79MTaH6Xun1V4PeKxLvCNv8mXS8wn0rw@mail.gmail.comOA
---
 src/backend/partitioning/partprune.c          | 10 ++--------
 src/test/regress/expected/partition_prune.out | 12 ++++++++++++
 src/test/regress/sql/partition_prune.sql      |  1 +
 3 files changed, 15 insertions(+), 8 deletions(-)

diff --git a/src/backend/partitioning/partprune.c b/src/backend/partitioning/partprune.c
index e7c318bbcac..afe57ac297d 100644
--- a/src/backend/partitioning/partprune.c
+++ b/src/backend/partitioning/partprune.c
@@ -3031,15 +3031,9 @@ get_matching_range_bounds(PartitionPruneContext *context,
 	 */
 	if (nvalues == 0)
 	{
-		/* ignore key space not covered by any partitions */
-		if (partindices[minoff] < 0)
-			minoff++;
-		if (partindices[maxoff] < 0)
-			maxoff--;
-
 		result->scan_default = partition_bound_has_default(boundinfo);
-		Assert(partindices[minoff] >= 0 &&
-			   partindices[maxoff] >= 0);
+		Assert(partindices[minoff] >= -1 &&
+			   partindices[maxoff] >= -1);
 		result->bound_offsets = bms_add_range(NULL, minoff, maxoff);
 
 		return result;
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index 849049f9c51..573bd67e8a4 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -569,6 +569,18 @@ explain (costs off) select * from rlp where a = 1 or b = 'ab';
          Filter: ((a = 1) OR ((b)::text = 'ab'::text))
 (25 rows)
 
+explain (costs off) select * from rlp where  a is not null and a in(9,20,30);
+                                QUERY PLAN                                
+--------------------------------------------------------------------------
+ Append
+   ->  Seq Scan on rlp2 rlp_1
+         Filter: ((a IS NOT NULL) AND (a = ANY ('{9,20,30}'::integer[])))
+   ->  Seq Scan on rlp4_1 rlp_2
+         Filter: ((a IS NOT NULL) AND (a = ANY ('{9,20,30}'::integer[])))
+   ->  Seq Scan on rlp_default_30 rlp_3
+         Filter: ((a IS NOT NULL) AND (a = ANY ('{9,20,30}'::integer[])))
+(7 rows)
+
 explain (costs off) select * from rlp where a > 20 and a < 27;
                QUERY PLAN                
 -----------------------------------------
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index 359a9208056..9a543ccd1ac 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -102,6 +102,7 @@ explain (costs off) select * from rlp where a = 30;	/* only default is scanned *
 explain (costs off) select * from rlp where a <= 31;
 explain (costs off) select * from rlp where a = 1 or a = 7;
 explain (costs off) select * from rlp where a = 1 or b = 'ab';
+explain (costs off) select * from rlp where  a is not null and a in(9,20,30);
 
 explain (costs off) select * from rlp where a > 20 and a < 27;
 explain (costs off) select * from rlp where a = 29;
-- 
2.53.0

