From 10ab2e74bfd8f0149317db5b00e85b83eb1d391a Mon Sep 17 00:00:00 2001
From: William Bernbaum <wbernbaum@dwdev.com>
Date: Wed, 26 Aug 2026 13:44:07 -0700
Subject: [PATCH v2 11/17] Compute aggregates above a pushed-down deduplication

Group the deduplicated rows as another path.  Also split
copy_rel_without_paths() out of build_grouped_rel().
---
 src/backend/optimizer/path/allpaths.c |  6 +++--
 src/backend/optimizer/plan/planner.c  | 37 ++++++++++++++++++++++++--
 src/backend/optimizer/util/relnode.c  | 38 ++++++++++++++++++---------
 src/include/optimizer/pathnode.h      |  1 +
 4 files changed, 66 insertions(+), 16 deletions(-)

diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 7c73a0ab387..53047e6a69e 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -3566,9 +3566,11 @@ generate_grouped_paths(PlannerInfo *root, RelOptInfo *grouped_rel,
 
 	/*
 	 * Determine whether we should consider hash-based implementations of
-	 * grouping.
+	 * grouping.  An ordered aggregate cannot be hashed.  Under eager
+	 * deduplication the aggregates stay above the join.
 	 */
-	Assert(root->numOrderedAggs == 0);
+	Assert(root->numOrderedAggs == 0 ||
+		   root->eager_agg_mode == EAGER_AGG_DEDUP);
 	can_hash = (agg_info->group_clauses != NIL &&
 				grouping_is_hashable(agg_info->group_clauses));
 
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index c8ca9dcbf02..fc8a0d63d40 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -4388,6 +4388,37 @@ create_ordinary_grouping_paths(PlannerInfo *root, RelOptInfo *input_rel,
 							  partially_grouped_rel, agg_costs, gd,
 							  extra);
 
+	/*
+	 * Eager aggregation may have pushed a deduplication below the joins.
+	 * Every aggregate the query computes here ignores how often a row
+	 * arrives, so grouping the deduplicated rows yields the same results as
+	 * grouping the join's rows.  Group them as a path.
+	 */
+	if (root->eager_agg_mode == EAGER_AGG_DEDUP &&
+		input_rel->grouped_rel != NULL &&
+		!IS_DUMMY_REL(input_rel->grouped_rel) &&
+		input_rel->grouped_rel->pathlist != NIL)
+	{
+		RelOptInfo *dedup_rel;
+		ListCell   *lc;
+
+		dedup_rel = copy_rel_without_paths(input_rel);
+
+		foreach(lc, input_rel->grouped_rel->pathlist)
+		{
+			Path	   *path = (Path *) lfirst(lc);
+
+			add_path(dedup_rel,
+					 (Path *) create_projection_path(root, dedup_rel,
+													 path,
+													 input_rel->reltarget));
+		}
+
+		set_cheapest(dedup_rel);
+		add_paths_to_grouping_rel(root, dedup_rel, grouped_rel, NULL,
+								  agg_costs, gd, extra);
+	}
+
 	/* Give a helpful error if we failed to find any implementation */
 	if (grouped_rel->pathlist == NIL)
 		ereport(ERROR,
@@ -7636,9 +7667,11 @@ create_partial_grouping_paths(PlannerInfo *root,
 
 	/*
 	 * Check whether any partially aggregated paths have been generated
-	 * through eager aggregation.
+	 * through eager aggregation.  A deduplication emits final rows, so
+	 * create_ordinary_grouping_paths() groups them instead.
 	 */
-	if (input_rel->grouped_rel &&
+	if (root->eager_agg_mode != EAGER_AGG_DEDUP &&
+		input_rel->grouped_rel &&
 		!IS_DUMMY_REL(input_rel->grouped_rel) &&
 		input_rel->grouped_rel->pathlist != NIL)
 		eager_agg_rel = input_rel->grouped_rel;
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index e9677abc2a5..47a2016b49b 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -500,18 +500,7 @@ build_grouped_rel(PlannerInfo *root, RelOptInfo *rel)
 {
 	RelOptInfo *grouped_rel;
 
-	grouped_rel = makeNode(RelOptInfo);
-	memcpy(grouped_rel, rel, sizeof(RelOptInfo));
-
-	/*
-	 * clear path info
-	 */
-	grouped_rel->pathlist = NIL;
-	grouped_rel->ppilist = NIL;
-	grouped_rel->partial_pathlist = NIL;
-	grouped_rel->cheapest_startup_path = NULL;
-	grouped_rel->cheapest_total_path = NULL;
-	grouped_rel->cheapest_parameterized_paths = NIL;
+	grouped_rel = copy_rel_without_paths(rel);
 
 	/*
 	 * clear partition info
@@ -536,6 +525,31 @@ build_grouped_rel(PlannerInfo *root, RelOptInfo *rel)
 	return grouped_rel;
 }
 
+/*
+ * copy_rel_without_paths
+ *	  Flat copy a relation, leaving the copy's path lists empty.
+ *
+ * The copy shares everything else with the original, so a caller wanting
+ * different size estimates or partitioning must reset those itself.
+ */
+RelOptInfo *
+copy_rel_without_paths(RelOptInfo *rel)
+{
+	RelOptInfo *newrel;
+
+	newrel = makeNode(RelOptInfo);
+	memcpy(newrel, rel, sizeof(RelOptInfo));
+
+	newrel->pathlist = NIL;
+	newrel->ppilist = NIL;
+	newrel->partial_pathlist = NIL;
+	newrel->cheapest_startup_path = NULL;
+	newrel->cheapest_total_path = NULL;
+	newrel->cheapest_parameterized_paths = NIL;
+
+	return newrel;
+}
+
 /*
  * find_base_rel
  *	  Find a base or otherrel relation entry, which must already exist.
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index e8db321f92b..1f5a911c388 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -346,6 +346,7 @@ extern RelOptInfo *build_simple_rel(PlannerInfo *root, int relid,
 									RelOptInfo *parent);
 extern RelOptInfo *build_simple_grouped_rel(PlannerInfo *root, RelOptInfo *rel);
 extern RelOptInfo *build_grouped_rel(PlannerInfo *root, RelOptInfo *rel);
+extern RelOptInfo *copy_rel_without_paths(RelOptInfo *rel);
 extern RelOptInfo *find_base_rel(PlannerInfo *root, int relid);
 extern RelOptInfo *find_base_rel_noerr(PlannerInfo *root, int relid);
 extern RelOptInfo *find_base_rel_ignore_join(PlannerInfo *root, int relid);
