From 3961b56774e1c04a0446f1a121a5eccf15bf0f58 Mon Sep 17 00:00:00 2001
From: Julien Tachoires <julien@tachoires.me>
Date: Tue, 2 Dec 2025 10:43:52 +0100
Subject: [PATCH 4/7] 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.
---
 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/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 ++--
 9 files changed, 67 insertions(+), 21 deletions(-)

diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml
index 6557c5cffd8..29118575b06 100644
--- a/doc/src/sgml/ref/create_table.sgml
+++ b/doc/src/sgml/ref/create_table.sgml
@@ -2012,6 +2012,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 9e288dfecbf..196faddf55b 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}}
 };
@@ -1926,7 +1935,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 7fc0ae5d97a..de9dafcd428 100644
--- a/src/backend/executor/nodeSeqscan.c
+++ b/src/backend/executor/nodeSeqscan.c
@@ -494,13 +494,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 20d7a65c614..e0282ab9a57 100644
--- a/src/bin/psql/tab-complete.in.c
+++ b/src/bin/psql/tab-complete.in.c
@@ -1434,6 +1434,7 @@ static const char *const table_storage_parameters[] = {
 	"log_autovacuum_min_duration",
 	"log_autoanalyze_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 80286076a11..dff085faa50 100644
--- a/src/include/utils/rel.h
+++ b/src/include/utils/rel.h
@@ -355,6 +355,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
@@ -410,6 +411,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/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index c116c3945ef..aef4aec89ab 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 123b063716f..daf5cc746d1 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 943cc9131ae..f6bd2b1e735 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2879,7 +2879,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute ab_q6
          Filter: ((a = $1) AND (b = (InitPlan expr_1).col1))
    ->  Seq Scan on xy_1 (actual rows=0.00 loops=1)
          Filter: ((x = $1) AND (y = (InitPlan expr_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 expr_1).col1))
    ->  Seq Scan on ab_a1_b2 ab_5 (never executed)
@@ -4115,7 +4115,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 expr_1).col1))
    InitPlan expr_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 3467feea2d3..90c24ace07d 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);
-- 
2.39.5

