From c532aa1c05285cb589df354692c8310907de5ecd Mon Sep 17 00:00:00 2001
From: William Bernbaum <wbernbaum@dwdev.com>
Date: Wed, 26 Aug 2026 20:46:45 -0700
Subject: [PATCH v1 15/16] Test deduplication where something else needs the
 collapsed rows

Test NULL grouping and join keys, a lateral reference to a column not covered by a grouping key, the query's EXISTS and NOT EXISTS, etc.
---
 src/test/regress/expected/eager_aggregate.out | 359 ++++++++++++++++++
 src/test/regress/sql/eager_aggregate.sql      | 189 +++++++++
 2 files changed, 548 insertions(+)

diff --git a/src/test/regress/expected/eager_aggregate.out b/src/test/regress/expected/eager_aggregate.out
index 5dd43d39f3a..2b2f2d8ecba 100644
--- a/src/test/regress/expected/eager_aggregate.out
+++ b/src/test/regress/expected/eager_aggregate.out
@@ -3196,3 +3196,362 @@ RESET enable_eager_aggregate;
 DROP TABLE eager_minmax_d;
 DROP TABLE eager_minmax_f1;
 DROP TABLE eager_minmax_f2;
+--
+-- Test the deduplication against constructs that read the rows behind it, or
+-- that rule it out
+--
+CREATE TABLE eager_edge_d (id int PRIMARY KEY, name text, k int);
+CREATE TABLE eager_edge_f (id int PRIMARY KEY, d_id int, flag bool);
+-- d 50 groups under a NULL name, 10 rows of f carry a NULL join key, and f
+-- matches nothing above d 90
+INSERT INTO eager_edge_d
+  SELECT i, CASE WHEN i = 50 THEN NULL ELSE 'd' || i % 50 END, i % 5
+    FROM generate_series(1, 100) i;
+INSERT INTO eager_edge_f
+  SELECT i, CASE WHEN i % 200 = 0 THEN NULL ELSE i % 90 + 1 END, i % 2 = 0
+    FROM generate_series(1, 2000) i;
+CREATE INDEX eager_edge_f_d_id ON eager_edge_f (d_id);
+ANALYZE eager_edge_d;
+ANALYZE eager_edge_f;
+-- The deduplication collects the NULL grouping keys into one group, as
+-- DISTINCT does
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.name
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ORDER BY d.name;
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
+ Sort
+   Sort Key: d.name
+   ->  HashAggregate
+         Group Key: d.name
+         ->  Nested Loop Semi Join
+               ->  Seq Scan on eager_edge_d d
+               ->  Index Only Scan using eager_edge_f_d_id on eager_edge_f f
+                     Index Cond: (d_id = d.id)
+(8 rows)
+
+SELECT count(*), count(name) FROM (
+  SELECT DISTINCT d.name
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id) s;
+ count | count 
+-------+-------
+    50 |    49
+(1 row)
+
+SET enable_eager_aggregate TO off;
+SELECT count(*), count(name) FROM (
+  SELECT DISTINCT d.name
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id) s;
+ count | count 
+-------+-------
+    50 |    49
+(1 row)
+
+RESET enable_eager_aggregate;
+-- A NULL join key matches nothing, whether the join produces the matches or
+-- only tests for them
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.id
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ORDER BY d.id;
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
+ Sort
+   Sort Key: d.id
+   ->  HashAggregate
+         Group Key: d.id
+         ->  Nested Loop Semi Join
+               ->  Seq Scan on eager_edge_d d
+               ->  Index Only Scan using eager_edge_f_d_id on eager_edge_f f
+                     Index Cond: (d_id = d.id)
+(8 rows)
+
+SELECT count(*), min(id), max(id) FROM (
+  SELECT DISTINCT d.id
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id) s;
+ count | min | max 
+-------+-----+-----
+    90 |   1 |  90
+(1 row)
+
+SET enable_eager_aggregate TO off;
+SELECT count(*), min(id), max(id) FROM (
+  SELECT DISTINCT d.id
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id) s;
+ count | min | max 
+-------+-----+-----
+    90 |   1 |  90
+(1 row)
+
+RESET enable_eager_aggregate;
+-- A lateral reference reads a column of f that no grouping key covers, so the
+-- rows of f are not interchangeable after all
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.id
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+  JOIN LATERAL (SELECT count(*) AS c FROM eager_edge_f f2
+                 WHERE f2.flag = f.flag) s ON s.c > 0
+ORDER BY d.id;
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
+ Sort
+   Sort Key: d.id
+   ->  HashAggregate
+         Group Key: d.id
+         ->  Hash Join
+               Hash Cond: (f.d_id = d.id)
+               ->  Partial HashAggregate
+                     Group Key: f.d_id
+                     ->  Nested Loop
+                           ->  Partial HashAggregate
+                                 Group Key: f.flag, f.d_id
+                                 ->  Seq Scan on eager_edge_f f
+                           ->  Memoize
+                                 Cache Key: f.flag
+                                 Cache Mode: binary
+                                 ->  Subquery Scan on s
+                                       ->  Aggregate
+                                             Filter: (count(*) > 0)
+                                             ->  Seq Scan on eager_edge_f f2
+                                                   Filter: (flag = f.flag)
+               ->  Hash
+                     ->  Seq Scan on eager_edge_d d
+(22 rows)
+
+SELECT count(*) FROM (
+  SELECT DISTINCT d.id
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id
+    JOIN LATERAL (SELECT count(*) AS c FROM eager_edge_f f2
+                   WHERE f2.flag = f.flag) s ON s.c > 0) s2;
+ count 
+-------
+    90
+(1 row)
+
+-- An existence check the query wrote itself is no obstacle
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.id
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ WHERE EXISTS (SELECT 1 FROM eager_edge_f f2
+                WHERE f2.d_id = d.id AND f2.flag)
+ORDER BY d.id;
+                                       QUERY PLAN                                        
+-----------------------------------------------------------------------------------------
+ Sort
+   Sort Key: d.id
+   ->  HashAggregate
+         Group Key: d.id
+         ->  Nested Loop Semi Join
+               ->  Seq Scan on eager_edge_d d
+               ->  Hash Join
+                     Hash Cond: (f2.d_id = f.d_id)
+                     Join Filter: (d.id = f2.d_id)
+                     ->  HashAggregate
+                           Group Key: f2.d_id
+                           ->  Seq Scan on eager_edge_f f2
+                                 Filter: flag
+                     ->  Hash
+                           ->  Index Only Scan using eager_edge_f_d_id on eager_edge_f f
+                                 Index Cond: (d_id = d.id)
+(16 rows)
+
+SELECT count(*) FROM (
+  SELECT DISTINCT d.id
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id
+   WHERE EXISTS (SELECT 1 FROM eager_edge_f f2
+                  WHERE f2.d_id = d.id AND f2.flag)) s;
+ count 
+-------
+    45
+(1 row)
+
+-- ... nor is its negation
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.id
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ WHERE NOT EXISTS (SELECT 1 FROM eager_edge_f f2
+                    WHERE f2.d_id = d.id AND f2.flag)
+ORDER BY d.id;
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
+ Sort
+   Sort Key: d.id
+   ->  HashAggregate
+         Group Key: d.id
+         ->  Nested Loop
+               ->  Hash Right Anti Join
+                     Hash Cond: (f2.d_id = d.id)
+                     ->  Seq Scan on eager_edge_f f2
+                           Filter: flag
+                     ->  Hash
+                           ->  Seq Scan on eager_edge_d d
+               ->  Index Only Scan using eager_edge_f_d_id on eager_edge_f f
+                     Index Cond: (d_id = d.id)
+(13 rows)
+
+SELECT count(*) FROM (
+  SELECT DISTINCT d.id
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id
+   WHERE NOT EXISTS (SELECT 1 FROM eager_edge_f f2
+                      WHERE f2.d_id = d.id AND f2.flag)) s;
+ count 
+-------
+    45
+(1 row)
+
+-- The primary key determines name, so the grouping clause loses it before
+-- eager aggregation reads it, and name rides along with the group
+EXPLAIN (COSTS OFF)
+SELECT d.id, d.name, max(d.k)
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ GROUP BY d.id, d.name
+ORDER BY d.id;
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
+ GroupAggregate
+   Group Key: d.id
+   ->  Sort
+         Sort Key: d.id
+         ->  Nested Loop Semi Join
+               ->  Seq Scan on eager_edge_d d
+               ->  Index Only Scan using eager_edge_f_d_id on eager_edge_f f
+                     Index Cond: (d_id = d.id)
+(8 rows)
+
+SELECT count(*), count(name), sum(m) FROM (
+  SELECT d.id, d.name, max(d.k) AS m
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id
+   GROUP BY d.id, d.name) s;
+ count | count | sum 
+-------+-------+-----
+    90 |    89 | 180
+(1 row)
+
+SET enable_eager_aggregate TO off;
+SELECT count(*), count(name), sum(m) FROM (
+  SELECT d.id, d.name, max(d.k) AS m
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id
+   GROUP BY d.id, d.name) s;
+ count | count | sum 
+-------+-------+-----
+    90 |    89 | 180
+(1 row)
+
+RESET enable_eager_aggregate;
+-- Grouping sets ask for several groupings at once, which is not supported
+EXPLAIN (COSTS OFF)
+SELECT d.id, d.k, max(d.k)
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ GROUP BY GROUPING SETS ((d.id), (d.k));
+                  QUERY PLAN                  
+----------------------------------------------
+ HashAggregate
+   Hash Key: d.id
+   Hash Key: d.k
+   ->  Hash Join
+         Hash Cond: (f.d_id = d.id)
+         ->  Seq Scan on eager_edge_f f
+         ->  Hash
+               ->  Seq Scan on eager_edge_d d
+(8 rows)
+
+-- A set-returning function in the target list is not supported either
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.id, generate_series(1, 2)
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id;
+                     QUERY PLAN                     
+----------------------------------------------------
+ HashAggregate
+   Group Key: d.id, generate_series(1, 2)
+   ->  ProjectSet
+         ->  Hash Join
+               Hash Cond: (f.d_id = d.id)
+               ->  Seq Scan on eager_edge_f f
+               ->  Hash
+                     ->  Seq Scan on eager_edge_d d
+(8 rows)
+
+-- DISTINCT ON keeps a particular row of each group, but a grouping clause
+-- beside it still supplies the keys
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT ON (d.id) d.id
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ GROUP BY d.id
+ORDER BY d.id;
+                                       QUERY PLAN                                        
+-----------------------------------------------------------------------------------------
+ Unique
+   ->  Group
+         Group Key: d.id
+         ->  Partial GroupAggregate
+               Group Key: d.id
+               ->  Sort
+                     Sort Key: d.id
+                     ->  Nested Loop Semi Join
+                           ->  Seq Scan on eager_edge_d d
+                           ->  Index Only Scan using eager_edge_f_d_id on eager_edge_f f
+                                 Index Cond: (d_id = d.id)
+(11 rows)
+
+-- A parallel plan deduplicates in each worker without a pushdown, and reaches
+-- the same rows
+SET parallel_setup_cost=0;
+SET parallel_tuple_cost=0;
+SET min_parallel_table_scan_size=0;
+SET max_parallel_workers_per_gather=4;
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.id
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ORDER BY d.id;
+                               QUERY PLAN                                
+-------------------------------------------------------------------------
+ Sort
+   Sort Key: d.id
+   ->  HashAggregate
+         Group Key: d.id
+         ->  Gather
+               Workers Planned: 3
+               ->  HashAggregate
+                     Group Key: d.id
+                     ->  Parallel Hash Join
+                           Hash Cond: (f.d_id = d.id)
+                           ->  Parallel Seq Scan on eager_edge_f f
+                           ->  Parallel Hash
+                                 ->  Parallel Seq Scan on eager_edge_d d
+(13 rows)
+
+SELECT count(*) FROM (
+  SELECT DISTINCT d.id
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id) s;
+ count 
+-------
+    90
+(1 row)
+
+RESET parallel_setup_cost;
+RESET parallel_tuple_cost;
+RESET min_parallel_table_scan_size;
+RESET max_parallel_workers_per_gather;
+DROP TABLE eager_edge_d;
+DROP TABLE eager_edge_f;
diff --git a/src/test/regress/sql/eager_aggregate.sql b/src/test/regress/sql/eager_aggregate.sql
index f929a2a4db0..632e5dff995 100644
--- a/src/test/regress/sql/eager_aggregate.sql
+++ b/src/test/regress/sql/eager_aggregate.sql
@@ -1060,3 +1060,192 @@ RESET enable_eager_aggregate;
 DROP TABLE eager_minmax_d;
 DROP TABLE eager_minmax_f1;
 DROP TABLE eager_minmax_f2;
+
+
+--
+-- Test the deduplication against constructs that read the rows behind it, or
+-- that rule it out
+--
+
+CREATE TABLE eager_edge_d (id int PRIMARY KEY, name text, k int);
+CREATE TABLE eager_edge_f (id int PRIMARY KEY, d_id int, flag bool);
+
+-- d 50 groups under a NULL name, 10 rows of f carry a NULL join key, and f
+-- matches nothing above d 90
+INSERT INTO eager_edge_d
+  SELECT i, CASE WHEN i = 50 THEN NULL ELSE 'd' || i % 50 END, i % 5
+    FROM generate_series(1, 100) i;
+INSERT INTO eager_edge_f
+  SELECT i, CASE WHEN i % 200 = 0 THEN NULL ELSE i % 90 + 1 END, i % 2 = 0
+    FROM generate_series(1, 2000) i;
+
+CREATE INDEX eager_edge_f_d_id ON eager_edge_f (d_id);
+
+ANALYZE eager_edge_d;
+ANALYZE eager_edge_f;
+
+-- The deduplication collects the NULL grouping keys into one group, as
+-- DISTINCT does
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.name
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ORDER BY d.name;
+
+SELECT count(*), count(name) FROM (
+  SELECT DISTINCT d.name
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id) s;
+
+SET enable_eager_aggregate TO off;
+
+SELECT count(*), count(name) FROM (
+  SELECT DISTINCT d.name
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id) s;
+
+RESET enable_eager_aggregate;
+
+-- A NULL join key matches nothing, whether the join produces the matches or
+-- only tests for them
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.id
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ORDER BY d.id;
+
+SELECT count(*), min(id), max(id) FROM (
+  SELECT DISTINCT d.id
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id) s;
+
+SET enable_eager_aggregate TO off;
+
+SELECT count(*), min(id), max(id) FROM (
+  SELECT DISTINCT d.id
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id) s;
+
+RESET enable_eager_aggregate;
+
+-- A lateral reference reads a column of f that no grouping key covers, so the
+-- rows of f are not interchangeable after all
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.id
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+  JOIN LATERAL (SELECT count(*) AS c FROM eager_edge_f f2
+                 WHERE f2.flag = f.flag) s ON s.c > 0
+ORDER BY d.id;
+
+SELECT count(*) FROM (
+  SELECT DISTINCT d.id
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id
+    JOIN LATERAL (SELECT count(*) AS c FROM eager_edge_f f2
+                   WHERE f2.flag = f.flag) s ON s.c > 0) s2;
+
+-- An existence check the query wrote itself is no obstacle
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.id
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ WHERE EXISTS (SELECT 1 FROM eager_edge_f f2
+                WHERE f2.d_id = d.id AND f2.flag)
+ORDER BY d.id;
+
+SELECT count(*) FROM (
+  SELECT DISTINCT d.id
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id
+   WHERE EXISTS (SELECT 1 FROM eager_edge_f f2
+                  WHERE f2.d_id = d.id AND f2.flag)) s;
+
+-- ... nor is its negation
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.id
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ WHERE NOT EXISTS (SELECT 1 FROM eager_edge_f f2
+                    WHERE f2.d_id = d.id AND f2.flag)
+ORDER BY d.id;
+
+SELECT count(*) FROM (
+  SELECT DISTINCT d.id
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id
+   WHERE NOT EXISTS (SELECT 1 FROM eager_edge_f f2
+                      WHERE f2.d_id = d.id AND f2.flag)) s;
+
+-- The primary key determines name, so the grouping clause loses it before
+-- eager aggregation reads it, and name rides along with the group
+EXPLAIN (COSTS OFF)
+SELECT d.id, d.name, max(d.k)
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ GROUP BY d.id, d.name
+ORDER BY d.id;
+
+SELECT count(*), count(name), sum(m) FROM (
+  SELECT d.id, d.name, max(d.k) AS m
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id
+   GROUP BY d.id, d.name) s;
+
+SET enable_eager_aggregate TO off;
+
+SELECT count(*), count(name), sum(m) FROM (
+  SELECT d.id, d.name, max(d.k) AS m
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id
+   GROUP BY d.id, d.name) s;
+
+RESET enable_eager_aggregate;
+
+-- Grouping sets ask for several groupings at once, which is not supported
+EXPLAIN (COSTS OFF)
+SELECT d.id, d.k, max(d.k)
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ GROUP BY GROUPING SETS ((d.id), (d.k));
+
+-- A set-returning function in the target list is not supported either
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.id, generate_series(1, 2)
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id;
+
+-- DISTINCT ON keeps a particular row of each group, but a grouping clause
+-- beside it still supplies the keys
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT ON (d.id) d.id
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ GROUP BY d.id
+ORDER BY d.id;
+
+-- A parallel plan deduplicates in each worker without a pushdown, and reaches
+-- the same rows
+SET parallel_setup_cost=0;
+SET parallel_tuple_cost=0;
+SET min_parallel_table_scan_size=0;
+SET max_parallel_workers_per_gather=4;
+
+EXPLAIN (COSTS OFF)
+SELECT DISTINCT d.id
+  FROM eager_edge_d d
+  JOIN eager_edge_f f ON f.d_id = d.id
+ORDER BY d.id;
+
+SELECT count(*) FROM (
+  SELECT DISTINCT d.id
+    FROM eager_edge_d d
+    JOIN eager_edge_f f ON f.d_id = d.id) s;
+
+RESET parallel_setup_cost;
+RESET parallel_tuple_cost;
+RESET min_parallel_table_scan_size;
+RESET max_parallel_workers_per_gather;
+
+DROP TABLE eager_edge_d;
+DROP TABLE eager_edge_f;
