From 7a7793109764bff1ae7633caf5a3c5b401be3b84 Mon Sep 17 00:00:00 2001
From: Julien Tachoires <julien@tachoires.me>
Date: Tue, 2 Dec 2025 10:42:38 +0100
Subject: [PATCH 3/7] Simple quals push down to table AMs

Simple quals like: <column> <op> <const|func|var|subquery> are now
converted to ScanKeys and then passed to the underlying layer via the
table AM API. During the execution of sequential scans, the table AM can
use the given ScanKeys to filter out the tuples not satisfying the
condition before returning them to the executor. Doing this kind of
early tuples filtering speed up sequential scans execution time when a
large portion of the table must be excluded from the final result.

The query planner, via fix_tablequal_references(), is in charge of
pre-processing the quals and exclude those that cannot be used as
ScanKeys. Pre-processing quals consists in making sure that the key is
on left part of the expression, the value is on the right, and both left
and right are not relabeled.

Non-constant values are registered as run-time keys and then evaluated
and converted to ScanKeys when a rescan is requested via
ExecReScanSeqScan(). InitPlan (sub-SELECT executed only once) is the
only type of SubQuery supported for now.

A new instrumention counter is added in order to make the distinction
between the tuples excluded by the table AM and those excluded by the
executor. The explain command output is modified in that sense too.
---
 .../postgres_fdw/expected/postgres_fdw.out    |   4 +-
 src/backend/access/heap/heapam.c              |  15 +-
 src/backend/commands/explain.c                |  51 ++-
 src/backend/executor/instrument.c             |   1 +
 src/backend/executor/nodeSeqscan.c            | 358 +++++++++++++++++-
 src/backend/optimizer/plan/createplan.c       | 221 ++++++++++-
 src/include/access/relscan.h                  |   1 +
 src/include/executor/instrument.h             |   7 +-
 src/include/executor/nodeSeqscan.h            |   3 +
 src/include/nodes/execnodes.h                 |  40 +-
 src/include/nodes/plannodes.h                 |   2 +
 src/test/regress/expected/memoize.out         |  21 +-
 src/test/regress/expected/merge.out           |   2 +-
 src/test/regress/expected/partition_prune.out |  28 +-
 src/test/regress/expected/select_parallel.out |   4 +-
 src/test/regress/sql/partition_prune.sql      |   2 +-
 16 files changed, 688 insertions(+), 72 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index 48e3185b227..5747606ff90 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -11925,7 +11925,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 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)
@@ -12220,7 +12220,7 @@ SELECT * FROM async_pt t1 WHERE t1.b === 505 LIMIT 1;
                Filter: (b === 505)
          ->  Seq Scan on async_p3 t1_3 (actual rows=1.00 loops=1)
                Filter: (b === 505)
-               Rows Removed by Filter: 101
+               Rows Removed In Executor by Filter: 101
 (9 rows)
 
 SELECT * FROM async_pt t1 WHERE t1.b === 505 LIMIT 1;
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 25bc941a815..f3c4dc91e54 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -352,7 +352,8 @@ bitmapheap_stream_read_next(ReadStream *pgsr, void *private_data,
  * ----------------
  */
 static void
-initscan(HeapScanDesc scan, int nkeys, ScanKey keys, bool keep_startblock)
+initscan(HeapScanDesc scan, int nkeys, ScanKey keys, bool keep_startblock,
+		 bool update_stats)
 {
 	ParallelBlockTableScanDesc bpscan = NULL;
 	bool		allow_strat;
@@ -487,7 +488,7 @@ initscan(HeapScanDesc scan, int nkeys, ScanKey keys, bool keep_startblock)
 	 * e.g for bitmap scans the underlying bitmap index scans will be counted,
 	 * and for sample scans we update stats for tuple fetches).
 	 */
-	if (scan->rs_base.rs_flags & SO_TYPE_SEQSCAN)
+	if (update_stats && (scan->rs_base.rs_flags & SO_TYPE_SEQSCAN))
 		pgstat_count_heap_scan(scan->rs_base.rs_rd);
 }
 
@@ -993,6 +994,8 @@ continue_page:
 				!HeapKeyTest(tuple, RelationGetDescr(scan->rs_base.rs_rd),
 							 nkeys, key))
 			{
+				scan->rs_base.rs_nskip++;
+
 				/*
 				 * When the tuple is visible but does not satisfy any scan
 				 * key, then we have to re-acquire the buffer lock to examine
@@ -1107,7 +1110,10 @@ continue_page:
 			if (key != NULL &&
 				!HeapKeyTest(tuple, RelationGetDescr(scan->rs_base.rs_rd),
 							 nkeys, key))
+			{
+				scan->rs_base.rs_nskip++;
 				continue;
+			}
 
 			scan->rs_cindex = lineindex;
 			return;
@@ -1167,6 +1173,7 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	scan->rs_base.rs_rd = relation;
 	scan->rs_base.rs_snapshot = snapshot;
 	scan->rs_base.rs_nkeys = nkeys;
+	scan->rs_base.rs_nskip = 0;
 	scan->rs_base.rs_flags = flags;
 	scan->rs_base.rs_parallel = parallel_scan;
 	scan->rs_strategy = NULL;	/* set in initscan */
@@ -1234,7 +1241,7 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	else
 		scan->rs_base.rs_key = NULL;
 
-	initscan(scan, nkeys, keys, false);
+	initscan(scan, nkeys, keys, false, true);
 
 	scan->rs_read_stream = NULL;
 
@@ -1335,7 +1342,7 @@ heap_rescan(TableScanDesc sscan, int nkeys, ScanKey keys, bool set_params,
 	/*
 	 * reinitialize scan descriptor
 	 */
-	initscan(scan, nkeys, keys, true);
+	initscan(scan, nkeys, keys, true, false);
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 7e699f8595e..2b0df2eb256 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1968,7 +1968,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 						   "Order By", planstate, ancestors, es);
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 1,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 										   planstate, es);
 			show_indexsearches_info(planstate, es);
 			break;
@@ -1982,7 +1982,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 						   "Order By", planstate, ancestors, es);
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 1,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 										   planstate, es);
 			if (es->analyze)
 				ExplainPropertyFloat("Heap Fetches", NULL,
@@ -2002,7 +2002,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 										   planstate, es);
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 1,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 										   planstate, es);
 			show_tidbitmap_info((BitmapHeapScanState *) planstate, es);
 			break;
@@ -2012,6 +2012,15 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			/* fall through to print additional fields the same as SeqScan */
 			/* FALLTHROUGH */
 		case T_SeqScan:
+			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
+			if (plan->qual)
+			{
+				show_instrumentation_count("Rows Removed In Table AM by Filter", 3,
+										   planstate, es);
+				show_instrumentation_count("Rows Removed In Executor by Filter", 1,
+										   planstate, es);
+			}
+			break;
 		case T_ValuesScan:
 		case T_CteScan:
 		case T_NamedTuplestoreScan:
@@ -2019,7 +2028,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 		case T_SubqueryScan:
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 1,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 										   planstate, es);
 			if (IsA(plan, CteScan))
 				show_ctescan_info(castNode(CteScanState, planstate), es);
@@ -2030,7 +2039,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 
 				show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 				if (plan->qual)
-					show_instrumentation_count("Rows Removed by Filter", 1,
+					show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 											   planstate, es);
 				ExplainPropertyInteger("Workers Planned", NULL,
 									   gather->num_workers, es);
@@ -2054,7 +2063,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 
 				show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 				if (plan->qual)
-					show_instrumentation_count("Rows Removed by Filter", 1,
+					show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 											   planstate, es);
 				ExplainPropertyInteger("Workers Planned", NULL,
 									   gm->num_workers, es);
@@ -2088,7 +2097,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			}
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 1,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 										   planstate, es);
 			break;
 		case T_TableFuncScan:
@@ -2102,7 +2111,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			}
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 1,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 										   planstate, es);
 			show_table_func_scan_info(castNode(TableFuncScanState,
 											   planstate), es);
@@ -2120,7 +2129,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_scan_qual(tidquals, "TID Cond", planstate, ancestors, es);
 				show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 				if (plan->qual)
-					show_instrumentation_count("Rows Removed by Filter", 1,
+					show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 											   planstate, es);
 			}
 			break;
@@ -2137,14 +2146,14 @@ ExplainNode(PlanState *planstate, List *ancestors,
 				show_scan_qual(tidquals, "TID Cond", planstate, ancestors, es);
 				show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 				if (plan->qual)
-					show_instrumentation_count("Rows Removed by Filter", 1,
+					show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 											   planstate, es);
 			}
 			break;
 		case T_ForeignScan:
 			show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 1,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 										   planstate, es);
 			show_foreignscan_info((ForeignScanState *) planstate, es);
 			break;
@@ -2154,7 +2163,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 
 				show_scan_qual(plan->qual, "Filter", planstate, ancestors, es);
 				if (plan->qual)
-					show_instrumentation_count("Rows Removed by Filter", 1,
+					show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 											   planstate, es);
 				if (css->methods->ExplainCustomScan)
 					css->methods->ExplainCustomScan(css, ancestors, es);
@@ -2168,7 +2177,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 										   planstate, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 2,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 2,
 										   planstate, es);
 			break;
 		case T_MergeJoin:
@@ -2181,7 +2190,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 										   planstate, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 2,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 2,
 										   planstate, es);
 			break;
 		case T_HashJoin:
@@ -2194,7 +2203,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 										   planstate, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 2,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 2,
 										   planstate, es);
 			break;
 		case T_Agg:
@@ -2202,7 +2211,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
 			show_hashagg_info((AggState *) planstate, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 1,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 										   planstate, es);
 			break;
 		case T_WindowAgg:
@@ -2211,7 +2220,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 							"Run Condition", planstate, ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 1,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 										   planstate, es);
 			show_windowagg_info(castNode(WindowAggState, planstate), es);
 			break;
@@ -2219,7 +2228,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 			show_group_keys(castNode(GroupState, planstate), ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 1,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 										   planstate, es);
 			break;
 		case T_Sort:
@@ -2242,7 +2251,7 @@ ExplainNode(PlanState *planstate, List *ancestors,
 							"One-Time Filter", planstate, ancestors, es);
 			show_upper_qual(plan->qual, "Filter", planstate, ancestors, es);
 			if (plan->qual)
-				show_instrumentation_count("Rows Removed by Filter", 1,
+				show_instrumentation_count("Rows Removed In Executor by Filter", 1,
 										   planstate, es);
 			break;
 		case T_ModifyTable:
@@ -3996,7 +4005,9 @@ show_instrumentation_count(const char *qlabel, int which,
 	if (!es->analyze || !planstate->instrument)
 		return;
 
-	if (which == 2)
+	if (which == 3)
+		nfiltered = planstate->instrument->nfiltered3;
+	else if (which == 2)
 		nfiltered = planstate->instrument->nfiltered2;
 	else
 		nfiltered = planstate->instrument->nfiltered1;
diff --git a/src/backend/executor/instrument.c b/src/backend/executor/instrument.c
index 9e11c662a7c..5c669ca5262 100644
--- a/src/backend/executor/instrument.c
+++ b/src/backend/executor/instrument.c
@@ -186,6 +186,7 @@ InstrAggNode(Instrumentation *dst, Instrumentation *add)
 	dst->nloops += add->nloops;
 	dst->nfiltered1 += add->nfiltered1;
 	dst->nfiltered2 += add->nfiltered2;
+	dst->nfiltered3 += add->nfiltered3;
 
 	/* Add delta of buffer usage since entry to node's totals */
 	if (dst->need_bufusage)
diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c
index 454d4ee0499..7fc0ae5d97a 100644
--- a/src/backend/executor/nodeSeqscan.c
+++ b/src/backend/executor/nodeSeqscan.c
@@ -29,9 +29,12 @@
 
 #include "access/relscan.h"
 #include "access/tableam.h"
+#include "executor/execExpr.h"
 #include "executor/execScan.h"
 #include "executor/executor.h"
 #include "executor/nodeSeqscan.h"
+#include "nodes/nodeFuncs.h"
+#include "utils/lsyscache.h"
 #include "utils/rel.h"
 
 static TupleTableSlot *SeqNext(SeqScanState *node);
@@ -41,6 +44,157 @@ static TupleTableSlot *SeqNext(SeqScanState *node);
  * ----------------------------------------------------------------
  */
 
+/* ----------------------------------------------------------------
+ *		ExecSeqBuildScanKeys
+ *
+ *		Builds the scan keys pushed to the table AM API. Scan keys
+ *		are used to filter out tuples before returning them to the
+ *		executor, based on the quals list.
+ * ----------------------------------------------------------------
+ */
+static void
+ExecSeqBuildScanKeys(PlanState *planstate, List *quals, int *numScanKeys,
+					 ScanKey *scanKeys, SeqScanRuntimeKeyInfo * *runtimeKeys,
+					 int *numRuntimeKeys)
+{
+	ListCell   *qual_cell;
+	ScanKey		scan_keys;
+	int			n_scan_keys = 0;
+	int			n_quals;
+	SeqScanRuntimeKeyInfo *runtime_keys;
+	int			n_runtime_keys;
+	int			max_runtime_keys;
+
+	n_quals = list_length(quals);
+
+	/*
+	 * If quals list is empty we have nothing to do.
+	 */
+	if (n_quals == 0)
+		return;
+
+	/*
+	 * Allocate an array of ScanKeyData structs: one per qual.
+	 *
+	 * Note: when we cannot convert all the quals to ScanKeys, then we waste
+	 * some memory but this avoids memory reallocation on the fly.
+	 */
+	scan_keys = (ScanKey) palloc(n_quals * sizeof(ScanKeyData));
+
+	/*
+	 * run-time_keys array is dynamically resized as needed. Caller must be
+	 * sure to pass in NULL/0 for first call.
+	 */
+	runtime_keys = *runtimeKeys;
+	n_runtime_keys = max_runtime_keys = *numRuntimeKeys;
+
+	foreach(qual_cell, quals)
+	{
+		Expr	   *clause = (Expr *) lfirst(qual_cell);
+		ScanKey		this_scan_key = &scan_keys[n_scan_keys];
+		RegProcedure opfuncid;	/* operator proc id used in scan */
+		Expr	   *leftop;		/* expr on lhs of operator */
+		Expr	   *rightop;	/* expr on rhs ... */
+		AttrNumber	varattno;	/* att number used in scan */
+
+		/*
+		 * Simple qual case: <leftop> <op> <rightop>
+		 */
+		if (IsA(clause, OpExpr))
+		{
+			int			flags = 0;
+			Datum		scanvalue;
+
+			opfuncid = ((OpExpr *) clause)->opfuncid;
+
+			/*
+			 * leftop and rightop are not relabeled and can be used as they
+			 * are because they have been pre-computed by
+			 * fix_tablequal_references(), so, the key Var is always on the
+			 * left.
+			 */
+			leftop = (Expr *) get_leftop(clause);
+			rightop = (Expr *) get_rightop(clause);
+
+			/* Left and right are not null */
+			Assert(leftop != NULL);
+			Assert(rightop != NULL);
+			/* The operator shouldn't be user defined */
+			Assert(((OpExpr *) clause)->opno < FirstNormalObjectId);
+			/* Left part is a Var */
+			Assert(IsA(leftop, Var));
+			/* Datatype is not TOASTable */
+			Assert(!TypeIsToastable(((Var *) leftop)->vartype));
+			/* Operator's function is leakproof */
+			Assert(get_func_leakproof(opfuncid));
+
+			varattno = ((Var *) leftop)->varattno;
+
+			if (IsA(rightop, Const))
+			{
+				/*
+				 * OK, simple constant comparison value
+				 */
+				scanvalue = ((Const *) rightop)->constvalue;
+				if (((Const *) rightop)->constisnull)
+					flags |= SK_ISNULL;
+			}
+			else
+			{
+				/* Need to treat this one as a run-time key */
+				if (n_runtime_keys >= max_runtime_keys)
+				{
+					if (max_runtime_keys == 0)
+					{
+						max_runtime_keys = 8;
+						runtime_keys = (SeqScanRuntimeKeyInfo *)
+							palloc(max_runtime_keys * sizeof(SeqScanRuntimeKeyInfo));
+					}
+					else
+					{
+						max_runtime_keys *= 2;
+						runtime_keys = (SeqScanRuntimeKeyInfo *)
+							repalloc(runtime_keys,
+									 max_runtime_keys * sizeof(SeqScanRuntimeKeyInfo));
+					}
+				}
+				runtime_keys[n_runtime_keys].scan_key = this_scan_key;
+				runtime_keys[n_runtime_keys].key_expr =
+					ExecInitExpr(rightop, planstate);
+				runtime_keys[n_runtime_keys].key_toastable = false;
+				n_runtime_keys++;
+				scanvalue = (Datum) 0;
+			}
+
+			n_scan_keys++;
+
+			ScanKeyEntryInitialize(this_scan_key,
+								   flags,
+								   varattno,
+								   InvalidStrategy, /* no strategy */
+								   InvalidOid,	/* no subtype */
+								   ((OpExpr *) clause)->inputcollid,
+								   opfuncid,
+								   scanvalue);
+		}
+		else
+		{
+			/*
+			 * Unsupported qual, then do not push it to the table AM.
+			 */
+			continue;
+		}
+	}
+
+	/*
+	 * Return info to our caller.
+	 */
+	*scanKeys = scan_keys;
+	*numScanKeys = n_scan_keys;
+	*runtimeKeys = runtime_keys;
+	*numRuntimeKeys = n_runtime_keys;
+}
+
 /* ----------------------------------------------------------------
  *		SeqNext
  *
@@ -71,15 +225,47 @@ SeqNext(SeqScanState *node)
 		 */
 		scandesc = table_beginscan(node->ss.ss_currentRelation,
 								   estate->es_snapshot,
-								   0, NULL);
+								   node->sss_NumScanKeys,
+								   node->sss_ScanKeys);
 		node->ss.ss_currentScanDesc = scandesc;
+
+		/*
+		 * If no run-time key to calculate or if they are ready to use, go
+		 * ahead and pass the ScanKeys to the table AM.
+		 */
+		if (node->sss_NumRuntimeKeys == 0 || node->sss_RuntimeKeysReady)
+			table_rescan(node->ss.ss_currentScanDesc, node->sss_NumScanKeys,
+						 node->sss_ScanKeys);
 	}
 
 	/*
 	 * get the next tuple from the table
 	 */
 	if (table_scan_getnextslot(scandesc, direction, slot))
+	{
+		/*
+		 * Update the instrumentation counter in charge of tracking the number
+		 * of tuples skipped during table/seq scan.
+		 *
+		 * Note: it seems necessary to do it after getting each tuple only
+		 * when the table scan is executed by the postgres_fdw. In all other
+		 * cases, we can update the counter only once when there is no next
+		 * tuple to return.
+		 */
+		InstrCountFiltered3(node, scandesc->rs_nskip);
+
+		/*
+		 * We have to reset the local counter once the instrumentation counter
+		 * has been updated.
+		 */
+		scandesc->rs_nskip = 0;
+
 		return slot;
+	}
+
+	InstrCountFiltered3(node, scandesc->rs_nskip);
+	scandesc->rs_nskip = 0;
+
 	return NULL;
 }
 
@@ -115,6 +301,15 @@ ExecSeqScan(PlanState *pstate)
 	Assert(pstate->qual == NULL);
 	Assert(pstate->ps_ProjInfo == NULL);
 
+	/*
+	 * If we have run-time keys and they've not already been set up, do it
+	 * now.
+	 */
+	if (node->sss_NumRuntimeKeys != 0 && !node->sss_RuntimeKeysReady)
+	{
+		ExecReScan((PlanState *) node);
+	}
+
 	return ExecScanExtended(&node->ss,
 							(ExecScanAccessMtd) SeqNext,
 							(ExecScanRecheckMtd) SeqRecheck,
@@ -139,6 +334,15 @@ ExecSeqScanWithQual(PlanState *pstate)
 	pg_assume(pstate->qual != NULL);
 	Assert(pstate->ps_ProjInfo == NULL);
 
+	/*
+	 * If we have run-time keys and they've not already been set up, do it
+	 * now.
+	 */
+	if (node->sss_NumRuntimeKeys != 0 && !node->sss_RuntimeKeysReady)
+	{
+		ExecReScan((PlanState *) node);
+	}
+
 	return ExecScanExtended(&node->ss,
 							(ExecScanAccessMtd) SeqNext,
 							(ExecScanRecheckMtd) SeqRecheck,
@@ -159,6 +363,15 @@ ExecSeqScanWithProject(PlanState *pstate)
 	Assert(pstate->qual == NULL);
 	pg_assume(pstate->ps_ProjInfo != NULL);
 
+	/*
+	 * If we have run-time keys and they've not already been set up, do it
+	 * now.
+	 */
+	if (node->sss_NumRuntimeKeys != 0 && !node->sss_RuntimeKeysReady)
+	{
+		ExecReScan((PlanState *) node);
+	}
+
 	return ExecScanExtended(&node->ss,
 							(ExecScanAccessMtd) SeqNext,
 							(ExecScanRecheckMtd) SeqRecheck,
@@ -180,6 +393,15 @@ ExecSeqScanWithQualProject(PlanState *pstate)
 	pg_assume(pstate->qual != NULL);
 	pg_assume(pstate->ps_ProjInfo != NULL);
 
+	/*
+	 * If we have run-time keys and they've not already been set up, do it
+	 * now.
+	 */
+	if (node->sss_NumRuntimeKeys != 0 && !node->sss_RuntimeKeysReady)
+	{
+		ExecReScan((PlanState *) node);
+	}
+
 	return ExecScanExtended(&node->ss,
 							(ExecScanAccessMtd) SeqNext,
 							(ExecScanRecheckMtd) SeqRecheck,
@@ -198,6 +420,15 @@ ExecSeqScanEPQ(PlanState *pstate)
 {
 	SeqScanState *node = castNode(SeqScanState, pstate);
 
+	/*
+	 * If we have run-time keys and they've not already been set up, do it
+	 * now.
+	 */
+	if (node->sss_NumRuntimeKeys != 0 && !node->sss_RuntimeKeysReady)
+	{
+		ExecReScan((PlanState *) node);
+	}
+
 	return ExecScan(&node->ss,
 					(ExecScanAccessMtd) SeqNext,
 					(ExecScanRecheckMtd) SeqRecheck);
@@ -225,6 +456,11 @@ ExecInitSeqScan(SeqScan *node, EState *estate, int eflags)
 	scanstate = makeNode(SeqScanState);
 	scanstate->ss.ps.plan = (Plan *) node;
 	scanstate->ss.ps.state = estate;
+	scanstate->sss_ScanKeys = NULL;
+	scanstate->sss_NumScanKeys = 0;
+	scanstate->sss_RuntimeKeysReady = false;
+	scanstate->sss_RuntimeKeys = NULL;
+	scanstate->sss_NumRuntimeKeys = 0;
 
 	/*
 	 * Miscellaneous initialization
@@ -258,6 +494,14 @@ 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);
+
 	/*
 	 * When EvalPlanQual() is not in use, assign ExecProcNode for this node
 	 * based on the presence of qual and projection. Each ExecSeqScan*()
@@ -280,6 +524,24 @@ ExecInitSeqScan(SeqScan *node, EState *estate, int eflags)
 			scanstate->ss.ps.ExecProcNode = ExecSeqScanWithQualProject;
 	}
 
+	/*
+	 * If we have runtime keys, we need an ExprContext to evaluate them. The
+	 * node's standard context won't do because we want to reset that context
+	 * for every tuple.  So, build another context just like the other one...
+	 */
+	if (scanstate->sss_NumRuntimeKeys != 0)
+	{
+		ExprContext *stdecontext = scanstate->ss.ps.ps_ExprContext;
+
+		ExecAssignExprContext(estate, &scanstate->ss.ps);
+		scanstate->sss_RuntimeContext = scanstate->ss.ps.ps_ExprContext;
+		scanstate->ss.ps.ps_ExprContext = stdecontext;
+	}
+	else
+	{
+		scanstate->sss_RuntimeContext = NULL;
+	}
+
 	return scanstate;
 }
 
@@ -322,16 +584,82 @@ ExecReScanSeqScan(SeqScanState *node)
 {
 	TableScanDesc scan;
 
+	/*
+	 * If we are doing runtime key calculations (ie, any of the scan key
+	 * values weren't simple Consts), compute the new key values.  But first,
+	 * reset the context so we don't leak memory as each outer tuple is
+	 * scanned.  Note this assumes that we will recalculate *all* runtime keys
+	 * on each call.
+	 */
+	if (node->sss_NumRuntimeKeys != 0)
+	{
+		ExprContext *econtext = node->sss_RuntimeContext;
+
+		ResetExprContext(econtext);
+		ExecSeqScanEvalRuntimeKeys(econtext,
+								   node->sss_RuntimeKeys,
+								   node->sss_NumRuntimeKeys);
+	}
+	node->sss_RuntimeKeysReady = true;
+
 	scan = node->ss.ss_currentScanDesc;
 
 	if (scan != NULL)
 		table_rescan(scan,		/* scan desc */
-					 0,			/* number of new scan keys */
-					 NULL);		/* new scan keys */
+					 node->sss_NumScanKeys, /* number of scan keys */
+					 node->sss_ScanKeys);	/* scan keys */
 
 	ExecScanReScan((ScanState *) node);
 }
 
+/* ----------------------------------------------------------------
+ * 		ExecSeqScanEvalRuntimeKeys
+ *
+ * 		Evaluate any run-time key values, and update the scankeys.
+ * ----------------------------------------------------------------
+ */
+void
+ExecSeqScanEvalRuntimeKeys(ExprContext *econtext,
+						   SeqScanRuntimeKeyInfo * runtimeKeys,
+						   int numRuntimeKeys)
+{
+	int			j;
+	MemoryContext oldContext;
+
+	/* We want to keep the key values in per-tuple memory */
+	oldContext = MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);
+
+	for (j = 0; j < numRuntimeKeys; j++)
+	{
+		ScanKey		scan_key = runtimeKeys[j].scan_key;
+		ExprState  *key_expr = runtimeKeys[j].key_expr;
+		bool		isNull;
+
+		/*
+		 * For each run-time key, extract the run-time expression and evaluate
+		 * it with respect to the current context.  We then stick the result
+		 * into the proper scan key.
+		 *
+		 * Note: the result of the eval could be a pass-by-ref value that's
+		 * stored in some outer scan's tuple, not in
+		 * econtext->ecxt_per_tuple_memory.  We assume that the outer tuple
+		 * will stay put throughout our scan.  If this is wrong, we could copy
+		 * the result into our context explicitly, but I think that's not
+		 * necessary.
+		 */
+		scan_key->sk_argument = ExecEvalExpr(key_expr,
+											 econtext,
+											 &isNull);
+
+		if (isNull)
+			scan_key->sk_flags |= SK_ISNULL;
+		else
+			scan_key->sk_flags &= ~SK_ISNULL;
+	}
+
+	MemoryContextSwitchTo(oldContext);
+}
+
 /* ----------------------------------------------------------------
  *						Parallel Scan Support
  * ----------------------------------------------------------------
@@ -375,7 +703,17 @@ ExecSeqScanInitializeDSM(SeqScanState *node,
 								  estate->es_snapshot);
 	shm_toc_insert(pcxt->toc, node->ss.ps.plan->plan_node_id, pscan);
 	node->ss.ss_currentScanDesc =
-		table_beginscan_parallel(node->ss.ss_currentRelation, pscan, 0, NULL);
+		table_beginscan_parallel(node->ss.ss_currentRelation, pscan,
+								 node->sss_NumScanKeys,
+								 node->sss_ScanKeys);
+
+	/*
+	 * If no run-time keys to calculate or they are ready, go ahead and pass
+	 * the scankeys to the table AM.
+	 */
+	if (node->sss_NumRuntimeKeys == 0 || node->sss_RuntimeKeysReady)
+		table_rescan(node->ss.ss_currentScanDesc, node->sss_NumScanKeys,
+					 node->sss_ScanKeys);
 }
 
 /* ----------------------------------------------------------------
@@ -408,5 +746,15 @@ ExecSeqScanInitializeWorker(SeqScanState *node,
 
 	pscan = shm_toc_lookup(pwcxt->toc, node->ss.ps.plan->plan_node_id, false);
 	node->ss.ss_currentScanDesc =
-		table_beginscan_parallel(node->ss.ss_currentRelation, pscan, 0, NULL);
+		table_beginscan_parallel(node->ss.ss_currentRelation, pscan,
+								 node->sss_NumScanKeys,
+								 node->sss_ScanKeys);
+
+	/*
+	 * If no run-time keys to calculate or they are ready, go ahead and pass
+	 * the scankeys to the table AM.
+	 */
+	if (node->sss_NumRuntimeKeys == 0 || node->sss_RuntimeKeysReady)
+		table_rescan(node->ss.ss_currentScanDesc, node->sss_NumScanKeys,
+					 node->sss_ScanKeys);
 }
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 8af091ba647..b7adc512189 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -20,6 +20,7 @@
 
 #include "access/sysattr.h"
 #include "catalog/pg_class.h"
+#include "executor/executor.h"
 #include "foreign/fdwapi.h"
 #include "miscadmin.h"
 #include "nodes/extensible.h"
@@ -40,8 +41,10 @@
 #include "optimizer/tlist.h"
 #include "parser/parse_clause.h"
 #include "parser/parsetree.h"
+#include "parser/parse_relation.h"
 #include "partitioning/partprune.h"
 #include "tcop/tcopprot.h"
+#include "utils/acl.h"
 #include "utils/lsyscache.h"
 
 
@@ -171,6 +174,8 @@ static Node *fix_indexqual_clause(PlannerInfo *root,
 								  IndexOptInfo *index, int indexcol,
 								  Node *clause, List *indexcolnos);
 static Node *fix_indexqual_operand(Node *node, IndexOptInfo *index, int indexcol);
+static void fix_tablequal_references(PlannerInfo *root, Path *best_path,
+									 List *scan_clauses, List **fixed_tablequals_p);
 static List *get_switched_clauses(List *clauses, Relids outerrelids);
 static List *order_qual_clauses(PlannerInfo *root, List *clauses);
 static void copy_generic_path_info(Plan *dest, Path *src);
@@ -179,7 +184,7 @@ static void label_sort_with_costsize(PlannerInfo *root, Sort *plan,
 									 double limit_tuples);
 static void label_incrementalsort_with_costsize(PlannerInfo *root, IncrementalSort *plan,
 												List *pathkeys, double limit_tuples);
-static SeqScan *make_seqscan(List *qptlist, List *qpqual, Index scanrelid);
+static SeqScan *make_seqscan(List *qptlist, List *qpqual, Index scanrelid, List *tablequal);
 static SampleScan *make_samplescan(List *qptlist, List *qpqual, Index scanrelid,
 								   TableSampleClause *tsc);
 static IndexScan *make_indexscan(List *qptlist, List *qpqual, Index scanrelid,
@@ -2755,10 +2760,16 @@ create_seqscan_plan(PlannerInfo *root, Path *best_path,
 {
 	SeqScan    *scan_plan;
 	Index		scan_relid = best_path->parent->relid;
+	List	   *fixed_tablequals = NIL;
+	RangeTblEntry *rte;
+	RTEPermissionInfo *perminfo;
+	bool		do_fix_tablequal_ref = true;
 
 	/* it should be a base rel... */
 	Assert(scan_relid > 0);
+	rte = planner_rt_fetch(scan_relid, root);
 	Assert(best_path->parent->rtekind == RTE_RELATION);
+	Assert(rte->rtekind == RTE_RELATION);
 
 	/* Sort clauses into best execution order */
 	scan_clauses = order_qual_clauses(root, scan_clauses);
@@ -2773,9 +2784,25 @@ create_seqscan_plan(PlannerInfo *root, Path *best_path,
 			replace_nestloop_params(root, (Node *) scan_clauses);
 	}
 
+	/*
+	 * Check relation permission before doing any preliminary work on quals.
+	 * If the permissions can't be checked, then we won't do unnecessary work
+	 * related to quals push down.
+	 */
+	if (rte->perminfoindex != 0)
+	{
+		perminfo = getRTEPermissionInfo(root->parse->rteperminfos, rte);
+		if (!ExecCheckOneRelPerms(perminfo))
+			do_fix_tablequal_ref = false;
+	}
+
+	if (do_fix_tablequal_ref)
+		fix_tablequal_references(root, best_path, scan_clauses, &fixed_tablequals);
+
 	scan_plan = make_seqscan(tlist,
 							 scan_clauses,
-							 scan_relid);
+							 scan_relid,
+							 fixed_tablequals);
 
 	copy_generic_path_info(&scan_plan->scan.plan, best_path);
 
@@ -5168,6 +5195,192 @@ fix_indexqual_operand(Node *node, IndexOptInfo *index, int indexcol)
 	return NULL;				/* keep compiler quiet */
 }
 
+/*
+ * Check if the right part of a qual can be used in a ScanKey that will
+ * later be pushed down during sequential scan.
+ */
+static bool inline
+check_tablequal_rightop(Expr *rightop)
+{
+	switch (nodeTag((Node *) rightop))
+	{
+			/* Supported nodes */
+		case T_Const:
+		case T_Param:
+			break;
+
+			/*
+			 * In case of function expression, make sure function args do not
+			 * contain any reference to the table being scanned.
+			 */
+		case T_FuncExpr:
+			{
+				FuncExpr   *func = (FuncExpr *) rightop;
+				ListCell   *temp;
+
+				foreach(temp, func->args)
+				{
+					Node	   *arg = lfirst(temp);
+
+					if (IsA(arg, Var) && ((Var *) arg)->varattno > 0)
+						return false;
+				}
+
+				break;
+			}
+
+			/*
+			 * In case of Var, check if this is an attribute of a relation,
+			 * which is not supported.
+			 */
+		case T_Var:
+			{
+				if (((Var *) rightop)->varattno > 0)
+					return false;
+				break;
+			}
+			/* Unsupported nodes */
+		default:
+			return false;
+			break;
+	}
+
+	return true;
+}
+
+/*
+ * fix_tablequal_references
+ *    Precompute scan clauses in order to pass them ready to be pushed down by
+ *    the executor during table scan.
+ *
+ * We do left/right commutation if needed because we want to keep the scan key
+ * on left.
+ */
+static void
+fix_tablequal_references(PlannerInfo *root, Path *best_path,
+						 List *scan_clauses, List **fixed_tablequals_p)
+{
+	List	   *fixed_tablequals;
+	ListCell   *lc;
+
+	fixed_tablequals = NIL;
+
+	scan_clauses = (List *) replace_nestloop_params(root, (Node *) scan_clauses);
+
+	foreach(lc, scan_clauses)
+	{
+		/*
+		 * Let work with a "deep" copy of the original scan clause in order to
+		 * avoid any update on the initial scan clause.
+		 */
+		Expr	   *clause = (Expr *) copyObject(lfirst(lc));
+
+		switch (nodeTag((Node *) clause))
+		{
+				/*
+				 * Simple qual case: <leftop> <op> <rightop>
+				 */
+			case T_OpExpr:
+				{
+					OpExpr	   *opexpr = (OpExpr *) clause;
+					Expr	   *leftop;
+					Expr	   *rightop;
+
+					leftop = (Expr *) get_leftop(clause);
+					rightop = (Expr *) get_rightop(clause);
+
+					if (leftop && IsA(leftop, RelabelType))
+						leftop = ((RelabelType *) leftop)->arg;
+
+					if (rightop && IsA(rightop, RelabelType))
+						rightop = ((RelabelType *) rightop)->arg;
+
+					if (leftop == NULL || rightop == NULL)
+						continue;
+
+					/*
+					 * Ignore qual if the operator is user defined
+					 */
+					if (opexpr->opno >= FirstNormalObjectId)
+						continue;
+
+					/*
+					 * Ignore qual if the function is not leakproof
+					 */
+					if (!get_func_leakproof(opexpr->opfuncid))
+						continue;
+
+					/*
+					 * Commute left and right if needed and reflect those
+					 * changes on the clause, this way, the executor won't
+					 * have to check positions of Var and Const/other: Var is
+					 * always on the left while Const/other is on the right.
+					 */
+					if (IsA(rightop, Var) && !IsA(leftop, Var)
+						&& ((Var *) rightop)->varattno > 0)
+					{
+						Expr	   *tmpop = leftop;
+						Oid			commutator;
+
+						leftop = rightop;
+						rightop = tmpop;
+
+						commutator = get_commutator(opexpr->opno);
+
+						if (OidIsValid(commutator))
+						{
+							opexpr->opno = commutator;
+							opexpr->opfuncid = get_opcode(opexpr->opno);
+						}
+						else
+						{
+							/*
+							 * If we don't have any commutator function
+							 * available for this operator, then ignore the
+							 * qual because we cannot commute it.
+							 */
+							continue;
+						}
+					}
+
+					/*
+					 * Make sure our left part is a Var referencing an
+					 * attribute.
+					 */
+					if (!(IsA(leftop, Var) && ((Var *) leftop)->varattno > 0))
+						continue;
+
+					/*
+					 * Make sure the var type is not TOASTable as we don't
+					 * want to deal with potentially TOASTed data when
+					 * evaluating the scan keys.
+					 */
+					if (TypeIsToastable(((Var *) leftop)->vartype))
+						continue;
+
+					if (!check_tablequal_rightop(rightop))
+						continue;
+
+					/*
+					 * Even if there is no left/right commutation, update the
+					 * clause in order to avoid unnecessary checks by the
+					 * executor.
+					 */
+					list_free(opexpr->args);
+					opexpr->args = list_make2(leftop, rightop);
+
+					/* Append the modified clause to fixed_tablequals */
+					fixed_tablequals = lappend(fixed_tablequals, clause);
+					break;
+				}
+			default:
+				continue;
+		}
+	}
+
+	*fixed_tablequals_p = fixed_tablequals;
+}
+
 /*
  * get_switched_clauses
  *	  Given a list of merge or hash joinclauses (as RestrictInfo nodes),
@@ -5480,7 +5693,8 @@ bitmap_subplan_mark_shared(Plan *plan)
 static SeqScan *
 make_seqscan(List *qptlist,
 			 List *qpqual,
-			 Index scanrelid)
+			 Index scanrelid,
+			 List *tablequal)
 {
 	SeqScan    *node = makeNode(SeqScan);
 	Plan	   *plan = &node->scan.plan;
@@ -5490,6 +5704,7 @@ make_seqscan(List *qptlist,
 	plan->lefttree = NULL;
 	plan->righttree = NULL;
 	node->scan.scanrelid = scanrelid;
+	node->tablequal = tablequal;
 
 	return node;
 }
diff --git a/src/include/access/relscan.h b/src/include/access/relscan.h
index 87a8be10461..a8dcc00c8a0 100644
--- a/src/include/access/relscan.h
+++ b/src/include/access/relscan.h
@@ -65,6 +65,7 @@ typedef struct TableScanDescData
 
 	struct ParallelTableScanDescData *rs_parallel;	/* parallel scan
 													 * information */
+	uint64		rs_nskip;		/* number of tuples skipped during table scan */
 } TableScanDescData;
 typedef struct TableScanDescData *TableScanDesc;
 
diff --git a/src/include/executor/instrument.h b/src/include/executor/instrument.h
index ffe470f2b84..48114f2d5df 100644
--- a/src/include/executor/instrument.h
+++ b/src/include/executor/instrument.h
@@ -88,8 +88,11 @@ typedef struct Instrumentation
 	double		ntuples;		/* total tuples produced */
 	double		ntuples2;		/* secondary node-specific tuple counter */
 	double		nloops;			/* # of run cycles for this node */
-	double		nfiltered1;		/* # of tuples removed by scanqual or joinqual */
-	double		nfiltered2;		/* # of tuples removed by "other" quals */
+	double		nfiltered1;		/* # of tuples in executor removed by scanqual
+								 * or joinqual */
+	double		nfiltered2;		/* # of tuples in executor removed by "other"
+								 * quals */
+	double		nfiltered3;		/* # of tuples in table AM removed by quals */
 	BufferUsage bufusage;		/* total buffer usage */
 	WalUsage	walusage;		/* total WAL usage */
 } Instrumentation;
diff --git a/src/include/executor/nodeSeqscan.h b/src/include/executor/nodeSeqscan.h
index 3adad8b585b..6285ebc0e58 100644
--- a/src/include/executor/nodeSeqscan.h
+++ b/src/include/executor/nodeSeqscan.h
@@ -20,6 +20,9 @@
 extern SeqScanState *ExecInitSeqScan(SeqScan *node, EState *estate, int eflags);
 extern void ExecEndSeqScan(SeqScanState *node);
 extern void ExecReScanSeqScan(SeqScanState *node);
+extern void ExecSeqScanEvalRuntimeKeys(ExprContext *econtext,
+									   SeqScanRuntimeKeyInfo * runtimeKeys,
+									   int numRuntimeKeys);
 
 /* parallel scan support */
 extern void ExecSeqScanEstimate(SeqScanState *node, ParallelContext *pcxt);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 64ff6996431..9b136ab43eb 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1276,6 +1276,11 @@ typedef struct PlanState
 		if (((PlanState *)(node))->instrument) \
 			((PlanState *)(node))->instrument->nfiltered2 += (delta); \
 	} while(0)
+#define InstrCountFiltered3(node, delta) \
+	do { \
+		if (((PlanState *)(node))->instrument) \
+			((PlanState *)(node))->instrument->nfiltered3 += (delta); \
+	} while(0)
 
 /*
  * EPQState is state for executing an EvalPlanQual recheck on a candidate
@@ -1624,14 +1629,38 @@ typedef struct ScanState
 	TupleTableSlot *ss_ScanTupleSlot;
 } ScanState;
 
+typedef struct RuntimeKeyInfo
+{
+	ScanKeyData *scan_key;		/* scankey to put value into */
+	ExprState  *key_expr;		/* expr to evaluate to get value */
+	bool		key_toastable;	/* is expr's result a toastable datatype? */
+}			RuntimeKeyInfo;
+
+typedef struct RuntimeKeyInfo SeqScanRuntimeKeyInfo;
+
 /* ----------------
  *	 SeqScanState information
+ *
+ *		ss					its first field is NodeTag
+ *		pscan_len			size of parallel heap scan descriptor
+ *		sss_ScanKeys		Skeys array used to push down quals
+ *		sss_NumScanKeys		number of Skeys
+ *		sss_RuntimeKeys		info about Skeys that must be evaluated at runtime
+ *		sss_NumRuntimeKeys	number of RuntimeKeys
+ *		sss_RuntimeKeysReady true if runtime Skeys have been computed
+ *		sss_RuntimeContext	expr context for evaling runtime Skeys
  * ----------------
  */
 typedef struct SeqScanState
 {
-	ScanState	ss;				/* its first field is NodeTag */
-	Size		pscan_len;		/* size of parallel heap scan descriptor */
+	ScanState	ss;
+	Size		pscan_len;
+	struct ScanKeyData *sss_ScanKeys;
+	int			sss_NumScanKeys;
+	SeqScanRuntimeKeyInfo *sss_RuntimeKeys;
+	int			sss_NumRuntimeKeys;
+	bool		sss_RuntimeKeysReady;
+	ExprContext *sss_RuntimeContext;
 } SeqScanState;
 
 /* ----------------
@@ -1660,12 +1689,7 @@ typedef struct SampleScanState
  * constant right-hand sides.  See comments for ExecIndexBuildScanKeys()
  * for discussion.
  */
-typedef struct
-{
-	ScanKeyData *scan_key;		/* scankey to put value into */
-	ExprState  *key_expr;		/* expr to evaluate to get value */
-	bool		key_toastable;	/* is expr's result a toastable datatype? */
-} IndexRuntimeKeyInfo;
+typedef struct RuntimeKeyInfo IndexRuntimeKeyInfo;
 
 typedef struct
 {
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index c4393a94321..fcbe482c770 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -530,6 +530,8 @@ typedef struct Scan
 typedef struct SeqScan
 {
 	Scan		scan;
+	/* list of quals (usually OpExprs) pushed down to the table AM */
+	List	   *tablequal;
 } SeqScan;
 
 /* ----------------
diff --git a/src/test/regress/expected/memoize.out b/src/test/regress/expected/memoize.out
index 00c30b91459..c116c3945ef 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 by Filter: 9000
+               Rows Removed In Table AM 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 by Filter: 9000
+               Rows Removed In Table AM by Filter: 9000
          ->  Memoize (actual rows=1.00 loops=N)
                Cache Key: t1.twenty
                Cache Mode: binary
@@ -117,7 +117,7 @@ WHERE t1.unique1 < 10;', false);
                Hits: 8  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Subquery Scan on t2 (actual rows=2.00 loops=N)
                      Filter: (t1.two = t2.two)
-                     Rows Removed by Filter: 2
+                     Rows Removed In Executor by Filter: 2
                      ->  Index Scan using tenk1_unique1 on tenk1 t2_1 (actual rows=4.00 loops=N)
                            Index Cond: (unique1 < 4)
                            Index Searches: N
@@ -146,14 +146,14 @@ 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 by Filter: 9000
+               Rows Removed In Table AM by Filter: 9000
          ->  Memoize (actual rows=1.00 loops=N)
                Cache Key: (t1.two + 1)
                Cache Mode: binary
                Hits: 998  Misses: 2  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Index Only Scan using tenk1_unique1 on tenk1 t2 (actual rows=1.00 loops=N)
                      Filter: ((t1.two + 1) = unique1)
-                     Rows Removed by Filter: 9999
+                     Rows Removed In Executor by Filter: 9999
                      Heap Fetches: N
                      Index Searches: N
 (14 rows)
@@ -179,15 +179,16 @@ 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 by Filter: 9000
+               Rows Removed In Table AM 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 by Filter: 9999
-(12 rows)
+                     Rows Removed In Table AM by Filter: 5000
+                     Rows Removed In Executor by Filter: 4999
+(13 rows)
 
 -- And check we get the expected results.
 SELECT COUNT(*), AVG(t1.twenty) FROM tenk1 t1 LEFT JOIN
@@ -246,7 +247,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 by Filter: 8800
+               Rows Removed In Table AM by Filter: 8800
          ->  Memoize (actual rows=1.00 loops=N)
                Cache Key: t2.thousand
                Cache Mode: logical
@@ -522,7 +523,7 @@ WHERE t2.a IS NULL;', false);
                Hits: 97  Misses: 3  Evictions: Zero  Overflows: 0  Memory Usage: NkB
                ->  Subquery Scan on t2 (actual rows=0.67 loops=N)
                      Filter: ((t1.a + 1) = t2.a)
-                     Rows Removed by Filter: 2
+                     Rows Removed In Executor by Filter: 2
                      ->  Unique (actual rows=2.67 loops=N)
                            ->  Sort (actual rows=67.33 loops=N)
                                  Sort Key: t2_1.a
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index 9cb1d87066a..123b063716f 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 by Filter: 54
+                     Rows Removed In Table AM 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 deacdd75807..943cc9131ae 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2346,16 +2346,16 @@ explain (analyze, costs off, summary off, timing off, buffers off) select * from
  Append (actual rows=0.00 loops=1)
    ->  Seq Scan on list_part1 list_part_1 (actual rows=0.00 loops=1)
          Filter: (a = (list_part_fn(1) + a))
-         Rows Removed by Filter: 1
+         Rows Removed In Executor by Filter: 1
    ->  Seq Scan on list_part2 list_part_2 (actual rows=0.00 loops=1)
          Filter: (a = (list_part_fn(1) + a))
-         Rows Removed by Filter: 1
+         Rows Removed In Executor by Filter: 1
    ->  Seq Scan on list_part3 list_part_3 (actual rows=0.00 loops=1)
          Filter: (a = (list_part_fn(1) + a))
-         Rows Removed by Filter: 1
+         Rows Removed In Executor by Filter: 1
    ->  Seq Scan on list_part4 list_part_4 (actual rows=0.00 loops=1)
          Filter: (a = (list_part_fn(1) + a))
-         Rows Removed by Filter: 1
+         Rows Removed In Executor by Filter: 1
 (13 rows)
 
 rollback;
@@ -2381,7 +2381,7 @@ begin
     loop
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
-        ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        ln := regexp_replace(ln, 'Rows Removed In Executor by Filter: \d+', 'Rows Removed In Executor by Filter: N');
         perform regexp_matches(ln, 'Index Searches: \d+');
         if found then
           continue;
@@ -2623,7 +2623,7 @@ select explain_parallel_append('select avg(ab.a) from ab inner join lprt_a a on
                ->  Nested Loop (actual rows=N loops=N)
                      ->  Parallel Seq Scan on lprt_a a (actual rows=N loops=N)
                            Filter: (a = ANY ('{1,0,0}'::integer[]))
-                           Rows Removed by Filter: N
+                           Rows Removed In Executor by Filter: N
                      ->  Append (actual rows=N loops=N)
                            ->  Index Scan using ab_a1_b1_a_idx on ab_a1_b1 ab_1 (actual rows=N loops=N)
                                  Index Cond: (a = a.a)
@@ -2657,7 +2657,7 @@ select explain_parallel_append('select avg(ab.a) from ab inner join lprt_a a on
                ->  Nested Loop (actual rows=N loops=N)
                      ->  Parallel Seq Scan on lprt_a a (actual rows=N loops=N)
                            Filter: (a = ANY ('{1,0,0}'::integer[]))
-                           Rows Removed by Filter: N
+                           Rows Removed In Executor by Filter: N
                      ->  Append (actual rows=N loops=N)
                            ->  Index Scan using ab_a1_b1_a_idx on ab_a1_b1 ab_1 (never executed)
                                  Index Cond: (a = a.a)
@@ -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 by Filter: 1
+         Rows Removed In Table AM 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)
@@ -3543,7 +3543,7 @@ select * from boolp where a = (select value from boolvalues where value);
    InitPlan expr_1
      ->  Seq Scan on boolvalues (actual rows=1.00 loops=1)
            Filter: value
-           Rows Removed by Filter: 1
+           Rows Removed In Executor by Filter: 1
    ->  Seq Scan on boolp_f boolp_1 (never executed)
          Filter: (a = (InitPlan expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (actual rows=0.00 loops=1)
@@ -3558,7 +3558,7 @@ select * from boolp where a = (select value from boolvalues where not value);
    InitPlan expr_1
      ->  Seq Scan on boolvalues (actual rows=1.00 loops=1)
            Filter: (NOT value)
-           Rows Removed by Filter: 1
+           Rows Removed In Executor by Filter: 1
    ->  Seq Scan on boolp_f boolp_1 (actual rows=0.00 loops=1)
          Filter: (a = (InitPlan expr_1).col1)
    ->  Seq Scan on boolp_t boolp_2 (never executed)
@@ -3587,11 +3587,11 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Subplans Removed: 1
    ->  Index Scan using ma_test_p2_b_idx on ma_test_p2 ma_test_1 (actual rows=1.00 loops=1)
          Filter: ((a >= $1) AND ((a % 10) = 5))
-         Rows Removed by Filter: 9
+         Rows Removed In Executor by Filter: 9
          Index Searches: 1
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_2 (actual rows=1.00 loops=1)
          Filter: ((a >= $1) AND ((a % 10) = 5))
-         Rows Removed by Filter: 9
+         Rows Removed In Executor by Filter: 9
          Index Searches: 1
 (11 rows)
 
@@ -3610,7 +3610,7 @@ explain (analyze, costs off, summary off, timing off, buffers off) execute mt_q1
    Subplans Removed: 2
    ->  Index Scan using ma_test_p3_b_idx on ma_test_p3 ma_test_1 (actual rows=1.00 loops=1)
          Filter: ((a >= $1) AND ((a % 10) = 5))
-         Rows Removed by Filter: 9
+         Rows Removed In Executor by Filter: 9
          Index Searches: 1
 (7 rows)
 
@@ -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 (never executed)
+     ->  Result (actual rows=1.00 loops=1)
 (4 rows)
 
 --
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 933921d1860..3467feea2d3 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 by Filter: 9990
+               Rows Removed In Table AM 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 by Filter: 40
+                     Rows Removed In Table AM by Filter: 40
 (11 rows)
 
 alter table tenk2 reset (parallel_workers);
diff --git a/src/test/regress/sql/partition_prune.sql b/src/test/regress/sql/partition_prune.sql
index d93c0c03bab..b939d725e91 100644
--- a/src/test/regress/sql/partition_prune.sql
+++ b/src/test/regress/sql/partition_prune.sql
@@ -587,7 +587,7 @@ begin
     loop
         ln := regexp_replace(ln, 'Workers Launched: \d+', 'Workers Launched: N');
         ln := regexp_replace(ln, 'actual rows=\d+(?:\.\d+)? loops=\d+', 'actual rows=N loops=N');
-        ln := regexp_replace(ln, 'Rows Removed by Filter: \d+', 'Rows Removed by Filter: N');
+        ln := regexp_replace(ln, 'Rows Removed In Executor by Filter: \d+', 'Rows Removed In Executor by Filter: N');
         perform regexp_matches(ln, 'Index Searches: \d+');
         if found then
           continue;
-- 
2.39.5

