From 60abe9a8a9aec7bb618d1f7113941a7f4b65e164 Mon Sep 17 00:00:00 2001
From: Julien Tachoires <julien@tachoires.me>
Date: Mon, 25 Aug 2025 18:46:06 +0200
Subject: [PATCH 4/6] Add tests for quals push down to table AM

With the help of the EXPLAIN command, we check if the rows are filtered
out by the executor or by the table AM. We also make sure that by
default, quals are not pushed down.
---
 src/test/regress/expected/qual_pushdown.out | 253 ++++++++++++++++++++
 src/test/regress/parallel_schedule          |   2 +-
 src/test/regress/sql/qual_pushdown.sql      |  48 ++++
 3 files changed, 302 insertions(+), 1 deletion(-)
 create mode 100644 src/test/regress/expected/qual_pushdown.out
 create mode 100644 src/test/regress/sql/qual_pushdown.sql

diff --git a/src/test/regress/expected/qual_pushdown.out b/src/test/regress/expected/qual_pushdown.out
new file mode 100644
index 00000000000..5b43553c945
--- /dev/null
+++ b/src/test/regress/expected/qual_pushdown.out
@@ -0,0 +1,253 @@
+DROP TABLE IF EXISTS qa;
+NOTICE:  table "qa" does not exist, skipping
+DROP TABLE IF EXISTS qb;
+NOTICE:  table "qb" does not exist, skipping
+CREATE TABLE qa (i INTEGER, ii INTEGER);
+CREATE TABLE qb (j INTEGER);
+INSERT INTO qa SELECT n, n * n  FROM generate_series(1, 1000) as n;
+INSERT INTO qb SELECT n FROM generate_series(1, 1000) as n;
+ANALYZE qa;
+ANALYZE qb;
+-- By default, the quals are not pushed down. The tuples are filtered out by
+-- the executor.
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = 100;
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: (i = 100)
+   Rows Removed In Executor by Filter: 999
+(3 rows)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i < 10;
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=9.00 loops=1)
+   Filter: (i < 10)
+   Rows Removed In Executor by Filter: 991
+(3 rows)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE 100 = i;
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: (100 = i)
+   Rows Removed In Executor by Filter: 999
+(3 rows)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE 10 > i;
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=9.00 loops=1)
+   Filter: (10 > i)
+   Rows Removed In Executor by Filter: 991
+(3 rows)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = SQRT(25)::INT;
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: (i = 5)
+   Rows Removed In Executor by Filter: 999
+(3 rows)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = (SELECT 100);
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: (i = (InitPlan 1).col1)
+   Rows Removed In Executor by Filter: 999
+   InitPlan 1
+     ->  Result (actual rows=1.00 loops=1)
+(5 rows)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = (SELECT SQRT(j)::INT FROM qb WHERE j = 100);
+                    QUERY PLAN                     
+---------------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: (i = (InitPlan 1).col1)
+   Rows Removed In Executor by Filter: 999
+   InitPlan 1
+     ->  Seq Scan on qb (actual rows=1.00 loops=1)
+           Filter: (j = 100)
+           Rows Removed In Executor by Filter: 999
+(7 rows)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa JOIN qb ON (qa.i = qb.j) WHERE j = 100;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Nested Loop (actual rows=1.00 loops=1)
+   ->  Seq Scan on qa (actual rows=1.00 loops=1)
+         Filter: (i = 100)
+         Rows Removed In Executor by Filter: 999
+   ->  Seq Scan on qb (actual rows=1.00 loops=1)
+         Filter: (j = 100)
+         Rows Removed In Executor by Filter: 999
+(7 rows)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = ii AND ii < 10;
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: ((ii < 10) AND (i = ii))
+   Rows Removed In Executor by Filter: 999
+(3 rows)
+
+-- Enable quals push down
+ALTER TABLE qa SET (quals_push_down=on);
+ALTER TABLE qb SET (quals_push_down=on);
+-- Now, we expect to see the tuples being filtered out by the table AM
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = 100;
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: (i = 100)
+   Rows Removed In Table AM by Filter: 999
+(3 rows)
+
+SELECT ii FROM qa WHERE i = 100;
+  ii   
+-------
+ 10000
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i < 10;
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=9.00 loops=1)
+   Filter: (i < 10)
+   Rows Removed In Table AM by Filter: 991
+(3 rows)
+
+SELECT ii FROM qa WHERE i < 10;
+ ii 
+----
+  1
+  4
+  9
+ 16
+ 25
+ 36
+ 49
+ 64
+ 81
+(9 rows)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE 100 = i;
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: (100 = i)
+   Rows Removed In Table AM by Filter: 999
+(3 rows)
+
+SELECT ii FROM qa WHERE 100 = i;
+  ii   
+-------
+ 10000
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE 10 > i;
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=9.00 loops=1)
+   Filter: (10 > i)
+   Rows Removed In Table AM by Filter: 991
+(3 rows)
+
+SELECT ii FROM qa WHERE 10 > i;
+ ii 
+----
+  1
+  4
+  9
+ 16
+ 25
+ 36
+ 49
+ 64
+ 81
+(9 rows)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = SQRT(25)::INT;
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: (i = 5)
+   Rows Removed In Table AM by Filter: 999
+(3 rows)
+
+SELECT ii FROM qa WHERE i = SQRT(25)::INT;
+ ii 
+----
+ 25
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = (SELECT 100);
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: (i = (InitPlan 1).col1)
+   Rows Removed In Table AM by Filter: 999
+   InitPlan 1
+     ->  Result (actual rows=1.00 loops=1)
+(5 rows)
+
+SELECT ii FROM qa WHERE i = (SELECT 100);
+  ii   
+-------
+ 10000
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = (SELECT SQRT(j)::INT FROM qb WHERE j = 100);
+                    QUERY PLAN                     
+---------------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: (i = (InitPlan 1).col1)
+   Rows Removed In Table AM by Filter: 999
+   InitPlan 1
+     ->  Seq Scan on qb (actual rows=1.00 loops=1)
+           Filter: (j = 100)
+           Rows Removed In Table AM by Filter: 999
+(7 rows)
+
+SELECT ii FROM qa WHERE i = (SELECT SQRT(j)::INT FROM qb WHERE j = 100);
+ ii  
+-----
+ 100
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa JOIN qb ON (qa.i = qb.j) WHERE j = 100;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Nested Loop (actual rows=1.00 loops=1)
+   ->  Seq Scan on qa (actual rows=1.00 loops=1)
+         Filter: (i = 100)
+         Rows Removed In Table AM by Filter: 999
+   ->  Seq Scan on qb (actual rows=1.00 loops=1)
+         Filter: (j = 100)
+         Rows Removed In Table AM by Filter: 999
+(7 rows)
+
+SELECT ii FROM qa JOIN qb ON (qa.i = qb.j) WHERE j = 100;
+  ii   
+-------
+ 10000
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = ii AND ii < 10;
+                QUERY PLAN                 
+-------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: ((ii < 10) AND (i = ii))
+   Rows Removed In Table AM by Filter: 997
+   Rows Removed In Executor by Filter: 2
+(4 rows)
+
+SELECT ii FROM qa WHERE i = ii AND ii < 10;
+ ii 
+----
+  1
+(1 row)
+
+DROP TABLE IF EXISTS qa;
+DROP TABLE IF EXISTS qb;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index fbffc67ae60..21291f38db3 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -123,7 +123,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 memoize stats predicate numa
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression compression_lz4 memoize stats predicate numa qual_pushdown
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/qual_pushdown.sql b/src/test/regress/sql/qual_pushdown.sql
new file mode 100644
index 00000000000..0f0410cd1d5
--- /dev/null
+++ b/src/test/regress/sql/qual_pushdown.sql
@@ -0,0 +1,48 @@
+DROP TABLE IF EXISTS qa;
+DROP TABLE IF EXISTS qb;
+
+CREATE TABLE qa (i INTEGER, ii INTEGER);
+CREATE TABLE qb (j INTEGER);
+INSERT INTO qa SELECT n, n * n  FROM generate_series(1, 1000) as n;
+INSERT INTO qb SELECT n FROM generate_series(1, 1000) as n;
+ANALYZE qa;
+ANALYZE qb;
+
+-- By default, the quals are not pushed down. The tuples are filtered out by
+-- the executor.
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = 100;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i < 10;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE 100 = i;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE 10 > i;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = SQRT(25)::INT;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = (SELECT 100);
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = (SELECT SQRT(j)::INT FROM qb WHERE j = 100);
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa JOIN qb ON (qa.i = qb.j) WHERE j = 100;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = ii AND ii < 10;
+
+-- Enable quals push down
+ALTER TABLE qa SET (quals_push_down=on);
+ALTER TABLE qb SET (quals_push_down=on);
+
+-- Now, we expect to see the tuples being filtered out by the table AM
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = 100;
+SELECT ii FROM qa WHERE i = 100;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i < 10;
+SELECT ii FROM qa WHERE i < 10;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE 100 = i;
+SELECT ii FROM qa WHERE 100 = i;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE 10 > i;
+SELECT ii FROM qa WHERE 10 > i;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = SQRT(25)::INT;
+SELECT ii FROM qa WHERE i = SQRT(25)::INT;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = (SELECT 100);
+SELECT ii FROM qa WHERE i = (SELECT 100);
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = (SELECT SQRT(j)::INT FROM qb WHERE j = 100);
+SELECT ii FROM qa WHERE i = (SELECT SQRT(j)::INT FROM qb WHERE j = 100);
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa JOIN qb ON (qa.i = qb.j) WHERE j = 100;
+SELECT ii FROM qa JOIN qb ON (qa.i = qb.j) WHERE j = 100;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = ii AND ii < 10;
+SELECT ii FROM qa WHERE i = ii AND ii < 10;
+
+DROP TABLE IF EXISTS qa;
+DROP TABLE IF EXISTS qb;
-- 
2.39.5

