From 56d162e5644692828564909bbef214727dec25ca Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Wed, 22 Jul 2026 23:44:22 +0200
Subject: [PATCH v10 10/21] Add assert to prevent "partial" filters

When adding a path (add_path or add_partial_path), make sure none of the
expected filters overlaps with the relids of the relation. Such filters
are invalid, we can't apply them incrementally. We must get all build
relids at once.

Fix a bug in compute_join_expected_filters(), violating this. It failed
to reject filters matching only some of the build relids.
---
 src/backend/optimizer/path/joinpath.c |  9 ++++
 src/backend/optimizer/util/pathnode.c | 42 ++++++++++++++++++
 src/test/regress/expected/join.out    | 64 +++++++++++++++------------
 3 files changed, 86 insertions(+), 29 deletions(-)

diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c
index 82d3a9a2306..d1a1d81fb2d 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -1103,6 +1103,15 @@ compute_join_expected_filters(PlannerInfo *root,
 					goto contradiction;
 				}
 			}
+			else if (bms_overlap(f->build_relids, join_relids))
+			{
+				/*
+				 * We have some relids needed to build the filter, but not all
+				 * of them. We can't apply filters incrementally, either we
+				 * have all the relids in other_relids, or none of them.
+				 */
+				goto contradiction;
+			}
 			else
 			{
 				/*
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index ac8b7ef314c..7cd1ea0130d 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -613,6 +613,27 @@ add_path(RelOptInfo *parent_rel, Path *new_path)
 	 */
 	CHECK_FOR_INTERRUPTS();
 
+	/*
+	 * If the new path expects filters, none of the filters can intersect
+	 * with the rel. Relids of the relation must have no overlap with the
+	 * build relids of the filter.
+	 *
+	 * XXX Do it here, because all paths have to go either through add_path
+	 * or add_partial_path. But maybe there's a better place.
+	 */
+#if USE_ASSERT_CHECKING
+	{
+		ListCell *lc;
+
+		foreach (lc, new_path->expected_filters)
+		{
+			ExpectedFilter *f = (ExpectedFilter *) lfirst(lc);
+
+			Assert(!bms_overlap(f->build_relids, parent_rel->relids));
+		}
+	}
+#endif
+
 	/* Pretend parameterized paths have no pathkeys, per comment above */
 	new_path_pathkeys = new_path->param_info ? NIL : new_path->pathkeys;
 
@@ -974,6 +995,27 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path)
 	/* Relation should be OK for parallelism, too. */
 	Assert(parent_rel->consider_parallel);
 
+	/*
+	 * If the new path expects filters, none of the filters can intersect
+	 * with the rel. Relids of the relation must have no overlap with the
+	 * build relids of the filter.
+	 *
+	 * XXX Do it here, because all paths have to go either through add_path
+	 * or add_partial_path. But maybe there's a better place.
+	 */
+#if USE_ASSERT_CHECKING
+	{
+		ListCell *lc;
+
+		foreach (lc, new_path->expected_filters)
+		{
+			ExpectedFilter *f = (ExpectedFilter *) lfirst(lc);
+
+			Assert(!bms_overlap(f->build_relids, parent_rel->relids));
+		}
+	}
+#endif
+
 	/*
 	 * As in add_path, throw out any paths which are dominated by the new
 	 * path, but throw out the new path if some existing path dominates it.
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 926d68d8b5b..42ab6de25e1 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4777,38 +4777,44 @@ select ss1.d1 from
     on i8.q1 = i4.f1
   on t1.tenthous = ss1.d1
 where t1.unique1 < i4.f1;
-                                                                                                          QUERY PLAN                                                                                                           
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Nested Loop
-   Output: (64)::information_schema.cardinal_number
-   ->  Seq Scan on public.tenk1 t3
-         Output: t3.unique1, t3.unique2, t3.two, t3.four, t3.ten, t3.twenty, t3.hundred, t3.thousand, t3.twothousand, t3.fivethous, t3.tenthous, t3.odd, t3.even, t3.stringu1, t3.stringu2, t3.string4
-         Filter: (t3.fivethous < 0)
-   ->  Nested Loop
-         Output: t1.tenthous, t2.ten
+                                                                                                             QUERY PLAN                                                                                                              
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Hash Join
+   Output: ((64)::information_schema.cardinal_number)
+   Hash Cond: (t2.ten = t1.tenthous)
+   ->  Seq Scan on public.tenk1 t2
+         Output: t2.unique1, t2.unique2, t2.two, t2.four, t2.ten, t2.twenty, t2.hundred, t2.thousand, t2.twothousand, t2.fivethous, t2.tenthous, t2.odd, t2.even, t2.stringu1, t2.stringu2, t2.string4
+         Bloom Filter 1: keys=(t2.ten)
+   ->  Hash
+         Output: t1.tenthous, ((64)::information_schema.cardinal_number)
+         Bloom Filter 1
          ->  Nested Loop
-               Output: t1.tenthous, t2.ten, i4.f1
-               Join Filter: (t1.unique1 < i4.f1)
+               Output: t1.tenthous, (64)::information_schema.cardinal_number
+               Join Filter: (t1.tenthous = ((64)::information_schema.cardinal_number)::integer)
+               ->  Seq Scan on public.tenk1 t3
+                     Output: t3.unique1, t3.unique2, t3.two, t3.four, t3.ten, t3.twenty, t3.hundred, t3.thousand, t3.twothousand, t3.fivethous, t3.tenthous, t3.odd, t3.even, t3.stringu1, t3.stringu2, t3.string4
+                     Filter: (t3.fivethous < 0)
                ->  Nested Loop
-                     Output: t1.tenthous, t1.unique1, t2.ten
-                     Join Filter: (t1.tenthous = ((64)::information_schema.cardinal_number)::integer)
+                     Output: t1.tenthous
                      ->  Nested Loop
-                           Output: t2.ten
-                           Join Filter: (((64)::information_schema.cardinal_number)::integer = t2.ten)
-                           ->  Result
-                                 Output: ((abs(t3.unique1))::double precision + random())
-                           ->  Seq Scan on public.tenk1 t2
-                                 Output: t2.unique1, t2.unique2, t2.two, t2.four, t2.ten, t2.twenty, t2.hundred, t2.thousand, t2.twothousand, t2.fivethous, t2.tenthous, t2.odd, t2.even, t2.stringu1, t2.stringu2, t2.string4
-                     ->  Index Scan using tenk1_thous_tenthous on public.tenk1 t1
-                           Output: t1.unique1, t1.unique2, t1.two, t1.four, t1.ten, t1.twenty, t1.hundred, t1.thousand, t1.twothousand, t1.fivethous, t1.tenthous, t1.odd, t1.even, t1.stringu1, t1.stringu2, t1.string4
-                           Index Cond: (t1.tenthous = t2.ten)
-               ->  Seq Scan on public.int4_tbl i4
-                     Output: i4.f1
-                     Filter: (i4.f1 = ((64)::information_schema.cardinal_number)::integer)
-         ->  Seq Scan on public.int8_tbl i8
-               Output: i8.q1, i8.q2
-               Filter: (i8.q1 = ((64)::information_schema.cardinal_number)::integer)
-(29 rows)
+                           Output: t1.tenthous, i4.f1
+                           Join Filter: (t1.unique1 < i4.f1)
+                           ->  Nested Loop
+                                 Output: t1.tenthous, t1.unique1
+                                 ->  Subquery Scan on ss0
+                                       Output: ss0.x, (64)::information_schema.cardinal_number
+                                       ->  Result
+                                             Output: ((abs(t3.unique1))::double precision + random())
+                                 ->  Index Scan using tenk1_thous_tenthous on public.tenk1 t1
+                                       Output: t1.unique1, t1.unique2, t1.two, t1.four, t1.ten, t1.twenty, t1.hundred, t1.thousand, t1.twothousand, t1.fivethous, t1.tenthous, t1.odd, t1.even, t1.stringu1, t1.stringu2, t1.string4
+                                       Index Cond: (t1.tenthous = (((64)::information_schema.cardinal_number))::integer)
+                           ->  Seq Scan on public.int4_tbl i4
+                                 Output: i4.f1
+                                 Filter: (i4.f1 = ((64)::information_schema.cardinal_number)::integer)
+                     ->  Seq Scan on public.int8_tbl i8
+                           Output: i8.q1, i8.q2
+                           Filter: (i8.q1 = ((64)::information_schema.cardinal_number)::integer)
+(35 rows)
 
 select ss1.d1 from
   tenk1 as t1
-- 
2.50.1 (Apple Git-155)

