From 01809f0e80eca29ef03e0d21d98aa48dc3f1723e Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Wed, 22 Jul 2026 22:47:46 +0200
Subject: [PATCH v7 10/12] Fix handling of filters in add_partial_path

We're allowing partial paths with expected filters, in the hope that the
filter will be realized by a join in the parallel worker (we can't pass
it through Gather yet).

But add_partial_path is expected to sort paths by cost, so that various
places can simply do linitial(rel->partial_pathlist) to get the cheapest
path. For example generate_gather_paths() does that. But those places
don't expect paths with filters, so we must not break the order.

Sort paths by number of filters (and then number of disabled nodes, and
finally cost), so that the first path always has no filters. We know
there's such path, because the filtered paths are in addition to that.
---
 src/backend/optimizer/util/pathnode.c | 27 +++++++++++++++++++++++----
 1 file changed, 23 insertions(+), 4 deletions(-)

diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 8c2ecdbcb8f..26fecc89cd9 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -983,6 +983,17 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path)
 		Path	   *old_path = (Path *) lfirst(p1);
 		bool		remove_old = false; /* unless new proves superior */
 		PathKeysComparison keyscmp;
+		bool		filters_match;
+
+		/*
+		 * Paths carrying different sets of expected Bloom filters serve
+		 * different purposes (each may be consumed by a different parent
+		 * join, or none at all), and their cost/row estimates aren't directly
+		 * comparable.  So if the two paths don't expect the same filters,
+		 * keep both and don't let either dominate the other.
+		 */
+		filters_match = expected_filters_equal(new_path->expected_filters,
+											   old_path->expected_filters);
 
 		/* Compare pathkeys. */
 		keyscmp = compare_pathkeys(new_path->pathkeys, old_path->pathkeys);
@@ -991,8 +1002,11 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path)
 		 * Unless pathkeys are incompatible, see if one of the paths dominates
 		 * the other (both in startup and total cost). It may happen that one
 		 * path has lower startup cost, the other has lower total cost.
+		 *
+		 * Treat expected filters just like pathkeys - if the paths expect
+		 * different filters, they can't dominate each other.
 		 */
-		if (keyscmp != PATHKEYS_DIFFERENT)
+		if (keyscmp != PATHKEYS_DIFFERENT && filters_match)
 		{
 			PathCostComparison costcmp;
 
@@ -1039,10 +1053,15 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path)
 			/*
 			 * new belongs after this old path if it has more disabled nodes
 			 * or if it has the same number of nodes but a greater total cost
+			 *
+			 * Compare the number of filters first, so that the initial path
+			 * has no filters (there always has to be such path).
 			 */
-			if (new_path->disabled_nodes > old_path->disabled_nodes ||
-				(new_path->disabled_nodes == old_path->disabled_nodes &&
-				 new_path->total_cost >= old_path->total_cost))
+			if ((list_length(new_path->expected_filters) > list_length(old_path->expected_filters)) ||
+				((list_length(new_path->expected_filters) == list_length(old_path->expected_filters)) &&
+				 (new_path->disabled_nodes > old_path->disabled_nodes ||
+				  (new_path->disabled_nodes == old_path->disabled_nodes &&
+				   new_path->total_cost >= old_path->total_cost))))
 				insert_at = foreach_current_index(p1) + 1;
 		}
 
-- 
2.55.0

