From 13cd2db9ffcbb8482ea792fa1dc41aa4a234a278 Mon Sep 17 00:00:00 2001
From: Julien Tachoires <julien@tachoires.me>
Date: Tue, 2 Dec 2025 10:48:42 +0100
Subject: [PATCH 7/7] Push down IS/IS NOT NULL quals to table AMs

This commit adds suuport for IS/IS NOT NULL where clauses push down
to table AMs during table scan.
---
 src/backend/access/heap/heapam_valid.c      |  16 ++-
 src/backend/executor/nodeSeqscan.c          |  77 +++++++++--
 src/backend/optimizer/plan/createplan.c     |  30 +++++
 src/test/regress/expected/qual_pushdown.out | 141 +++++++++++++-------
 src/test/regress/sql/qual_pushdown.sql      |   7 +
 5 files changed, 209 insertions(+), 62 deletions(-)

diff --git a/src/backend/access/heap/heapam_valid.c b/src/backend/access/heap/heapam_valid.c
index 7261723e378..a05738a9144 100644
--- a/src/backend/access/heap/heapam_valid.c
+++ b/src/backend/access/heap/heapam_valid.c
@@ -129,7 +129,12 @@ HeapKeyTest(HeapTuple tuple, TupleDesc tupdesc, int nkeys, ScanKey keys)
 		bool		isnull;
 		Datum		test;
 
-		if (cur_key->sk_flags & SK_ISNULL)
+		/*
+		 * When the SK_ISNULL flag is set but we are not handling the IS/IS
+		 * NOT NULL case
+		 */
+		if ((cur_key->sk_flags & SK_ISNULL)
+			&& !(cur_key->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL)))
 			return false;
 
 		atp = heap_getattr(tuple, cur_key->sk_attno, tupdesc, &isnull);
@@ -272,6 +277,15 @@ HeapKeyTest(HeapTuple tuple, TupleDesc tupdesc, int nkeys, ScanKey keys)
 
 			return false;
 		}
+		/* IS/IS NOT NULL case */
+		else if ((cur_key->sk_flags & SK_ISNULL)
+				 && (cur_key->sk_flags & (SK_SEARCHNULL | SK_SEARCHNOTNULL)))
+		{
+			if ((cur_key->sk_flags & SK_SEARCHNULL) && !isnull)
+				return false;
+			if ((cur_key->sk_flags & SK_SEARCHNOTNULL) && isnull)
+				return false;
+		}
 		else
 		{
 			if (isnull)
diff --git a/src/backend/executor/nodeSeqscan.c b/src/backend/executor/nodeSeqscan.c
index b5f0d8c23b9..30a9b092267 100644
--- a/src/backend/executor/nodeSeqscan.c
+++ b/src/backend/executor/nodeSeqscan.c
@@ -167,6 +167,18 @@ ExecSeqBuildScanKeys(PlanState *planstate, List *quals, int *numScanKeys,
 				n_runtime_keys++;
 				scanvalue = (Datum) 0;
 			}
+
+			n_scan_keys++;
+
+			ScanKeyEntryInitialize(this_scan_key,
+								   flags,
+								   varattno,
+								   InvalidStrategy, /* no strategy */
+								   InvalidOid,	/* no subtype */
+								   collationid,
+								   opfuncid,
+								   scanvalue);
+
 		}
 		/* <leftop> <op> ANY/ALL (array-expression) */
 		else if (IsA(clause, ScalarArrayOpExpr))
@@ -310,6 +322,58 @@ ExecSeqBuildScanKeys(PlanState *planstate, List *quals, int *numScanKeys,
 									 saop->inputcollid,
 									 NULL,
 									 NULL);
+
+			n_scan_keys++;
+
+			ScanKeyEntryInitialize(this_scan_key,
+								   flags,
+								   varattno,
+								   InvalidStrategy, /* no strategy */
+								   InvalidOid,	/* no subtype */
+								   collationid,
+								   opfuncid,
+								   scanvalue);
+
+			ScanKeyEntrySetHashInfo(this_scan_key, skeyhashinfo);
+
+		}
+		/* <leftop> IS/IS NOT NULL */
+		else if (IsA(clause, NullTest))
+		{
+			NullTest   *ntest = (NullTest *) clause;
+
+			leftop = ntest->arg;
+			collationid = InvalidOid;
+
+			varattno = ((Var *) leftop)->varattno;
+
+			/*
+			 * initialize the scan key's fields appropriately
+			 */
+			switch (ntest->nulltesttype)
+			{
+				case IS_NULL:
+					flags = SK_ISNULL | SK_SEARCHNULL;
+					break;
+				case IS_NOT_NULL:
+					flags = SK_ISNULL | SK_SEARCHNOTNULL;
+					break;
+				default:
+					elog(ERROR, "unrecognized nulltesttype: %d",
+						 (int) ntest->nulltesttype);
+					break;
+			}
+
+			n_scan_keys++;
+
+			ScanKeyEntryInitialize(this_scan_key,
+								   flags,
+								   varattno,
+								   InvalidStrategy, /* no strategy */
+								   InvalidOid,	/* no subtype */
+								   InvalidOid,	/* no collation */
+								   InvalidOid,	/* no reg proc for this */
+								   (Datum) 0);	/* constant */
 		}
 		else
 		{
@@ -318,19 +382,6 @@ ExecSeqBuildScanKeys(PlanState *planstate, List *quals, int *numScanKeys,
 			 */
 			continue;
 		}
-
-		n_scan_keys++;
-
-		ScanKeyEntryInitialize(this_scan_key,
-							   flags,
-							   varattno,
-							   InvalidStrategy, /* no strategy */
-							   InvalidOid,	/* no subtype */
-							   collationid,
-							   opfuncid,
-							   scanvalue);
-
-		ScanKeyEntrySetHashInfo(this_scan_key, skeyhashinfo);
 	}
 
 	/*
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 982bdd08e74..1b6fba85ba7 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -5433,6 +5433,36 @@ fix_tablequal_references(PlannerInfo *root, Path *best_path,
 					fixed_tablequals = lappend(fixed_tablequals, clause);
 					break;
 				}
+
+				/*
+				 * NullTest: <leftop> IS/IS NOT NULL
+				 */
+			case T_NullTest:
+				{
+					NullTest   *nt = (NullTest *) clause;
+					Expr	   *leftop;
+
+					leftop = (Expr *) nt->arg;
+
+					/*
+					 * Handle relabeling and make sure our left part is a
+					 * column name.
+					 */
+					if (leftop && IsA(leftop, RelabelType))
+						leftop = ((RelabelType *) leftop)->arg;
+
+					if (leftop == NULL)
+						continue;
+
+					if (!(IsA(leftop, Var) && ((Var *) leftop)->varattno > 0))
+						continue;
+
+					/* Override Null test arg in case of relabeling */
+					nt->arg = leftop;
+
+					fixed_tablequals = lappend(fixed_tablequals, clause);
+					break;
+				}
 			default:
 				continue;
 		}
diff --git a/src/test/regress/expected/qual_pushdown.out b/src/test/regress/expected/qual_pushdown.out
index 75fc9c93ad0..60baa3f4275 100644
--- a/src/test/regress/expected/qual_pushdown.out
+++ b/src/test/regress/expected/qual_pushdown.out
@@ -6,16 +6,17 @@ CREATE TABLE qa (i INTEGER, ii INTEGER);
 CREATE TABLE qb (j INTEGER);
 INSERT INTO qa SELECT n, n * n  FROM generate_series(1, 1000) as n;
 INSERT INTO qb SELECT n FROM generate_series(1, 1000) as n;
+INSERT INTO qa VALUES (1001, NULL);
 ANALYZE qa;
 ANALYZE qb;
 -- By default, the quals are not pushed down. The tuples are filtered out by
 -- the executor.
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = 100;
-                QUERY PLAN                 
--------------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Seq Scan on qa (actual rows=1.00 loops=1)
    Filter: (i = 100)
-   Rows Removed In Executor by Filter: 999
+   Rows Removed In Executor by Filter: 1000
 (3 rows)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i < 10;
@@ -23,15 +24,15 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 -------------------------------------------
  Seq Scan on qa (actual rows=9.00 loops=1)
    Filter: (i < 10)
-   Rows Removed In Executor by Filter: 991
+   Rows Removed In Executor by Filter: 992
 (3 rows)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE 100 = i;
-                QUERY PLAN                 
--------------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Seq Scan on qa (actual rows=1.00 loops=1)
    Filter: (100 = i)
-   Rows Removed In Executor by Filter: 999
+   Rows Removed In Executor by Filter: 1000
 (3 rows)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE 10 > i;
@@ -39,23 +40,23 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 -------------------------------------------
  Seq Scan on qa (actual rows=9.00 loops=1)
    Filter: (10 > i)
-   Rows Removed In Executor by Filter: 991
+   Rows Removed In Executor by Filter: 992
 (3 rows)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = SQRT(25)::INT;
-                QUERY PLAN                 
--------------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Seq Scan on qa (actual rows=1.00 loops=1)
    Filter: (i = 5)
-   Rows Removed In Executor by Filter: 999
+   Rows Removed In Executor by Filter: 1000
 (3 rows)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = (SELECT 100);
-                QUERY PLAN                 
--------------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Seq Scan on qa (actual rows=1.00 loops=1)
    Filter: (i = (InitPlan expr_1).col1)
-   Rows Removed In Executor by Filter: 999
+   Rows Removed In Executor by Filter: 1000
    InitPlan expr_1
      ->  Result (actual rows=1.00 loops=1)
 (5 rows)
@@ -65,7 +66,7 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 ---------------------------------------------------
  Seq Scan on qa (actual rows=1.00 loops=1)
    Filter: (i = (InitPlan expr_1).col1)
-   Rows Removed In Executor by Filter: 999
+   Rows Removed In Executor by Filter: 1000
    InitPlan expr_1
      ->  Seq Scan on qb (actual rows=1.00 loops=1)
            Filter: (j = 100)
@@ -73,23 +74,23 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 (7 rows)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa JOIN qb ON (qa.i = qb.j) WHERE j = 100;
-                   QUERY PLAN                    
--------------------------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop (actual rows=1.00 loops=1)
    ->  Seq Scan on qa (actual rows=1.00 loops=1)
          Filter: (i = 100)
-         Rows Removed In Executor by Filter: 999
+         Rows Removed In Executor by Filter: 1000
    ->  Seq Scan on qb (actual rows=1.00 loops=1)
          Filter: (j = 100)
          Rows Removed In Executor by Filter: 999
 (7 rows)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = ii AND ii < 10;
-                QUERY PLAN                 
--------------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Seq Scan on qa (actual rows=1.00 loops=1)
    Filter: ((ii < 10) AND (i = ii))
-   Rows Removed In Executor by Filter: 999
+   Rows Removed In Executor by Filter: 1000
 (3 rows)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = ANY('{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}'::INT[]);
@@ -97,7 +98,7 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 -----------------------------------------------------------
  Seq Scan on qa (actual rows=10.00 loops=1)
    Filter: (i = ANY ('{1,2,3,4,5,6,7,8,9,10}'::integer[]))
-   Rows Removed In Executor by Filter: 990
+   Rows Removed In Executor by Filter: 991
 (3 rows)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = ANY('{1, 2}'::INT[]);
@@ -105,7 +106,7 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 -------------------------------------------
  Seq Scan on qa (actual rows=2.00 loops=1)
    Filter: (i = ANY ('{1,2}'::integer[]))
-   Rows Removed In Executor by Filter: 998
+   Rows Removed In Executor by Filter: 999
 (3 rows)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE NOT (i <> ALL('{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}'::INT[]));
@@ -113,7 +114,7 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 -----------------------------------------------------------
  Seq Scan on qa (actual rows=10.00 loops=1)
    Filter: (i = ANY ('{1,2,3,4,5,6,7,8,9,10}'::integer[]))
-   Rows Removed In Executor by Filter: 990
+   Rows Removed In Executor by Filter: 991
 (3 rows)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = ANY((SELECT array_agg(j) FROM qb WHERE j > 50 AND j <= 60)::int[]);
@@ -121,7 +122,7 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 ----------------------------------------------------------
  Seq Scan on qa (actual rows=10.00 loops=1)
    Filter: (i = ANY ((InitPlan expr_1).col1))
-   Rows Removed In Executor by Filter: 990
+   Rows Removed In Executor by Filter: 991
    InitPlan expr_1
      ->  Aggregate (actual rows=1.00 loops=1)
            ->  Seq Scan on qb (actual rows=10.00 loops=1)
@@ -129,16 +130,32 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
                  Rows Removed In Executor by Filter: 990
 (8 rows)
 
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT i, ii FROM qa WHERE ii IS NULL;
+                 QUERY PLAN                 
+--------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: (ii IS NULL)
+   Rows Removed In Executor by Filter: 1000
+(3 rows)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT i, ii FROM qa WHERE ii IS NOT NULL AND i >= 1000;;
+                  QUERY PLAN                  
+----------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: ((ii IS NOT NULL) AND (i >= 1000))
+   Rows Removed In Executor by Filter: 1000
+(3 rows)
+
 -- Enable quals push down
 ALTER TABLE qa SET (quals_push_down=on);
 ALTER TABLE qb SET (quals_push_down=on);
 -- Now, we expect to see the tuples being filtered out by the table AM
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = 100;
-                QUERY PLAN                 
--------------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Seq Scan on qa (actual rows=1.00 loops=1)
    Filter: (i = 100)
-   Rows Removed In Table AM by Filter: 999
+   Rows Removed In Table AM by Filter: 1000
 (3 rows)
 
 SELECT ii FROM qa WHERE i = 100;
@@ -152,7 +169,7 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 -------------------------------------------
  Seq Scan on qa (actual rows=9.00 loops=1)
    Filter: (i < 10)
-   Rows Removed In Table AM by Filter: 991
+   Rows Removed In Table AM by Filter: 992
 (3 rows)
 
 SELECT ii FROM qa WHERE i < 10;
@@ -170,11 +187,11 @@ SELECT ii FROM qa WHERE i < 10;
 (9 rows)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE 100 = i;
-                QUERY PLAN                 
--------------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Seq Scan on qa (actual rows=1.00 loops=1)
    Filter: (100 = i)
-   Rows Removed In Table AM by Filter: 999
+   Rows Removed In Table AM by Filter: 1000
 (3 rows)
 
 SELECT ii FROM qa WHERE 100 = i;
@@ -188,7 +205,7 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 -------------------------------------------
  Seq Scan on qa (actual rows=9.00 loops=1)
    Filter: (10 > i)
-   Rows Removed In Table AM by Filter: 991
+   Rows Removed In Table AM by Filter: 992
 (3 rows)
 
 SELECT ii FROM qa WHERE 10 > i;
@@ -206,11 +223,11 @@ SELECT ii FROM qa WHERE 10 > i;
 (9 rows)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = SQRT(25)::INT;
-                QUERY PLAN                 
--------------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Seq Scan on qa (actual rows=1.00 loops=1)
    Filter: (i = 5)
-   Rows Removed In Table AM by Filter: 999
+   Rows Removed In Table AM by Filter: 1000
 (3 rows)
 
 SELECT ii FROM qa WHERE i = SQRT(25)::INT;
@@ -220,11 +237,11 @@ SELECT ii FROM qa WHERE i = SQRT(25)::INT;
 (1 row)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = (SELECT 100);
-                QUERY PLAN                 
--------------------------------------------
+                 QUERY PLAN                 
+--------------------------------------------
  Seq Scan on qa (actual rows=1.00 loops=1)
    Filter: (i = (InitPlan expr_1).col1)
-   Rows Removed In Table AM by Filter: 999
+   Rows Removed In Table AM by Filter: 1000
    InitPlan expr_1
      ->  Result (actual rows=1.00 loops=1)
 (5 rows)
@@ -240,7 +257,7 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 ---------------------------------------------------
  Seq Scan on qa (actual rows=1.00 loops=1)
    Filter: (i = (InitPlan expr_1).col1)
-   Rows Removed In Table AM by Filter: 999
+   Rows Removed In Table AM by Filter: 1000
    InitPlan expr_1
      ->  Seq Scan on qb (actual rows=1.00 loops=1)
            Filter: (j = 100)
@@ -254,12 +271,12 @@ SELECT ii FROM qa WHERE i = (SELECT SQRT(j)::INT FROM qb WHERE j = 100);
 (1 row)
 
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa JOIN qb ON (qa.i = qb.j) WHERE j = 100;
-                   QUERY PLAN                    
--------------------------------------------------
+                    QUERY PLAN                    
+--------------------------------------------------
  Nested Loop (actual rows=1.00 loops=1)
    ->  Seq Scan on qa (actual rows=1.00 loops=1)
          Filter: (i = 100)
-         Rows Removed In Table AM by Filter: 999
+         Rows Removed In Table AM by Filter: 1000
    ->  Seq Scan on qb (actual rows=1.00 loops=1)
          Filter: (j = 100)
          Rows Removed In Table AM by Filter: 999
@@ -276,7 +293,7 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 -------------------------------------------
  Seq Scan on qa (actual rows=1.00 loops=1)
    Filter: ((ii < 10) AND (i = ii))
-   Rows Removed In Table AM by Filter: 997
+   Rows Removed In Table AM by Filter: 998
    Rows Removed In Executor by Filter: 2
 (4 rows)
 
@@ -291,7 +308,7 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 -----------------------------------------------------------
  Seq Scan on qa (actual rows=10.00 loops=1)
    Filter: (i = ANY ('{1,2,3,4,5,6,7,8,9,10}'::integer[]))
-   Rows Removed In Table AM by Filter: 990
+   Rows Removed In Table AM by Filter: 991
 (3 rows)
 
 SELECT ii FROM qa WHERE i = ANY('{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}'::INT[]);
@@ -314,7 +331,7 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 -------------------------------------------
  Seq Scan on qa (actual rows=2.00 loops=1)
    Filter: (i = ANY ('{1,2}'::integer[]))
-   Rows Removed In Table AM by Filter: 998
+   Rows Removed In Table AM by Filter: 999
 (3 rows)
 
 SELECT ii FROM qa WHERE i = ANY('{1, 2}'::INT[]);
@@ -329,7 +346,7 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 -----------------------------------------------------------
  Seq Scan on qa (actual rows=10.00 loops=1)
    Filter: (i = ANY ('{1,2,3,4,5,6,7,8,9,10}'::integer[]))
-   Rows Removed In Table AM by Filter: 990
+   Rows Removed In Table AM by Filter: 991
 (3 rows)
 
 SELECT ii FROM qa WHERE NOT (i <> ALL('{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}'::INT[]));
@@ -352,7 +369,7 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 ----------------------------------------------------------
  Seq Scan on qa (actual rows=10.00 loops=1)
    Filter: (i = ANY ((InitPlan expr_1).col1))
-   Rows Removed In Table AM by Filter: 990
+   Rows Removed In Table AM by Filter: 991
    InitPlan expr_1
      ->  Aggregate (actual rows=1.00 loops=1)
            ->  Seq Scan on qb (actual rows=10.00 loops=1)
@@ -375,5 +392,33 @@ SELECT ii FROM qa WHERE i = ANY((SELECT array_agg(j) FROM qb WHERE j > 50 AND j
  3600
 (10 rows)
 
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT i, ii FROM qa WHERE ii IS NULL;
+                 QUERY PLAN                 
+--------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: (ii IS NULL)
+   Rows Removed In Table AM by Filter: 1000
+(3 rows)
+
+SELECT i, ii FROM qa WHERE ii IS NULL;
+  i   | ii 
+------+----
+ 1001 |   
+(1 row)
+
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT i, ii FROM qa WHERE ii IS NOT NULL AND i >= 1000;;
+                  QUERY PLAN                  
+----------------------------------------------
+ Seq Scan on qa (actual rows=1.00 loops=1)
+   Filter: ((ii IS NOT NULL) AND (i >= 1000))
+   Rows Removed In Table AM by Filter: 1000
+(3 rows)
+
+SELECT i, ii FROM qa WHERE ii IS NOT NULL AND i >= 1000;;
+  i   |   ii    
+------+---------
+ 1000 | 1000000
+(1 row)
+
 DROP TABLE IF EXISTS qa;
 DROP TABLE IF EXISTS qb;
diff --git a/src/test/regress/sql/qual_pushdown.sql b/src/test/regress/sql/qual_pushdown.sql
index 38e88a50c33..50d6f9b316a 100644
--- a/src/test/regress/sql/qual_pushdown.sql
+++ b/src/test/regress/sql/qual_pushdown.sql
@@ -5,6 +5,7 @@ CREATE TABLE qa (i INTEGER, ii INTEGER);
 CREATE TABLE qb (j INTEGER);
 INSERT INTO qa SELECT n, n * n  FROM generate_series(1, 1000) as n;
 INSERT INTO qb SELECT n FROM generate_series(1, 1000) as n;
+INSERT INTO qa VALUES (1001, NULL);
 ANALYZE qa;
 ANALYZE qb;
 
@@ -23,6 +24,8 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = ANY('{1, 2}'::INT[]);
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE NOT (i <> ALL('{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}'::INT[]));
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = ANY((SELECT array_agg(j) FROM qb WHERE j > 50 AND j <= 60)::int[]);
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT i, ii FROM qa WHERE ii IS NULL;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT i, ii FROM qa WHERE ii IS NOT NULL AND i >= 1000;;
 
 -- Enable quals push down
 ALTER TABLE qa SET (quals_push_down=on);
@@ -55,6 +58,10 @@ EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FRO
 SELECT ii FROM qa WHERE NOT (i <> ALL('{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}'::INT[]));
 EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT ii FROM qa WHERE i = ANY((SELECT array_agg(j) FROM qb WHERE j > 50 AND j <= 60)::INT[]);
 SELECT ii FROM qa WHERE i = ANY((SELECT array_agg(j) FROM qb WHERE j > 50 AND j <= 60)::INT[]);
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT i, ii FROM qa WHERE ii IS NULL;
+SELECT i, ii FROM qa WHERE ii IS NULL;
+EXPLAIN (ANALYZE, COSTS off, TIMING off, SUMMARY off, BUFFERS off) SELECT i, ii FROM qa WHERE ii IS NOT NULL AND i >= 1000;;
+SELECT i, ii FROM qa WHERE ii IS NOT NULL AND i >= 1000;;
 
 DROP TABLE IF EXISTS qa;
 DROP TABLE IF EXISTS qb;
-- 
2.39.5

