From 7988159e44d7786769bc2cb4fc7ab243e32a31a5 Mon Sep 17 00:00:00 2001
From: Julien Tachoires <julien@tachoires.me>
Date: Mon, 25 Aug 2025 18:23:19 +0200
Subject: [PATCH 2/6] 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              |  30 +-
 src/backend/commands/explain.c                |  51 ++-
 src/backend/executor/instrument.c             |   1 +
 src/backend/executor/nodeSeqscan.c            | 356 +++++++++++++++++-
 src/backend/optimizer/plan/createplan.c       | 213 ++++++++++-
 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/isolation/expected/stats.out         |  26 +-
 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/expected/updatable_views.out |   3 -
 src/test/regress/sql/partition_prune.sql      |   2 +-
 18 files changed, 701 insertions(+), 93 deletions(-)

diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index d3323b04676..bc7242835df 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 by Filter: 1
+         Rows Removed In Table AM 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)
@@ -12225,7 +12225,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 a5c74d8948e..71d8e06d8dd 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -347,7 +347,8 @@ bitmapheap_stream_read_next(ReadStream *pgsr, void *private_data,
  * ----------------
  */
 static void
-initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock)
+initscan(HeapScanDesc scan, int nkeys, ScanKey keys, bool keep_startblock,
+		 bool update_stats)
 {
 	ParallelBlockTableScanDesc bpscan = NULL;
 	bool		allow_strat;
@@ -456,17 +457,20 @@ initscan(HeapScanDesc scan, ScanKey key, bool keep_startblock)
 	/* page-at-a-time fields are always invalid when not rs_inited */
 
 	/*
-	 * copy the scan key, if appropriate
+	 * copy the scan keys, if appropriate
 	 */
-	if (key != NULL && scan->rs_base.rs_nkeys > 0)
-		memcpy(scan->rs_base.rs_key, key, scan->rs_base.rs_nkeys * sizeof(ScanKeyData));
+	if (keys != NULL && nkeys > 0)
+	{
+		scan->rs_base.rs_nkeys = nkeys;
+		memcpy(scan->rs_base.rs_key, keys, nkeys * sizeof(ScanKeyData));
+	}
 
 	/*
 	 * Currently, we only have a stats counter for sequential heap scans (but
 	 * 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);
 }
 
@@ -964,7 +968,10 @@ continue_page:
 			if (key != NULL &&
 				!HeapKeyTest(tuple, RelationGetDescr(scan->rs_base.rs_rd),
 							 nkeys, key))
+			{
+				scan->rs_base.rs_nskip++;
 				continue;
+			}
 
 			LockBuffer(scan->rs_cbuf, BUFFER_LOCK_UNLOCK);
 			scan->rs_coffset = lineoff;
@@ -1072,7 +1079,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;
@@ -1098,7 +1108,7 @@ continue_page:
 
 TableScanDesc
 heap_beginscan(Relation relation, Snapshot snapshot,
-			   int nkeys, ScanKey key,
+			   int nkeys, ScanKey keys,
 			   ParallelTableScanDesc parallel_scan,
 			   uint32 flags)
 {
@@ -1132,6 +1142,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 */
@@ -1199,7 +1210,7 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 	else
 		scan->rs_base.rs_key = NULL;
 
-	initscan(scan, key, false);
+	initscan(scan, nkeys, keys, false, true);
 
 	scan->rs_read_stream = NULL;
 
@@ -1251,7 +1262,7 @@ heap_beginscan(Relation relation, Snapshot snapshot,
 }
 
 void
-heap_rescan(TableScanDesc sscan, int nkeys, ScanKey key, bool set_params,
+heap_rescan(TableScanDesc sscan, int nkeys, ScanKey keys, bool set_params,
 			bool allow_strat, bool allow_sync, bool allow_pagemode)
 {
 	HeapScanDesc scan = (HeapScanDesc) sscan;
@@ -1297,10 +1308,11 @@ heap_rescan(TableScanDesc sscan, int nkeys, ScanKey key, bool set_params,
 	if (scan->rs_read_stream)
 		read_stream_reset(scan->rs_read_stream);
 
+
 	/*
 	 * reinitialize scan descriptor
 	 */
-	initscan(scan, key, true);
+	initscan(scan, nkeys, keys, true, false);
 }
 
 void
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 8345bc0264b..a3c8889632a 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -1963,7 +1963,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;
@@ -1977,7 +1977,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,
@@ -1997,7 +1997,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;
@@ -2007,6 +2007,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:
@@ -2014,7 +2023,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);
@@ -2025,7 +2034,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);
@@ -2049,7 +2058,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);
@@ -2083,7 +2092,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:
@@ -2097,7 +2106,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);
@@ -2115,7 +2124,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;
@@ -2132,14 +2141,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;
@@ -2149,7 +2158,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);
@@ -2163,7 +2172,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:
@@ -2176,7 +2185,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:
@@ -2189,7 +2198,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:
@@ -2197,7 +2206,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:
@@ -2206,7 +2215,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;
@@ -2214,7 +2223,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:
@@ -2236,7 +2245,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:
@@ -3990,7 +3999,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 56e635f4700..e9ddf39c42a 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 c89aa6c6616..0562377c42a 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,146 @@ 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);
+
+			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 =
+					TypeIsToastable(((Var *) leftop)->vartype);
+				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 +214,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 +290,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 +323,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 +352,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 +382,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 +409,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 +445,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 +483,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 +513,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 +573,91 @@ 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 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;
+		Datum		scanvalue;
+		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.
+		 */
+		scanvalue = ExecEvalExpr(key_expr,
+								 econtext,
+								 &isNull);
+		if (isNull)
+		{
+			scan_key->sk_argument = scanvalue;
+			scan_key->sk_flags |= SK_ISNULL;
+		}
+		else
+		{
+			if (runtimeKeys[j].key_toastable)
+				scanvalue = PointerGetDatum(PG_DETOAST_DATUM(scanvalue));
+			scan_key->sk_argument = scanvalue;
+			scan_key->sk_flags &= ~SK_ISNULL;
+		}
+	}
+
+	MemoryContextSwitchTo(oldContext);
+}
+
 /* ----------------------------------------------------------------
  *						Parallel Scan Support
  * ----------------------------------------------------------------
@@ -375,7 +701,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 +744,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 6791cbeb416..bb0856ac0bc 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"
 
 
@@ -170,6 +173,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);
@@ -178,7 +183,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,
@@ -2760,10 +2765,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);
@@ -2778,9 +2789,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);
 
@@ -5172,6 +5199,184 @@ 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;
+
+					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),
@@ -5484,7 +5689,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;
@@ -5494,6 +5700,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 b5e0fb386c0..9549bc29f38 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 03653ab6c6c..8e07a57a767 100644
--- a/src/include/executor/instrument.h
+++ b/src/include/executor/instrument.h
@@ -87,8 +87,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 de782014b2d..ec2d42c57b4 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -1273,6 +1273,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
@@ -1621,14 +1626,38 @@ typedef struct ScanState
 	TupleTableSlot *ss_ScanTupleSlot;
 } ScanState;
 
+typedef struct RuntimeKeyInfo
+{
+	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? */
+}			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;
 
 /* ----------------
@@ -1657,12 +1686,7 @@ typedef struct SampleScanState
  * constant right-hand sides.  See comments for ExecIndexBuildScanKeys()
  * for discussion.
  */
-typedef struct
-{
-	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 29d7732d6a0..595bb7f5e5a 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -501,6 +501,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/isolation/expected/stats.out b/src/test/isolation/expected/stats.out
index 8c7fe60217e..0064c0c8df0 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|           6|        1|        1|        0|         1|         1|           0
+       3|           5|        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|           5|        2|        0|        1|         1|         1|           0
+       3|           4|        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|           9|        2|        1|        1|         1|         2|           0
+       5|           7|        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|          31|        4|        5|        1|         3|         6|           0
+       9|          13|        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|          31|        4|        5|        1|         3|         6|           0
+       9|          13|        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|          29|        4|        5|        1|         1|         8|           0
+       9|          11|        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|          29|        4|        5|        1|         1|         8|           0
+       9|          11|        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|           9|        5|        1|        0|         1|         1|           0
+       3|           3|        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|           9|        5|        1|        0|         1|         1|           0
+       3|           3|        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|           9|        4|        2|        0|         4|         2|           0
+       3|           3|        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|           9|        4|        2|        0|         4|         2|           0
+       3|           3|        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|          16|        5|        3|        1|         4|         4|           0
+       4|           4|        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|          16|        5|        3|        1|         4|         4|           0
+       4|           4|        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 150dc1b44cf..f2a92d1fdfd 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 cf2219df754..f8b9172df20 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 d1966cd7d82..c633e7089ce 100644
--- a/src/test/regress/expected/partition_prune.out
+++ b/src/test/regress/expected/partition_prune.out
@@ -2333,16 +2333,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;
@@ -2368,7 +2368,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;
@@ -2610,7 +2610,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)
@@ -2644,7 +2644,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)
@@ -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 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 1).col1))
    ->  Seq Scan on ab_a1_b2 ab_5 (never executed)
@@ -3529,7 +3529,7 @@ select * from boolp where a = (select value from boolvalues where value);
    InitPlan 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 1).col1)
    ->  Seq Scan on boolp_t boolp_2 (actual rows=0.00 loops=1)
@@ -3544,7 +3544,7 @@ select * from boolp where a = (select value from boolvalues where not value);
    InitPlan 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 1).col1)
    ->  Seq Scan on boolp_t boolp_2 (never executed)
@@ -3573,11 +3573,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)
 
@@ -3596,7 +3596,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)
 
@@ -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 (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 0185ef661b1..572337c7b77 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/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 095df0a670c..8d513926b3b 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2931,7 +2931,6 @@ $$
 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 
 --------
@@ -2941,10 +2940,8 @@ 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);
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

