From fbce0efb501feb2b85a68e6da789ed619641a12b Mon Sep 17 00:00:00 2001
From: Julien Tachoires <julien@tachoires.me>
Date: Tue, 26 Aug 2025 08:49:59 +0200
Subject: [PATCH 6/6] 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 1b181c8f254..210d4cb84e0 100644
--- a/src/backend/executor/nodeSeqscan.c
+++ b/src/backend/executor/nodeSeqscan.c
@@ -156,6 +156,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))
@@ -299,6 +311,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
 		{
@@ -307,19 +371,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 d301dc22661..e868ac2e7b1 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -5429,6 +5429,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 965102b146c..bc7050b5708 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 1).col1)
-   Rows Removed In Executor by Filter: 999
+   Rows Removed In Executor by Filter: 1000
    InitPlan 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 1).col1)
-   Rows Removed In Executor by Filter: 999
+   Rows Removed In Executor by Filter: 1000
    InitPlan 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 1).col1))
-   Rows Removed In Executor by Filter: 990
+   Rows Removed In Executor by Filter: 991
    InitPlan 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 1).col1)
-   Rows Removed In Table AM by Filter: 999
+   Rows Removed In Table AM by Filter: 1000
    InitPlan 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 1).col1)
-   Rows Removed In Table AM by Filter: 999
+   Rows Removed In Table AM by Filter: 1000
    InitPlan 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 1).col1))
-   Rows Removed In Table AM by Filter: 990
+   Rows Removed In Table AM by Filter: 991
    InitPlan 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

