From b8cf0b028d2202d503a39f18b62a106d0ad45906 Mon Sep 17 00:00:00 2001
From: Julien Tachoires <julien@tachoires.me>
Date: Mon, 25 Aug 2025 18:43:57 +0200
Subject: [PATCH 3/6] Add the table reloption quals_push_down

The reloption quals_push_down enables or disables qualifiers to ScanKey
transformation and push down to the table access method during table
scan execution.

The default value is off, making the Quals Push Down feature disabled by
default.
---
 .../postgres_fdw/expected/postgres_fdw.out    |  2 +-
 doc/src/sgml/ref/create_table.sgml            | 19 ++++++++++++++
 src/backend/access/common/reloptions.c        | 13 +++++++++-
 src/backend/executor/nodeSeqscan.c            | 21 ++++++++++-----
 src/bin/psql/tab-complete.in.c                |  1 +
 src/include/utils/rel.h                       |  9 +++++++
 src/test/isolation/expected/stats.out         | 26 +++++++++----------
 src/test/regress/expected/memoize.out         | 15 +++++------
 src/test/regress/expected/merge.out           |  2 +-
 src/test/regress/expected/partition_prune.out |  4 +--
 src/test/regress/expected/select_parallel.out |  4 +--
 src/test/regress/expected/updatable_views.out |  3 +++
 12 files changed, 84 insertions(+), 35 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index bc7242835df..c5e3761f648 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -11930,7 +11930,7 @@ SELECT * FROM local_tbl, async_pt WHERE local_tbl.a = async_pt.a AND local_tbl.c
  Nested Loop (actual rows=1.00 loops=1)
    ->  Seq Scan on local_tbl (actual rows=1.00 loops=1)
          Filter: (c = 'bar'::text)
-         Rows Removed In Table AM by Filter: 1
+         Rows Removed In Executor by Filter: 1
    ->  Append (actual rows=1.00 loops=1)
          ->  Async Foreign Scan on async_p1 async_pt_1 (never executed)
          ->  Async Foreign Scan on async_p2 async_pt_2 (actual rows=1.00 loops=1)
diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index dc000e913c1..7cc52852fc0 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -1997,6 +1997,25 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     </listitem>
    </varlistentry>
 
+   <varlistentry id="reloption-quals-push-down" xreflabel="quals_push_down">
+    <term><literal>quals_push_down</literal> (<type>boolean</type>)
+    <indexterm>
+     <primary><varname>quals_push_down</varname> storage parameter</primary>
+    </indexterm>
+    </term>
+    <listitem>
+     <para>
+     Enables or disables qualifiers (<literal>WHERE</literal> clause) push
+     down to the table access method. When enabled, during table scan execution,
+     the table access method is able to apply early tuple filtering and returns
+     only the tuples satisfying the qualifiers. By default, this option is
+     disabled, then the table access method returns all the visible tuples and
+     let the query executor alone in charge of doing tuple filtering based
+     on the qualifiers.
+     </para>
+    </listitem>
+   </varlistentry>
+
    </variablelist>
 
   </refsect2>
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 0af3fea68fa..dd57599b080 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -166,6 +166,15 @@ static relopt_bool boolRelOpts[] =
 		},
 		true
 	},
+	{
+		{
+			"quals_push_down",
+			"Enables query qualifiers push down to the table access method during table scan",
+			RELOPT_KIND_HEAP,
+			AccessExclusiveLock
+		},
+		false
+	},
 	/* list terminator */
 	{{NULL}}
 };
@@ -1915,7 +1924,9 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind)
 		{"vacuum_truncate", RELOPT_TYPE_BOOL,
 		offsetof(StdRdOptions, vacuum_truncate), offsetof(StdRdOptions, vacuum_truncate_set)},
 		{"vacuum_max_eager_freeze_failure_rate", RELOPT_TYPE_REAL,
-		offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)}
+		offsetof(StdRdOptions, vacuum_max_eager_freeze_failure_rate)},
+		{"quals_push_down", RELOPT_TYPE_BOOL,
+		offsetof(StdRdOptions, quals_push_down)}
 	};
 
 	return (bytea *) build_reloptions(reloptions, validate, kind,
diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c
index 0562377c42a..f134ff591c3 100644
--- a/src/backend/executor/nodeSeqscan.c
+++ b/src/backend/executor/nodeSeqscan.c
@@ -483,13 +483,20 @@ ExecInitSeqScan(SeqScan *node, EState *estate, int eflags)
 	scanstate->ss.ps.qual =
 		ExecInitQual(node->scan.plan.qual, (PlanState *) scanstate);
 
-	/* Build sequential scan keys */
-	ExecSeqBuildScanKeys((PlanState *) scanstate,
-						 node->tablequal,
-						 &scanstate->sss_NumScanKeys,
-						 &scanstate->sss_ScanKeys,
-						 &scanstate->sss_RuntimeKeys,
-						 &scanstate->sss_NumRuntimeKeys);
+	/*
+	 * Build an push the ScanKeys only if the relation's reloption
+	 * quals_push_down is on.
+	 */
+	if (RelationGetQualsPushDown(scanstate->ss.ss_currentRelation))
+	{
+		/* Build sequential scan keys */
+		ExecSeqBuildScanKeys((PlanState *) scanstate,
+							 node->tablequal,
+							 &scanstate->sss_NumScanKeys,
+							 &scanstate->sss_ScanKeys,
+							 &scanstate->sss_RuntimeKeys,
+							 &scanstate->sss_NumRuntimeKeys);
+	}
 
 	/*
 	 * When EvalPlanQual() is not in use, assign ExecProcNode for this node
diff --git a/src/bin/psql/tab-complete.in.c b/src/bin/psql/tab-complete.in.c
index 8b10f2313f3..0827679649b 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1412,6 +1412,7 @@ static const char *const table_storage_parameters[] = {
 	"fillfactor",
 	"log_autovacuum_min_duration",
 	"parallel_workers",
+	"quals_push_down",
 	"toast.autovacuum_enabled",
 	"toast.autovacuum_freeze_max_age",
 	"toast.autovacuum_freeze_min_age",
diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h
index b552359915f..8907d53a4ca 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -354,6 +354,7 @@ typedef struct StdRdOptions
 	 * to freeze. 0 if disabled, -1 if unspecified.
 	 */
 	double		vacuum_max_eager_freeze_failure_rate;
+	bool		quals_push_down;	/* enable quals push down to the table AM */
 } StdRdOptions;
 
 #define HEAP_MIN_FILLFACTOR			10
@@ -409,6 +410,14 @@ typedef struct StdRdOptions
 	((relation)->rd_options ? \
 	 ((StdRdOptions *) (relation)->rd_options)->parallel_workers : (defaultpw))
 
+/*
+ * RelationGetQualsPushDown
+ *		Returns the relation's quals_push_down reloption setting.
+ */
+#define RelationGetQualsPushDown(relation) \
+	((relation)->rd_options ? \
+	 ((StdRdOptions *) (relation)->rd_options)->quals_push_down : false)
+
 /* ViewOptions->check_option values */
 typedef enum ViewOptCheckOption
 {
diff --git a/src/test/isolation/expected/stats.out b/src/test/isolation/expected/stats.out
index 0064c0c8df0..8c7fe60217e 100644
--- a/src/test/isolation/expected/stats.out
+++ b/src/test/isolation/expected/stats.out
@@ -2414,7 +2414,7 @@ step s1_table_stats:
 
 seq_scan|seq_tup_read|n_tup_ins|n_tup_upd|n_tup_del|n_live_tup|n_dead_tup|vacuum_count
 --------+------------+---------+---------+---------+----------+----------+------------
-       3|           5|        1|        1|        0|         1|         1|           0
+       3|           6|        1|        1|        0|         1|         1|           0
 (1 row)
 
 
@@ -2476,7 +2476,7 @@ step s1_table_stats:
 
 seq_scan|seq_tup_read|n_tup_ins|n_tup_upd|n_tup_del|n_live_tup|n_dead_tup|vacuum_count
 --------+------------+---------+---------+---------+----------+----------+------------
-       3|           4|        2|        0|        1|         1|         1|           0
+       3|           5|        2|        0|        1|         1|         1|           0
 (1 row)
 
 step s1_table_select: SELECT * FROM test_stat_tab ORDER BY key, value;
@@ -2508,7 +2508,7 @@ step s1_table_stats:
 
 seq_scan|seq_tup_read|n_tup_ins|n_tup_upd|n_tup_del|n_live_tup|n_dead_tup|vacuum_count
 --------+------------+---------+---------+---------+----------+----------+------------
-       5|           7|        2|        1|        1|         1|         2|           0
+       5|           9|        2|        1|        1|         1|         2|           0
 (1 row)
 
 
@@ -2571,7 +2571,7 @@ step s1_table_stats:
 
 seq_scan|seq_tup_read|n_tup_ins|n_tup_upd|n_tup_del|n_live_tup|n_dead_tup|vacuum_count
 --------+------------+---------+---------+---------+----------+----------+------------
-       9|          13|        4|        5|        1|         3|         6|           0
+       9|          31|        4|        5|        1|         3|         6|           0
 (1 row)
 
 
@@ -2640,7 +2640,7 @@ step s1_table_stats:
 
 seq_scan|seq_tup_read|n_tup_ins|n_tup_upd|n_tup_del|n_live_tup|n_dead_tup|vacuum_count
 --------+------------+---------+---------+---------+----------+----------+------------
-       9|          13|        4|        5|        1|         3|         6|           0
+       9|          31|        4|        5|        1|         3|         6|           0
 (1 row)
 
 
@@ -2701,7 +2701,7 @@ step s1_table_stats:
 
 seq_scan|seq_tup_read|n_tup_ins|n_tup_upd|n_tup_del|n_live_tup|n_dead_tup|vacuum_count
 --------+------------+---------+---------+---------+----------+----------+------------
-       9|          11|        4|        5|        1|         1|         8|           0
+       9|          29|        4|        5|        1|         1|         8|           0
 (1 row)
 
 
@@ -2768,7 +2768,7 @@ step s1_table_stats:
 
 seq_scan|seq_tup_read|n_tup_ins|n_tup_upd|n_tup_del|n_live_tup|n_dead_tup|vacuum_count
 --------+------------+---------+---------+---------+----------+----------+------------
-       9|          11|        4|        5|        1|         1|         8|           0
+       9|          29|        4|        5|        1|         1|         8|           0
 (1 row)
 
 
@@ -2808,7 +2808,7 @@ step s1_table_stats:
 
 seq_scan|seq_tup_read|n_tup_ins|n_tup_upd|n_tup_del|n_live_tup|n_dead_tup|vacuum_count
 --------+------------+---------+---------+---------+----------+----------+------------
-       3|           3|        5|        1|        0|         1|         1|           0
+       3|           9|        5|        1|        0|         1|         1|           0
 (1 row)
 
 
@@ -2854,7 +2854,7 @@ step s1_table_stats:
 
 seq_scan|seq_tup_read|n_tup_ins|n_tup_upd|n_tup_del|n_live_tup|n_dead_tup|vacuum_count
 --------+------------+---------+---------+---------+----------+----------+------------
-       3|           3|        5|        1|        0|         1|         1|           0
+       3|           9|        5|        1|        0|         1|         1|           0
 (1 row)
 
 
@@ -2894,7 +2894,7 @@ step s1_table_stats:
 
 seq_scan|seq_tup_read|n_tup_ins|n_tup_upd|n_tup_del|n_live_tup|n_dead_tup|vacuum_count
 --------+------------+---------+---------+---------+----------+----------+------------
-       3|           3|        4|        2|        0|         4|         2|           0
+       3|           9|        4|        2|        0|         4|         2|           0
 (1 row)
 
 
@@ -2940,7 +2940,7 @@ step s1_table_stats:
 
 seq_scan|seq_tup_read|n_tup_ins|n_tup_upd|n_tup_del|n_live_tup|n_dead_tup|vacuum_count
 --------+------------+---------+---------+---------+----------+----------+------------
-       3|           3|        4|        2|        0|         4|         2|           0
+       3|           9|        4|        2|        0|         4|         2|           0
 (1 row)
 
 
@@ -2981,7 +2981,7 @@ step s1_table_stats:
 
 seq_scan|seq_tup_read|n_tup_ins|n_tup_upd|n_tup_del|n_live_tup|n_dead_tup|vacuum_count
 --------+------------+---------+---------+---------+----------+----------+------------
-       4|           4|        5|        3|        1|         4|         4|           0
+       4|          16|        5|        3|        1|         4|         4|           0
 (1 row)
 
 
@@ -3028,7 +3028,7 @@ step s1_table_stats:
 
 seq_scan|seq_tup_read|n_tup_ins|n_tup_upd|n_tup_del|n_live_tup|n_dead_tup|vacuum_count
 --------+------------+---------+---------+---------+----------+----------+------------
-       4|           4|        5|        3|        1|         4|         4|           0
+       4|          16|        5|        3|        1|         4|         4|           0
 (1 row)
 
 
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index f2a92d1fdfd..4af6bb4ce0f 100644
--- a/src/test/regress/expected/memoize.out
+++ b/src/test/regress/expected/memoize.out
@@ -43,7 +43,7 @@ WHERE t2.unique1 < 1000;', false);
    ->  Nested Loop (actual rows=1000.00 loops=N)
          ->  Seq Scan on tenk1 t2 (actual rows=1000.00 loops=N)
                Filter: (unique1 < 1000)
-               Rows Removed In Table AM by Filter: 9000
+               Rows Removed In Executor by Filter: 9000
          ->  Memoize (actual rows=1.00 loops=N)
                Cache Key: t2.twenty
                Cache Mode: logical
@@ -75,7 +75,7 @@ WHERE t1.unique1 < 1000;', false);
    ->  Nested Loop (actual rows=1000.00 loops=N)
          ->  Seq Scan on tenk1 t1 (actual rows=1000.00 loops=N)
                Filter: (unique1 < 1000)
-               Rows Removed In Table AM by Filter: 9000
+               Rows Removed In Executor by Filter: 9000
          ->  Memoize (actual rows=1.00 loops=N)
                Cache Key: t1.twenty
                Cache Mode: binary
@@ -146,7 +146,7 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
    ->  Nested Loop (actual rows=1000.00 loops=N)
          ->  Seq Scan on tenk1 t1 (actual rows=1000.00 loops=N)
                Filter: (unique1 < 1000)
-               Rows Removed In Table AM by Filter: 9000
+               Rows Removed In Executor by Filter: 9000
          ->  Memoize (actual rows=1.00 loops=N)
                Cache Key: (t1.two + 1)
                Cache Mode: binary
@@ -179,16 +179,15 @@ WHERE s.c1 = s.c2 AND t1.unique1 < 1000;', false);
    ->  Nested Loop (actual rows=1000.00 loops=N)
          ->  Seq Scan on tenk1 t1 (actual rows=1000.00 loops=N)
                Filter: (unique1 < 1000)
-               Rows Removed In Table AM by Filter: 9000
+               Rows Removed In Executor by Filter: 9000
          ->  Memoize (actual rows=1.00 loops=N)
                Cache Key: t1.two, t1.twenty
                Cache Mode: binary
                Hits: 980  Misses: 20  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Seq Scan on tenk1 t2 (actual rows=1.00 loops=N)
                      Filter: ((t1.twenty = unique1) AND (t1.two = two))
-                     Rows Removed In Table AM by Filter: 5000
-                     Rows Removed In Executor by Filter: 4999
-(13 rows)
+                     Rows Removed In Executor by Filter: 9999
+(12 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -247,7 +246,7 @@ WHERE t2.unique1 < 1200;', true);
    ->  Nested Loop (actual rows=1200.00 loops=N)
          ->  Seq Scan on tenk1 t2 (actual rows=1200.00 loops=N)
                Filter: (unique1 < 1200)
-               Rows Removed In Table AM by Filter: 8800
+               Rows Removed In Executor by Filter: 8800
          ->  Memoize (actual rows=1.00 loops=N)
                Cache Key: t2.thousand
                Cache Mode: logical
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index f8b9172df20..3029bb6ba10 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -1801,7 +1801,7 @@ WHEN MATCHED AND t.a < 10 THEN
                Sort Method: quicksort  Memory: xxx
                ->  Seq Scan on ex_mtarget t (actual rows=0.00 loops=1)
                      Filter: (a < '-1000'::integer)
-                     Rows Removed In Table AM by Filter: 54
+                     Rows Removed In Executor by Filter: 54
          ->  Sort (never executed)
                Sort Key: s.a
                ->  Seq Scan on ex_msource s (never executed)
diff --git a/src/test/regress/expected/partition_prune.out b/src/test/regress/expected/partition_prune.out
index c633e7089ce..dbbd7b05e11 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2866,7 +2866,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q6
          Filter: ((a = $1) AND (b = (InitPlan 1).col1))
    ->  Seq Scan on xy_1 (actual rows=0.00 loops=1)
          Filter: ((x = $1) AND (y = (InitPlan 1).col1))
-         Rows Removed In Table AM by Filter: 1
+         Rows Removed In Executor by Filter: 1
    ->  Seq Scan on ab_a1_b1 ab_4 (never executed)
          Filter: ((a = $1) AND (b = (InitPlan 1).col1))
    ->  Seq Scan on ab_a1_b2 ab_5 (never executed)
@@ -4096,7 +4096,7 @@ select * from listp where a = (select 2) and b <> 10;
  Seq Scan on listp1 listp (actual rows=0.00 loops=1)
    Filter: ((b <> 10) AND (a = (InitPlan 1).col1))
    InitPlan 1
-     ->  Result (actual rows=1.00 loops=1)
+     ->  Result (never executed)
 (4 rows)
 
 --
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 572337c7b77..b1e6f5681ac 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -589,13 +589,13 @@ explain (analyze, timing off, summary off, costs off, buffers off)
    ->  Nested Loop (actual rows=98000.00 loops=1)
          ->  Seq Scan on tenk2 (actual rows=10.00 loops=1)
                Filter: (thousand = 0)
-               Rows Removed In Table AM by Filter: 9990
+               Rows Removed In Executor by Filter: 9990
          ->  Gather (actual rows=9800.00 loops=10)
                Workers Planned: 4
                Workers Launched: 4
                ->  Parallel Seq Scan on tenk1 (actual rows=1960.00 loops=50)
                      Filter: (hundred > 1)
-                     Rows Removed In Table AM by Filter: 40
+                     Rows Removed In Executor by Filter: 40
 (11 rows)
 
 alter table tenk2 reset (parallel_workers);
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 8d513926b3b..095df0a670c 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2931,6 +2931,7 @@ $$
 LANGUAGE plpgsql STRICT IMMUTABLE LEAKPROOF;
 SELECT * FROM rw_view1 WHERE snoop(person);
 NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Dick
 NOTICE:  snooped value: Harry
  person 
 --------
@@ -2940,8 +2941,10 @@ NOTICE:  snooped value: Harry
 
 UPDATE rw_view1 SET person=person WHERE snoop(person);
 NOTICE:  snooped value: Tom
+NOTICE:  snooped value: Dick
 NOTICE:  snooped value: Harry
 DELETE FROM rw_view1 WHERE NOT snoop(person);
+NOTICE:  snooped value: Dick
 NOTICE:  snooped value: Tom
 NOTICE:  snooped value: Harry
 ALTER VIEW rw_view1 SET (security_barrier = true);
-- 
2.39.5

