From a524cb70d74fc88aee6e0f2f43c47ee15e9c3ba2 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Wed, 22 Jul 2026 23:44:22 +0200
Subject: [PATCH v8 12/20] 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 b661a5b800f..40f762ccc19 100644
--- a/src/backend/optimizer/path/joinpath.c
+++ b/src/backend/optimizer/path/joinpath.c
@@ -1088,6 +1088,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 26fecc89cd9..2e7c9c8bb18 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 00fb9efc69d..d7b8412320d 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -4110,38 +4110,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.55.0

