From a94f966c0f8b6e980036eb60ea90c39f1fb1e300 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Mon, 27 Jul 2026 15:27:39 +0200
Subject: [PATCH v10 14/21] Add basic regression tests for filter pushdown

Adds three test suites specifically for Bloom filter pushdown, for
basic schemas - two relations, starjoin and snowflake.

The filter pushdown affects plans in a number of existing test suites,
in an accidental way. These tests are focused on filter pushdown,
generating plans with known plan.
---
 src/test/regress/expected/hashjoin_bloom.out  |  65 +++
 .../expected/hashjoin_bloom_snowflake.out     | 182 --------
 .../regress/expected/hashjoin_bloom_star.out  | 410 ++++++++++++++++++
 src/test/regress/parallel_schedule            |   3 +
 src/test/regress/sql/hashjoin_bloom.sql       |  43 ++
 .../regress/sql/hashjoin_bloom_snowflake.sql  | 142 ++++++
 src/test/regress/sql/hashjoin_bloom_star.sql  | 150 +++++++
 7 files changed, 813 insertions(+), 182 deletions(-)
 create mode 100644 src/test/regress/expected/hashjoin_bloom.out
 create mode 100644 src/test/regress/expected/hashjoin_bloom_star.out
 create mode 100644 src/test/regress/sql/hashjoin_bloom.sql
 create mode 100644 src/test/regress/sql/hashjoin_bloom_snowflake.sql
 create mode 100644 src/test/regress/sql/hashjoin_bloom_star.sql

diff --git a/src/test/regress/expected/hashjoin_bloom.out b/src/test/regress/expected/hashjoin_bloom.out
new file mode 100644
index 00000000000..aa967d76063
--- /dev/null
+++ b/src/test/regress/expected/hashjoin_bloom.out
@@ -0,0 +1,65 @@
+-- tests to validate bloom filter pushdown
+-- no parallel query support for now
+SET max_parallel_workers_per_gather = 0;
+-- stabilize estimates
+SET default_statistics_target = 10000;
+-- a couple very simple join queries
+CREATE TABLE bloom_simple_dim (id int, r real);
+CREATE TABLE bloom_simple_fact (id int, padding text);
+INSERT INTO bloom_simple_dim SELECT i, random() FROM generate_series(1,1000) s(i);
+INSERT INTO bloom_simple_fact
+SELECT
+    1 + mod(i, 1000),
+    md5(i::text)
+FROM generate_series(1,100000) s(i);
+VACUUM ANALYZE;
+-- simple query, no join is selective enough for a filter
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_simple_fact f
+JOIN bloom_simple_dim d ON (f.id = d.id);
+                 QUERY PLAN                 
+--------------------------------------------
+ Hash Join
+   Hash Cond: (f.id = d.id)
+   ->  Seq Scan on bloom_simple_fact f
+   ->  Hash
+         ->  Seq Scan on bloom_simple_dim d
+(5 rows)
+
+-- join is 75% selective (not enough for a filter to be created)
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_simple_fact f
+JOIN bloom_simple_dim d ON (f.id = d.id)
+WHERE d.r < 0.75;
+                      QUERY PLAN                      
+------------------------------------------------------
+ Hash Join
+   Hash Cond: (f.id = d.id)
+   ->  Seq Scan on bloom_simple_fact f
+   ->  Hash
+         ->  Seq Scan on bloom_simple_dim d
+               Filter: (r < '0.75'::double precision)
+(6 rows)
+
+-- join is 50% selective (enough for a filter to be pushed down)
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_simple_fact f
+JOIN bloom_simple_dim d ON (f.id = d.id)
+WHERE d.r < 0.5;
+                     QUERY PLAN                      
+-----------------------------------------------------
+ Hash Join
+   Hash Cond: (f.id = d.id)
+   ->  Seq Scan on bloom_simple_fact f
+         Bloom Filter 1: keys=(id)
+   ->  Hash
+         Bloom Filter 1
+         ->  Seq Scan on bloom_simple_dim d
+               Filter: (r < '0.5'::double precision)
+(8 rows)
+
+DROP TABLE bloom_simple_dim;
+DROP TABLE bloom_simple_fact;
diff --git a/src/test/regress/expected/hashjoin_bloom_snowflake.out b/src/test/regress/expected/hashjoin_bloom_snowflake.out
index d953b247cd2..21449cfe7b0 100644
--- a/src/test/regress/expected/hashjoin_bloom_snowflake.out
+++ b/src/test/regress/expected/hashjoin_bloom_snowflake.out
@@ -374,185 +374,3 @@ DROP TABLE bloom_snowflake_dim_2_1;
 DROP TABLE bloom_snowflake_dim_2_2;
 DROP TABLE bloom_snowflake_dim_2;
 DROP TABLE bloom_snowflake_fact;
--- snowflake queries on multi-column FK joins
-CREATE TABLE bloom_snowflake_multi_dim_1_1 (a int, b int, r real, primary key (a, b));
-CREATE TABLE bloom_snowflake_multi_dim_1_2 (a int, b int, r real, primary key (a, b));
-CREATE TABLE bloom_snowflake_multi_dim_1 (
-	a int, b int,
-	id11a int, id11b int,
-	id12a int, id12b int,
-	r real,
-	foreign key (id11a, id11b) references bloom_snowflake_multi_dim_1_1(a, b),
-	foreign key (id12a, id12b) references bloom_snowflake_multi_dim_1_2(a, b),
-	primary key (a, b));
-CREATE TABLE bloom_snowflake_multi_dim_2_1 (a int, b int, r real, primary key (a, b));
-CREATE TABLE bloom_snowflake_multi_dim_2_2 (a int, b int, r real, primary key (a, b));
-CREATE TABLE bloom_snowflake_multi_dim_2 (
-	a int, b int,
-	id21a int, id21b int,
-	id22a int, id22b int,
-	r real,
-	foreign key (id21a, id21b) references bloom_snowflake_multi_dim_2_1(a, b),
-	foreign key (id22a, id22b) references bloom_snowflake_multi_dim_2_2(a, b),
-	primary key (a, b));
-CREATE TABLE bloom_snowflake_multi_fact (
-	id1a int, id1b int,
-	id2a int, id2b int,
-	padding text,
-	foreign key (id1a, id1b) references bloom_snowflake_multi_dim_1(a, b),
-	foreign key (id2a, id2b) references bloom_snowflake_multi_dim_2(a, b));
-SELECT setseed(0.5);
- setseed 
----------
- 
-(1 row)
-
-INSERT INTO bloom_snowflake_multi_dim_1_1 SELECT i, i, random() FROM generate_series(1, 1000) s(i);
-INSERT INTO bloom_snowflake_multi_dim_1_2 SELECT i, i, random() FROM generate_series(1, 1000) s(i);
-WITH d AS (SELECT i, 1 + mod((100000 * random())::int, 1000) AS a, 1 + mod((100000 * random())::int, 1000) AS b FROM generate_series(1, 1000) s(i))
-INSERT INTO bloom_snowflake_multi_dim_1 SELECT i, i, a, a, b, b, random() FROM d;
-INSERT INTO bloom_snowflake_multi_dim_2_1 SELECT i, i, random() FROM generate_series(1, 10000) s(i);
-INSERT INTO bloom_snowflake_multi_dim_2_2 SELECT i, i, random() FROM generate_series(1, 10000) s(i);
-WITH d AS (SELECT i, 1 + mod((100000 * random())::int, 1000) AS a, 1 + mod((100000 * random())::int, 1000) AS b FROM generate_series(1, 1000) s(i))
-INSERT INTO bloom_snowflake_multi_dim_2 SELECT i, i, a, a, b, b, random() FROM d;
-WITH d AS (SELECT 1 + mod((100000 * random())::int, 1000) AS a, 1 + mod((100000 * random())::int, 1000) AS b, md5(i::text) AS p FROM generate_series(1, 100000) AS s(i))
-INSERT INTO bloom_snowflake_multi_fact
-SELECT
-    a, a, b, b, p
-FROM d;
-VACUUM ANALYZE;
--- increase the accepted build size (includes the fact)
-SET bloom_filter_pushdown_max_build_relids = 4;
-EXPLAIN (ANALYZE, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
-SELECT *
-FROM bloom_snowflake_multi_fact f
-JOIN bloom_snowflake_multi_dim_1 d1 ON (f.id1a = d1.a AND f.id1b = d1.b)
-JOIN bloom_snowflake_multi_dim_1_1 d11 ON (d1.id11a = d11.a AND d1.id11b = d11.b)
-JOIN bloom_snowflake_multi_dim_1_2 d12 ON (d1.id12a = d12.a AND d1.id12b = d12.b)
-WHERE d11.r < 0.45 AND d12.r < 0.55;
-                                                                   QUERY PLAN                                                                   
-------------------------------------------------------------------------------------------------------------------------------------------------
- Hash Join  (cost=78.07..2418.84 rows=24628 width=101) (actual rows=23530.00 loops=1)
-   Hash Cond: ((f.id1a = d1.a) AND (f.id1b = d1.b))
-   ->  Seq Scan on bloom_snowflake_multi_fact f  (cost=0.00..2156.00 rows=24628 width=49) (actual rows=23531.00 loops=1)
-         Bloom Filter 3: keys=(id1a, id1b) expected=24.6% checked=99999 rejected=76469 (76.5%)
-   ->  Hash  (cost=74.38..74.38 rows=246 width=52) (actual rows=234.00 loops=1)
-         Buckets: 1024  Batches: 1  Memory Usage: 28kB
-         Bloom Filter 3: bits=8192 hashes=4 memory=1kB checked=99999 rejected=76469
-         ->  Hash Join  (cost=52.00..74.38 rows=246 width=52) (actual rows=234.00 loops=1)
-               Hash Cond: ((d1.id12a = d12.a) AND (d1.id12b = d12.b))
-               ->  Hash Join  (cost=25.09..46.18 rows=246 width=40) (actual rows=235.00 loops=1)
-                     Hash Cond: ((d1.id11a = d11.a) AND (d1.id11b = d11.b))
-                     ->  Seq Scan on bloom_snowflake_multi_dim_1 d1  (cost=0.00..19.80 rows=246 width=28) (actual rows=236.00 loops=1)
-                           Bloom Filter 1: keys=(id11a, id11b) expected=43.9% checked=996 rejected=569 (57.1%)
-                           Bloom Filter 2: keys=(id12a, id12b) expected=56.1% checked=431 rejected=195 (45.2%)
-                     ->  Hash  (cost=18.50..18.50 rows=439 width=12) (actual rows=440.00 loops=1)
-                           Buckets: 1024  Batches: 1  Memory Usage: 27kB
-                           Bloom Filter 1: bits=8192 hashes=4 memory=1kB checked=996 rejected=569
-                           ->  Seq Scan on bloom_snowflake_multi_dim_1_1 d11  (cost=0.00..18.50 rows=439 width=12) (actual rows=440.00 loops=1)
-                                 Filter: (r < '0.45'::double precision)
-                                 Rows Removed by Filter: 560
-               ->  Hash  (cost=18.50..18.50 rows=561 width=12) (actual rows=562.00 loops=1)
-                     Buckets: 1024  Batches: 1  Memory Usage: 33kB
-                     Bloom Filter 2: bits=8192 hashes=4 memory=1kB checked=431 rejected=195
-                     ->  Seq Scan on bloom_snowflake_multi_dim_1_2 d12  (cost=0.00..18.50 rows=561 width=12) (actual rows=562.00 loops=1)
-                           Filter: (r < '0.55'::double precision)
-                           Rows Removed by Filter: 438
-(26 rows)
-
-EXPLAIN (ANALYZE, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
-SELECT *
-FROM bloom_snowflake_multi_fact f
-JOIN bloom_snowflake_multi_dim_1 d1 ON (f.id1a = d1.a AND f.id1b = d1.b)
-JOIN bloom_snowflake_multi_dim_1_1 d11 ON (d1.id11a = d11.a AND d1.id11b = d11.b)
-JOIN bloom_snowflake_multi_dim_1_2 d12 ON (d1.id12a = d12.a AND d1.id12b = d12.b)
-WHERE d11.r < 0.75 AND d12.r < 0.75;
-                                                                   QUERY PLAN                                                                   
-------------------------------------------------------------------------------------------------------------------------------------------------
- Hash Join  (cost=94.81..2667.85 rows=55564 width=101) (actual rows=55598.00 loops=1)
-   Hash Cond: ((f.id1a = d1.a) AND (f.id1b = d1.b))
-   ->  Seq Scan on bloom_snowflake_multi_fact f  (cost=0.00..2156.00 rows=55564 width=49) (actual rows=55694.00 loops=1)
-         Bloom Filter 1: keys=(id1a, id1b) expected=55.6% checked=99999 rejected=44306 (44.3%)
-   ->  Hash  (cost=86.47..86.47 rows=556 width=52) (actual rows=555.00 loops=1)
-         Buckets: 1024  Batches: 1  Memory Usage: 54kB
-         Bloom Filter 1: bits=8192 hashes=4 memory=1kB checked=99999 rejected=44306
-         ->  Hash Join  (cost=59.36..86.47 rows=556 width=52) (actual rows=555.00 loops=1)
-               Hash Cond: ((d1.id12a = d12.a) AND (d1.id12b = d12.b))
-               ->  Hash Join  (cost=29.51..52.76 rows=734 width=40) (actual rows=737.00 loops=1)
-                     Hash Cond: ((d1.id11a = d11.a) AND (d1.id11b = d11.b))
-                     ->  Seq Scan on bloom_snowflake_multi_dim_1 d1  (cost=0.00..18.00 rows=1000 width=28) (actual rows=1000.00 loops=1)
-                     ->  Hash  (cost=18.50..18.50 rows=734 width=12) (actual rows=735.00 loops=1)
-                           Buckets: 1024  Batches: 1  Memory Usage: 40kB
-                           ->  Seq Scan on bloom_snowflake_multi_dim_1_1 d11  (cost=0.00..18.50 rows=734 width=12) (actual rows=735.00 loops=1)
-                                 Filter: (r < '0.75'::double precision)
-                                 Rows Removed by Filter: 265
-               ->  Hash  (cost=18.50..18.50 rows=757 width=12) (actual rows=758.00 loops=1)
-                     Buckets: 1024  Batches: 1  Memory Usage: 41kB
-                     ->  Seq Scan on bloom_snowflake_multi_dim_1_2 d12  (cost=0.00..18.50 rows=757 width=12) (actual rows=758.00 loops=1)
-                           Filter: (r < '0.75'::double precision)
-                           Rows Removed by Filter: 242
-(22 rows)
-
-EXPLAIN (ANALYZE, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
-SELECT *
-FROM bloom_snowflake_multi_fact f
-JOIN bloom_snowflake_multi_dim_1 d1 ON (f.id1a = d1.a AND f.id1b = d1.b)
-JOIN bloom_snowflake_multi_dim_1_1 d11 ON (d1.id11a = d11.a AND d1.id11b = d11.b)
-JOIN bloom_snowflake_multi_dim_1_2 d12 ON (d1.id12a = d12.a AND d1.id12b = d12.b)
-JOIN bloom_snowflake_multi_dim_2 d2 ON (f.id2a = d2.a AND f.id2b = d2.b)
-JOIN bloom_snowflake_multi_dim_2_1 d21 ON (d2.id21a = d21.a AND d2.id21b = d21.b)
-JOIN bloom_snowflake_multi_dim_2_2 d22 ON (d2.id22a = d22.a AND d2.id22b = d22.b)
-WHERE d11.r < 0.75 AND d12.r < 0.75;
-                                                                                             QUERY PLAN                                                                                              
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Hash Join  (cost=331.90..3322.23 rows=55564 width=153) (actual rows=55598.00 loops=1)
-   Hash Cond: ((f.id1a = d1.a) AND (f.id1b = d1.b))
-   ->  Hash Join  (cost=237.09..2810.38 rows=55564 width=101) (actual rows=55694.00 loops=1)
-         Hash Cond: ((f.id2a = d2.a) AND (f.id2b = d2.b))
-         ->  Seq Scan on bloom_snowflake_multi_fact f  (cost=0.00..2156.00 rows=55564 width=49) (actual rows=55694.00 loops=1)
-               Bloom Filter 1: keys=(id1a, id1b) expected=55.6% checked=100000 rejected=44306 (44.3%)
-         ->  Hash  (cost=222.09..222.09 rows=1000 width=52) (actual rows=1000.00 loops=1)
-               Buckets: 1024  Batches: 1  Memory Usage: 91kB
-               ->  Merge Join  (cost=170.14..222.09 rows=1000 width=52) (actual rows=1000.00 loops=1)
-                     Merge Cond: ((d22.a = d2.id22a) AND (d22.b = d2.id22b))
-                     ->  Index Scan using bloom_snowflake_multi_dim_2_2_pkey on bloom_snowflake_multi_dim_2_2 d22  (cost=0.29..395.16 rows=10000 width=12) (actual rows=1001.00 loops=1)
-                           Index Searches: 1
-                     ->  Sort  (cost=169.85..172.35 rows=1000 width=40) (actual rows=1000.00 loops=1)
-                           Sort Key: d2.id22a, d2.id22b
-                           Sort Method: quicksort  Memory: 79kB
-                           ->  Merge Join  (cost=68.11..120.02 rows=1000 width=40) (actual rows=1000.00 loops=1)
-                                 Merge Cond: ((d21.a = d2.id21a) AND (d21.b = d2.id21b))
-                                 ->  Index Scan using bloom_snowflake_multi_dim_2_1_pkey on bloom_snowflake_multi_dim_2_1 d21  (cost=0.29..395.16 rows=10000 width=12) (actual rows=1000.00 loops=1)
-                                       Index Searches: 1
-                                 ->  Sort  (cost=67.83..70.33 rows=1000 width=28) (actual rows=1000.00 loops=1)
-                                       Sort Key: d2.id21a, d2.id21b
-                                       Sort Method: quicksort  Memory: 71kB
-                                       ->  Seq Scan on bloom_snowflake_multi_dim_2 d2  (cost=0.00..18.00 rows=1000 width=28) (actual rows=1000.00 loops=1)
-   ->  Hash  (cost=86.47..86.47 rows=556 width=52) (actual rows=555.00 loops=1)
-         Buckets: 1024  Batches: 1  Memory Usage: 54kB
-         Bloom Filter 1: bits=8192 hashes=4 memory=1kB checked=100000 rejected=44306
-         ->  Hash Join  (cost=59.36..86.47 rows=556 width=52) (actual rows=555.00 loops=1)
-               Hash Cond: ((d1.id12a = d12.a) AND (d1.id12b = d12.b))
-               ->  Hash Join  (cost=29.51..52.76 rows=734 width=40) (actual rows=737.00 loops=1)
-                     Hash Cond: ((d1.id11a = d11.a) AND (d1.id11b = d11.b))
-                     ->  Seq Scan on bloom_snowflake_multi_dim_1 d1  (cost=0.00..18.00 rows=1000 width=28) (actual rows=1000.00 loops=1)
-                     ->  Hash  (cost=18.50..18.50 rows=734 width=12) (actual rows=735.00 loops=1)
-                           Buckets: 1024  Batches: 1  Memory Usage: 40kB
-                           ->  Seq Scan on bloom_snowflake_multi_dim_1_1 d11  (cost=0.00..18.50 rows=734 width=12) (actual rows=735.00 loops=1)
-                                 Filter: (r < '0.75'::double precision)
-                                 Rows Removed by Filter: 265
-               ->  Hash  (cost=18.50..18.50 rows=757 width=12) (actual rows=758.00 loops=1)
-                     Buckets: 1024  Batches: 1  Memory Usage: 41kB
-                     ->  Seq Scan on bloom_snowflake_multi_dim_1_2 d12  (cost=0.00..18.50 rows=757 width=12) (actual rows=758.00 loops=1)
-                           Filter: (r < '0.75'::double precision)
-                           Rows Removed by Filter: 242
-(41 rows)
-
-RESET bloom_filter_pushdown_max_build_relids;
-DROP TABLE bloom_snowflake_multi_fact;
-DROP TABLE bloom_snowflake_multi_dim_1;
-DROP TABLE bloom_snowflake_multi_dim_2;
-DROP TABLE bloom_snowflake_multi_dim_1_1;
-DROP TABLE bloom_snowflake_multi_dim_1_2;
-DROP TABLE bloom_snowflake_multi_dim_2_1;
-DROP TABLE bloom_snowflake_multi_dim_2_2;
diff --git a/src/test/regress/expected/hashjoin_bloom_star.out b/src/test/regress/expected/hashjoin_bloom_star.out
new file mode 100644
index 00000000000..ca053673bd9
--- /dev/null
+++ b/src/test/regress/expected/hashjoin_bloom_star.out
@@ -0,0 +1,410 @@
+-- tests to validate bloom filter pushdown
+-- no parallel query support for now
+SET max_parallel_workers_per_gather = 0;
+-- stabilize estimates
+SET default_statistics_target = 10000;
+-- needed, because the filter pushdown eliminates one of the inputs for
+-- choosing join order (reduction of result size)
+SET join_collapse_limit = 1;
+-- simple starjoin queries (fact + up to 7 dimensions)
+CREATE TABLE bloom_star_dim_1 (id int, r real);
+CREATE TABLE bloom_star_dim_2 (id int, r real);
+CREATE TABLE bloom_star_dim_3 (id int, r real);
+CREATE TABLE bloom_star_dim_4 (id int, r real);
+CREATE TABLE bloom_star_dim_5 (id int, r real);
+CREATE TABLE bloom_star_dim_6 (id int, r real);
+CREATE TABLE bloom_star_dim_7 (id int, r real);
+CREATE TABLE bloom_star_fact (id1 int, id2 int, id3 int, id4 int, id5 int, id6 int, id7 int, padding text);
+INSERT INTO bloom_star_dim_1 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_star_dim_2 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_star_dim_3 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_star_dim_4 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_star_dim_5 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_star_dim_6 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_star_dim_7 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_star_fact
+SELECT
+    1 + mod(i, 10000),
+    1 + mod(i, 10000),
+    1 + mod(i, 10000),
+    1 + mod(i, 10000),
+    1 + mod(i, 10000),
+    1 + mod(i, 10000),
+    1 + mod(i, 10000),
+    md5(i::text)
+FROM generate_series(1,1000000) s(i);
+VACUUM ANALYZE;
+-- simple query, no join is selective enough for a filter
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id);
+                                   QUERY PLAN                                    
+---------------------------------------------------------------------------------
+ Hash Join
+   Hash Cond: (f.id7 = d7.id)
+   ->  Hash Join
+         Hash Cond: (f.id6 = d6.id)
+         ->  Hash Join
+               Hash Cond: (f.id5 = d5.id)
+               ->  Hash Join
+                     Hash Cond: (f.id4 = d4.id)
+                     ->  Hash Join
+                           Hash Cond: (f.id3 = d3.id)
+                           ->  Hash Join
+                                 Hash Cond: (f.id2 = d2.id)
+                                 ->  Hash Join
+                                       Hash Cond: (f.id1 = d1.id)
+                                       ->  Seq Scan on bloom_star_fact f
+                                       ->  Hash
+                                             ->  Seq Scan on bloom_star_dim_1 d1
+                                 ->  Hash
+                                       ->  Seq Scan on bloom_star_dim_2 d2
+                           ->  Hash
+                                 ->  Seq Scan on bloom_star_dim_3 d3
+                     ->  Hash
+                           ->  Seq Scan on bloom_star_dim_4 d4
+               ->  Hash
+                     ->  Seq Scan on bloom_star_dim_5 d5
+         ->  Hash
+               ->  Seq Scan on bloom_star_dim_6 d6
+   ->  Hash
+         ->  Seq Scan on bloom_star_dim_7 d7
+(29 rows)
+
+-- last join is 75% selective (not enough for a filter to be created)
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id)
+WHERE d7.r < 0.75;
+                                   QUERY PLAN                                    
+---------------------------------------------------------------------------------
+ Hash Join
+   Hash Cond: (f.id7 = d7.id)
+   ->  Hash Join
+         Hash Cond: (f.id6 = d6.id)
+         ->  Hash Join
+               Hash Cond: (f.id5 = d5.id)
+               ->  Hash Join
+                     Hash Cond: (f.id4 = d4.id)
+                     ->  Hash Join
+                           Hash Cond: (f.id3 = d3.id)
+                           ->  Hash Join
+                                 Hash Cond: (f.id2 = d2.id)
+                                 ->  Hash Join
+                                       Hash Cond: (f.id1 = d1.id)
+                                       ->  Seq Scan on bloom_star_fact f
+                                       ->  Hash
+                                             ->  Seq Scan on bloom_star_dim_1 d1
+                                 ->  Hash
+                                       ->  Seq Scan on bloom_star_dim_2 d2
+                           ->  Hash
+                                 ->  Seq Scan on bloom_star_dim_3 d3
+                     ->  Hash
+                           ->  Seq Scan on bloom_star_dim_4 d4
+               ->  Hash
+                     ->  Seq Scan on bloom_star_dim_5 d5
+         ->  Hash
+               ->  Seq Scan on bloom_star_dim_6 d6
+   ->  Hash
+         ->  Seq Scan on bloom_star_dim_7 d7
+               Filter: (r < '0.75'::double precision)
+(30 rows)
+
+-- last join is 50% selective (good enough for a filter push down)
+-- the join will be executed done last
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id)
+WHERE d7.r < 0.5;
+                                   QUERY PLAN                                    
+---------------------------------------------------------------------------------
+ Hash Join
+   Hash Cond: (f.id7 = d7.id)
+   ->  Hash Join
+         Hash Cond: (f.id6 = d6.id)
+         ->  Hash Join
+               Hash Cond: (f.id5 = d5.id)
+               ->  Hash Join
+                     Hash Cond: (f.id4 = d4.id)
+                     ->  Hash Join
+                           Hash Cond: (f.id3 = d3.id)
+                           ->  Hash Join
+                                 Hash Cond: (f.id2 = d2.id)
+                                 ->  Hash Join
+                                       Hash Cond: (f.id1 = d1.id)
+                                       ->  Seq Scan on bloom_star_fact f
+                                             Bloom Filter 1: keys=(id7)
+                                       ->  Hash
+                                             ->  Seq Scan on bloom_star_dim_1 d1
+                                 ->  Hash
+                                       ->  Seq Scan on bloom_star_dim_2 d2
+                           ->  Hash
+                                 ->  Seq Scan on bloom_star_dim_3 d3
+                     ->  Hash
+                           ->  Seq Scan on bloom_star_dim_4 d4
+               ->  Hash
+                     ->  Seq Scan on bloom_star_dim_5 d5
+         ->  Hash
+               ->  Seq Scan on bloom_star_dim_6 d6
+   ->  Hash
+         Bloom Filter 1
+         ->  Seq Scan on bloom_star_dim_7 d7
+               Filter: (r < '0.5'::double precision)
+(32 rows)
+
+-- first join is 50% selective (good enough for a filter push down)
+-- the join will be executed done last
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id)
+WHERE d1.r < 0.5;
+                                       QUERY PLAN                                        
+-----------------------------------------------------------------------------------------
+ Hash Join
+   Hash Cond: (f.id7 = d7.id)
+   ->  Hash Join
+         Hash Cond: (f.id6 = d6.id)
+         ->  Hash Join
+               Hash Cond: (f.id5 = d5.id)
+               ->  Hash Join
+                     Hash Cond: (f.id4 = d4.id)
+                     ->  Hash Join
+                           Hash Cond: (f.id3 = d3.id)
+                           ->  Hash Join
+                                 Hash Cond: (f.id2 = d2.id)
+                                 ->  Hash Join
+                                       Hash Cond: (f.id1 = d1.id)
+                                       ->  Seq Scan on bloom_star_fact f
+                                             Bloom Filter 1: keys=(id1)
+                                       ->  Hash
+                                             Bloom Filter 1
+                                             ->  Seq Scan on bloom_star_dim_1 d1
+                                                   Filter: (r < '0.5'::double precision)
+                                 ->  Hash
+                                       ->  Seq Scan on bloom_star_dim_2 d2
+                           ->  Hash
+                                 ->  Seq Scan on bloom_star_dim_3 d3
+                     ->  Hash
+                           ->  Seq Scan on bloom_star_dim_4 d4
+               ->  Hash
+                     ->  Seq Scan on bloom_star_dim_5 d5
+         ->  Hash
+               ->  Seq Scan on bloom_star_dim_6 d6
+   ->  Hash
+         ->  Seq Scan on bloom_star_dim_7 d7
+(32 rows)
+
+-- two joins 50% selective
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id)
+WHERE d1.r < 0.4 AND d7.r < 0.5;
+                                       QUERY PLAN                                        
+-----------------------------------------------------------------------------------------
+ Hash Join
+   Hash Cond: (f.id7 = d7.id)
+   ->  Hash Join
+         Hash Cond: (f.id6 = d6.id)
+         ->  Hash Join
+               Hash Cond: (f.id5 = d5.id)
+               ->  Hash Join
+                     Hash Cond: (f.id4 = d4.id)
+                     ->  Hash Join
+                           Hash Cond: (f.id3 = d3.id)
+                           ->  Hash Join
+                                 Hash Cond: (f.id2 = d2.id)
+                                 ->  Hash Join
+                                       Hash Cond: (f.id1 = d1.id)
+                                       ->  Seq Scan on bloom_star_fact f
+                                             Bloom Filter 1: keys=(id1)
+                                             Bloom Filter 2: keys=(id7)
+                                       ->  Hash
+                                             Bloom Filter 1
+                                             ->  Seq Scan on bloom_star_dim_1 d1
+                                                   Filter: (r < '0.4'::double precision)
+                                 ->  Hash
+                                       ->  Seq Scan on bloom_star_dim_2 d2
+                           ->  Hash
+                                 ->  Seq Scan on bloom_star_dim_3 d3
+                     ->  Hash
+                           ->  Seq Scan on bloom_star_dim_4 d4
+               ->  Hash
+                     ->  Seq Scan on bloom_star_dim_5 d5
+         ->  Hash
+               ->  Seq Scan on bloom_star_dim_6 d6
+   ->  Hash
+         Bloom Filter 2
+         ->  Seq Scan on bloom_star_dim_7 d7
+               Filter: (r < '0.5'::double precision)
+(35 rows)
+
+-- all joins selective for a filter (more than how many we allow)
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id)
+WHERE d1.r < 0.3 AND d2.r < 0.35 AND d3.r < 0.4 AND d4.r < 0.45 AND d5.r < 0.5 AND d6.r < 0.55 AND d7.r < 0.6;
+                                       QUERY PLAN                                        
+-----------------------------------------------------------------------------------------
+ Hash Join
+   Hash Cond: (f.id7 = d7.id)
+   ->  Hash Join
+         Hash Cond: (f.id6 = d6.id)
+         ->  Hash Join
+               Hash Cond: (f.id5 = d5.id)
+               ->  Hash Join
+                     Hash Cond: (f.id4 = d4.id)
+                     ->  Hash Join
+                           Hash Cond: (f.id3 = d3.id)
+                           ->  Hash Join
+                                 Hash Cond: (f.id2 = d2.id)
+                                 ->  Hash Join
+                                       Hash Cond: (f.id1 = d1.id)
+                                       ->  Seq Scan on bloom_star_fact f
+                                             Bloom Filter 1: keys=(id1)
+                                             Bloom Filter 2: keys=(id2)
+                                             Bloom Filter 3: keys=(id3)
+                                       ->  Hash
+                                             Bloom Filter 1
+                                             ->  Seq Scan on bloom_star_dim_1 d1
+                                                   Filter: (r < '0.3'::double precision)
+                                 ->  Hash
+                                       Bloom Filter 2
+                                       ->  Seq Scan on bloom_star_dim_2 d2
+                                             Filter: (r < '0.35'::double precision)
+                           ->  Hash
+                                 Bloom Filter 3
+                                 ->  Seq Scan on bloom_star_dim_3 d3
+                                       Filter: (r < '0.4'::double precision)
+                     ->  Hash
+                           ->  Seq Scan on bloom_star_dim_4 d4
+                                 Filter: (r < '0.45'::double precision)
+               ->  Hash
+                     ->  Seq Scan on bloom_star_dim_5 d5
+                           Filter: (r < '0.5'::double precision)
+         ->  Hash
+               ->  Seq Scan on bloom_star_dim_6 d6
+                     Filter: (r < '0.55'::double precision)
+   ->  Hash
+         ->  Seq Scan on bloom_star_dim_7 d7
+               Filter: (r < '0.6'::double precision)
+(42 rows)
+
+SET bloom_filter_pushdown_max = 10;
+-- all joins selective for a filter
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id)
+WHERE d1.r < 0.3 AND d2.r < 0.35 AND d3.r < 0.4 AND d4.r < 0.45 AND d5.r < 0.5 AND d6.r < 0.55 AND d7.r < 0.6;
+                                       QUERY PLAN                                        
+-----------------------------------------------------------------------------------------
+ Hash Join
+   Hash Cond: (f.id7 = d7.id)
+   ->  Hash Join
+         Hash Cond: (f.id6 = d6.id)
+         ->  Hash Join
+               Hash Cond: (f.id5 = d5.id)
+               ->  Hash Join
+                     Hash Cond: (f.id4 = d4.id)
+                     ->  Hash Join
+                           Hash Cond: (f.id3 = d3.id)
+                           ->  Hash Join
+                                 Hash Cond: (f.id2 = d2.id)
+                                 ->  Hash Join
+                                       Hash Cond: (f.id1 = d1.id)
+                                       ->  Seq Scan on bloom_star_fact f
+                                             Bloom Filter 1: keys=(id1)
+                                             Bloom Filter 2: keys=(id2)
+                                             Bloom Filter 3: keys=(id3)
+                                             Bloom Filter 4: keys=(id4)
+                                             Bloom Filter 5: keys=(id5)
+                                             Bloom Filter 6: keys=(id6)
+                                             Bloom Filter 7: keys=(id7)
+                                       ->  Hash
+                                             Bloom Filter 1
+                                             ->  Seq Scan on bloom_star_dim_1 d1
+                                                   Filter: (r < '0.3'::double precision)
+                                 ->  Hash
+                                       Bloom Filter 2
+                                       ->  Seq Scan on bloom_star_dim_2 d2
+                                             Filter: (r < '0.35'::double precision)
+                           ->  Hash
+                                 Bloom Filter 3
+                                 ->  Seq Scan on bloom_star_dim_3 d3
+                                       Filter: (r < '0.4'::double precision)
+                     ->  Hash
+                           Bloom Filter 4
+                           ->  Seq Scan on bloom_star_dim_4 d4
+                                 Filter: (r < '0.45'::double precision)
+               ->  Hash
+                     Bloom Filter 5
+                     ->  Seq Scan on bloom_star_dim_5 d5
+                           Filter: (r < '0.5'::double precision)
+         ->  Hash
+               Bloom Filter 6
+               ->  Seq Scan on bloom_star_dim_6 d6
+                     Filter: (r < '0.55'::double precision)
+   ->  Hash
+         Bloom Filter 7
+         ->  Seq Scan on bloom_star_dim_7 d7
+               Filter: (r < '0.6'::double precision)
+(50 rows)
+
+RESET bloom_filter_pushdown_max;
+RESET join_collapse_limit;
+DROP TABLE bloom_star_dim_1;
+DROP TABLE bloom_star_dim_2;
+DROP TABLE bloom_star_dim_3;
+DROP TABLE bloom_star_dim_4;
+DROP TABLE bloom_star_dim_5;
+DROP TABLE bloom_star_dim_6;
+DROP TABLE bloom_star_dim_7;
+DROP TABLE bloom_star_fact;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 8fa0a6c47fb..0de7bdc3c56 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -146,3 +146,6 @@ test: fast_default
 # run tablespace test at the end because it drops the tablespace created during
 # setup that other tests may use.
 test: tablespace
+
+# tests for hashjoin bloom filter pushdown
+test: hashjoin_bloom hashjoin_bloom_star hashjoin_bloom_snowflake
diff --git a/src/test/regress/sql/hashjoin_bloom.sql b/src/test/regress/sql/hashjoin_bloom.sql
new file mode 100644
index 00000000000..47554daec4c
--- /dev/null
+++ b/src/test/regress/sql/hashjoin_bloom.sql
@@ -0,0 +1,43 @@
+-- tests to validate bloom filter pushdown
+-- no parallel query support for now
+SET max_parallel_workers_per_gather = 0;
+
+-- stabilize estimates
+SET default_statistics_target = 10000;
+
+-- a couple very simple join queries
+CREATE TABLE bloom_simple_dim (id int, r real);
+CREATE TABLE bloom_simple_fact (id int, padding text);
+
+INSERT INTO bloom_simple_dim SELECT i, random() FROM generate_series(1,1000) s(i);
+
+INSERT INTO bloom_simple_fact
+SELECT
+    1 + mod(i, 1000),
+    md5(i::text)
+FROM generate_series(1,100000) s(i);
+
+VACUUM ANALYZE;
+
+-- simple query, no join is selective enough for a filter
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_simple_fact f
+JOIN bloom_simple_dim d ON (f.id = d.id);
+
+-- join is 75% selective (not enough for a filter to be created)
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_simple_fact f
+JOIN bloom_simple_dim d ON (f.id = d.id)
+WHERE d.r < 0.75;
+
+-- join is 50% selective (enough for a filter to be pushed down)
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_simple_fact f
+JOIN bloom_simple_dim d ON (f.id = d.id)
+WHERE d.r < 0.5;
+
+DROP TABLE bloom_simple_dim;
+DROP TABLE bloom_simple_fact;
diff --git a/src/test/regress/sql/hashjoin_bloom_snowflake.sql b/src/test/regress/sql/hashjoin_bloom_snowflake.sql
new file mode 100644
index 00000000000..9fda90bed9b
--- /dev/null
+++ b/src/test/regress/sql/hashjoin_bloom_snowflake.sql
@@ -0,0 +1,142 @@
+-- tests to validate bloom filter pushdown
+-- no parallel query support for now
+SET max_parallel_workers_per_gather = 0;
+
+-- stabilize estimates
+SET default_statistics_target = 10000;
+
+-- simple snowflake queries (fact + up to 7 dimensions)
+CREATE TABLE bloom_snowflake_dim_1_1 (id int, r real);
+CREATE TABLE bloom_snowflake_dim_1_2 (id int, r real);
+CREATE TABLE bloom_snowflake_dim_1 (id int, id11 int, id12 int, r real);
+CREATE TABLE bloom_snowflake_dim_2_1 (id int, r real);
+CREATE TABLE bloom_snowflake_dim_2_2 (id int, r real);
+CREATE TABLE bloom_snowflake_dim_2 (id int, id21 int, id22 int, r real);
+
+CREATE TABLE bloom_snowflake_fact (id1 int, id2 int, padding text);
+
+INSERT INTO bloom_snowflake_dim_1_1 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_snowflake_dim_1_2 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_snowflake_dim_1 SELECT i, 1 + mod(i, 10000), 1 + mod(i, 10000), random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_snowflake_dim_2_1 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_snowflake_dim_2_2 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_snowflake_dim_2 SELECT i, 1 + mod(i, 10000), 1 + mod(i, 10000), random() FROM generate_series(1,10000) s(i);
+
+INSERT INTO bloom_snowflake_fact
+SELECT
+    1 + mod(i, 10000),
+    1 + mod(i, 10000),
+    md5(i::text)
+FROM generate_series(1,1000000) s(i);
+
+VACUUM ANALYZE;
+
+-- simple query, no join is selective enough for a filter
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_snowflake_fact f
+JOIN bloom_snowflake_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_snowflake_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_snowflake_dim_1_1 d11 ON (d1.id11 = d11.id)
+JOIN bloom_snowflake_dim_1_2 d12 ON (d1.id12 = d12.id)
+JOIN bloom_snowflake_dim_2_1 d21 ON (d2.id21 = d21.id)
+JOIN bloom_snowflake_dim_2_2 d22 ON (d2.id22 = d22.id);
+
+-- join is 75% selective (not enough for a filter to be created)
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_snowflake_fact f
+JOIN bloom_snowflake_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_snowflake_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_snowflake_dim_1_1 d11 ON (d1.id11 = d11.id)
+JOIN bloom_snowflake_dim_1_2 d12 ON (d1.id12 = d12.id)
+JOIN bloom_snowflake_dim_2_1 d21 ON (d2.id21 = d21.id)
+JOIN bloom_snowflake_dim_2_2 d22 ON (d2.id22 = d22.id)
+WHERE d1.r < 0.75;
+
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_snowflake_fact f
+JOIN bloom_snowflake_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_snowflake_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_snowflake_dim_1_1 d11 ON (d1.id11 = d11.id)
+JOIN bloom_snowflake_dim_1_2 d12 ON (d1.id12 = d12.id)
+JOIN bloom_snowflake_dim_2_1 d21 ON (d2.id21 = d21.id)
+JOIN bloom_snowflake_dim_2_2 d22 ON (d2.id22 = d22.id)
+WHERE d11.r < 0.75;
+
+-- join is 50% selective (good enough for pushdown)
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_snowflake_fact f
+JOIN bloom_snowflake_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_snowflake_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_snowflake_dim_1_1 d11 ON (d1.id11 = d11.id)
+JOIN bloom_snowflake_dim_1_2 d12 ON (d1.id12 = d12.id)
+JOIN bloom_snowflake_dim_2_1 d21 ON (d2.id21 = d21.id)
+JOIN bloom_snowflake_dim_2_2 d22 ON (d2.id22 = d22.id)
+WHERE d1.r < 0.5;
+
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_snowflake_fact f
+JOIN bloom_snowflake_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_snowflake_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_snowflake_dim_1_1 d11 ON (d1.id11 = d11.id)
+JOIN bloom_snowflake_dim_1_2 d12 ON (d1.id12 = d12.id)
+JOIN bloom_snowflake_dim_2_1 d21 ON (d2.id21 = d21.id)
+JOIN bloom_snowflake_dim_2_2 d22 ON (d2.id22 = d22.id)
+WHERE d11.r < 0.5;
+
+-- increase the accepted build size (includes the fact)
+SET bloom_filter_pushdown_max_build_relids = 4;
+
+-- join is 50% selective (good enough for pushdown)
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_snowflake_fact f
+JOIN bloom_snowflake_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_snowflake_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_snowflake_dim_1_1 d11 ON (d1.id11 = d11.id)
+JOIN bloom_snowflake_dim_1_2 d12 ON (d1.id12 = d12.id)
+JOIN bloom_snowflake_dim_2_1 d21 ON (d2.id21 = d21.id)
+JOIN bloom_snowflake_dim_2_2 d22 ON (d2.id22 = d22.id)
+WHERE d1.r < 0.5;
+
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_snowflake_fact f
+JOIN bloom_snowflake_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_snowflake_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_snowflake_dim_1_1 d11 ON (d1.id11 = d11.id)
+JOIN bloom_snowflake_dim_1_2 d12 ON (d1.id12 = d12.id)
+JOIN bloom_snowflake_dim_2_1 d21 ON (d2.id21 = d21.id)
+JOIN bloom_snowflake_dim_2_2 d22 ON (d2.id22 = d22.id)
+WHERE d11.r < 0.5;
+
+-- needed to stabilize the join order
+SET join_collapse_limit = 1;
+
+-- two joins with 75% selectivity (not enough for pushdown individually,
+-- but good enough when combined)
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_snowflake_fact f
+JOIN bloom_snowflake_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_snowflake_dim_1_1 d11 ON (d1.id11 = d11.id)
+JOIN bloom_snowflake_dim_1_2 d12 ON (d1.id12 = d12.id)
+JOIN bloom_snowflake_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_snowflake_dim_2_1 d21 ON (d2.id21 = d21.id)
+JOIN bloom_snowflake_dim_2_2 d22 ON (d2.id22 = d22.id)
+WHERE d11.r < 0.75 AND d12.r < 0.75;
+
+RESET join_collapse_limit;
+RESET bloom_filter_pushdown_max_build_relids;
+
+DROP TABLE bloom_snowflake_dim_1_1;
+DROP TABLE bloom_snowflake_dim_1_2;
+DROP TABLE bloom_snowflake_dim_1;
+DROP TABLE bloom_snowflake_dim_2_1;
+DROP TABLE bloom_snowflake_dim_2_2;
+DROP TABLE bloom_snowflake_dim_2;
+DROP TABLE bloom_snowflake_fact;
diff --git a/src/test/regress/sql/hashjoin_bloom_star.sql b/src/test/regress/sql/hashjoin_bloom_star.sql
new file mode 100644
index 00000000000..8af3513ba7a
--- /dev/null
+++ b/src/test/regress/sql/hashjoin_bloom_star.sql
@@ -0,0 +1,150 @@
+-- tests to validate bloom filter pushdown
+-- no parallel query support for now
+SET max_parallel_workers_per_gather = 0;
+
+-- stabilize estimates
+SET default_statistics_target = 10000;
+
+-- needed, because the filter pushdown eliminates one of the inputs for
+-- choosing join order (reduction of result size)
+SET join_collapse_limit = 1;
+
+-- simple starjoin queries (fact + up to 7 dimensions)
+CREATE TABLE bloom_star_dim_1 (id int, r real);
+CREATE TABLE bloom_star_dim_2 (id int, r real);
+CREATE TABLE bloom_star_dim_3 (id int, r real);
+CREATE TABLE bloom_star_dim_4 (id int, r real);
+CREATE TABLE bloom_star_dim_5 (id int, r real);
+CREATE TABLE bloom_star_dim_6 (id int, r real);
+CREATE TABLE bloom_star_dim_7 (id int, r real);
+
+CREATE TABLE bloom_star_fact (id1 int, id2 int, id3 int, id4 int, id5 int, id6 int, id7 int, padding text);
+
+INSERT INTO bloom_star_dim_1 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_star_dim_2 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_star_dim_3 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_star_dim_4 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_star_dim_5 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_star_dim_6 SELECT i, random() FROM generate_series(1,10000) s(i);
+INSERT INTO bloom_star_dim_7 SELECT i, random() FROM generate_series(1,10000) s(i);
+
+INSERT INTO bloom_star_fact
+SELECT
+    1 + mod(i, 10000),
+    1 + mod(i, 10000),
+    1 + mod(i, 10000),
+    1 + mod(i, 10000),
+    1 + mod(i, 10000),
+    1 + mod(i, 10000),
+    1 + mod(i, 10000),
+    md5(i::text)
+FROM generate_series(1,1000000) s(i);
+
+VACUUM ANALYZE;
+
+-- simple query, no join is selective enough for a filter
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id);
+
+-- last join is 75% selective (not enough for a filter to be created)
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id)
+WHERE d7.r < 0.75;
+
+-- last join is 50% selective (good enough for a filter push down)
+-- the join will be executed done last
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id)
+WHERE d7.r < 0.5;
+
+-- first join is 50% selective (good enough for a filter push down)
+-- the join will be executed done last
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id)
+WHERE d1.r < 0.5;
+
+-- two joins 50% selective
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id)
+WHERE d1.r < 0.4 AND d7.r < 0.5;
+
+-- all joins selective for a filter (more than how many we allow)
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id)
+WHERE d1.r < 0.3 AND d2.r < 0.35 AND d3.r < 0.4 AND d4.r < 0.45 AND d5.r < 0.5 AND d6.r < 0.55 AND d7.r < 0.6;
+
+
+SET bloom_filter_pushdown_max = 10;
+
+-- all joins selective for a filter
+EXPLAIN (COSTS OFF)
+SELECT *
+FROM bloom_star_fact f
+JOIN bloom_star_dim_1 d1 ON (f.id1 = d1.id)
+JOIN bloom_star_dim_2 d2 ON (f.id2 = d2.id)
+JOIN bloom_star_dim_3 d3 ON (f.id3 = d3.id)
+JOIN bloom_star_dim_4 d4 ON (f.id4 = d4.id)
+JOIN bloom_star_dim_5 d5 ON (f.id5 = d5.id)
+JOIN bloom_star_dim_6 d6 ON (f.id6 = d6.id)
+JOIN bloom_star_dim_7 d7 ON (f.id7 = d7.id)
+WHERE d1.r < 0.3 AND d2.r < 0.35 AND d3.r < 0.4 AND d4.r < 0.45 AND d5.r < 0.5 AND d6.r < 0.55 AND d7.r < 0.6;
+
+RESET bloom_filter_pushdown_max;
+RESET join_collapse_limit;
+
+DROP TABLE bloom_star_dim_1;
+DROP TABLE bloom_star_dim_2;
+DROP TABLE bloom_star_dim_3;
+DROP TABLE bloom_star_dim_4;
+DROP TABLE bloom_star_dim_5;
+DROP TABLE bloom_star_dim_6;
+DROP TABLE bloom_star_dim_7;
+DROP TABLE bloom_star_fact;
-- 
2.50.1 (Apple Git-155)

