diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index b31d892121..36bf47dc8e 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -2618,6 +2618,225 @@ check_redundant_nullability_qual(PlannerInfo *root, Node *clause)
 	return false;
 }
 
+/*
+ * add_baserestrictinfo_to_rel
+ *	  Add 'restrictinfo' as a baserestrictinfo to the base relation denoted by
+ *	  'relid'.  While here, attempt to determine if the given qual is always
+ *	  true or always false when evaluated as a base qual.  If we can determine
+ *	  that the qual needn't be evaluated by the executor, we replace the qual
+ *	  with a constant "true" or "false".
+ */
+static void
+add_baserestrictinfo_to_rel(PlannerInfo *root, Index relid,
+							RestrictInfo *restrictinfo)
+{
+	RelOptInfo *rel = find_base_rel(root, relid);
+	Expr	   *newclause;
+
+	Assert(bms_membership(restrictinfo->required_relids) == BMS_SINGLETON);
+
+	newclause = transform_clause(root, restrictinfo->clause);
+
+	if (newclause != restrictinfo->clause)
+	{
+		restrictinfo = make_restrictinfo(root,
+										 newclause,
+										 restrictinfo->is_pushed_down,
+										 restrictinfo->has_clone,
+										 restrictinfo->is_clone,
+										 restrictinfo->pseudoconstant,
+										 restrictinfo->security_level,
+										 restrictinfo->required_relids,
+										 restrictinfo->incompatible_relids,
+										 restrictinfo->outer_relids);
+	}
+
+	/* Add clause to rel's restriction list */
+	rel->baserestrictinfo = lappend(rel->baserestrictinfo, restrictinfo);
+
+	/* Update security level info */
+	rel->baserestrict_min_security = Min(rel->baserestrict_min_security,
+										 restrictinfo->security_level);
+}
+
+/*
+ * transform_nulltest
+ *	  Attempt to transform the given NullTest and into a constant "true" or
+ *	  "false".  This maybe possible when the column has a NOT NULL constraint
+ *	  which means an IS NOT NULL clause is always true or a IS NULL clause is
+ *	  always false.  If there is no NOT NULL constraint or if the column is
+ *	  NULLable by an outer join, or the NullTest's arg isn't a Var, then we
+ *	  return the input NullTest.
+ */
+static Expr *
+transform_nulltest(PlannerInfo *root, NullTest *nulltest)
+{
+	Var		   *var;
+	bool		notnull = false;
+
+	Assert(IsA(nulltest, NullTest));
+
+	/*
+	 * We only support Vars as there's no ability to have a NOT NULL constraint
+	 * on anything else.
+	 */
+	if (!IsA(nulltest->arg, Var))
+		return (Expr *) nulltest;
+
+	var = (Var *) nulltest->arg;
+
+	/* could the Var be nulled by any outer joins? */
+	if (var->varnullingrels != NULL)
+		return (Expr *) nulltest;
+
+	/* system columns cannot be NULL */
+	if (var->varattno < 0)
+		notnull = true;
+	else if (var->varattno > 0)
+	{
+		/* is the column defined NOT NULL? */
+		RelOptInfo *rel = find_base_rel(root, var->varno);
+		
+		if (bms_is_member(var->varattno, rel->notnullattnums))
+			notnull = true;
+	}
+	else
+	{
+		RelOptInfo	   *rel = find_base_rel(root, var->varno);
+		Bitmapset	   *allattnums = bms_add_range(NULL, 1, rel->max_attr);
+
+		/*
+		 * Handle whole-row Vars.  The SQL standard defines IS [NOT] NULL for a
+		 * non-null rowtype argument as:
+		 *
+		 * "R IS NULL" is true if every field is the null value.
+		 *
+		 * "R IS NOT NULL" is true if no field is the null value.
+		 */
+
+		/*
+		 * XXX the IS NOT NULL won't work correctly if there are dropped columns.
+		 * Is it worth having another set in RelOptInfo to allow us to remove
+		 * dropped columns from allattnums before the bms_nonempty_difference?
+		 * XXX should we even bother handling whole-row Vars??
+		 */
+		if (nulltest->nulltesttype == IS_NOT_NULL)
+			notnull = !bms_nonempty_difference(allattnums,
+											   rel->notnullattnums);
+		else
+			notnull = bms_overlap(allattnums, rel->notnullattnums);
+		bms_free(allattnums);
+	}
+
+	/*
+	 * When the column cannot be NULL, we can transform IS NOT NULL quals to
+	 * "true" and IS NULL quals to "false".
+	 */
+	if (notnull)
+	{
+		bool	constexpr = (nulltest->nulltesttype == IS_NOT_NULL);
+
+		return (Expr *) makeBoolConst(constexpr, false);
+	}
+
+	return (Expr *) nulltest;
+}
+
+/*
+ * transform_clause
+ *	  Check and attempt to transform 'clause' into either a constant true or
+ *	  constant false if we're able to determine that the qual needn't be
+ *	  evaluated by the executor.
+ *
+ * Returns a newly allocated Expr if any tranformation was done, else returns
+ * the input Expr unmodified.
+ */
+Expr *
+transform_clause(PlannerInfo *root, Expr *clause)
+{
+	/*
+	 * Currently we only check for NullTest quals and OR clauses that include
+	 * NullTest quals.
+	 */
+
+	/* Check for NullTest qual */
+	if (IsA(clause, NullTest))
+		return transform_nulltest(root, (NullTest *) clause);
+
+	if (is_orclause(clause))
+	{
+		ListCell   *lc;
+		bool reeval_const = false;
+
+		/* If it's an OR, check its sub-clauses */
+		foreach (lc, ((BoolExpr *) clause)->args)
+		{
+			Expr   *orarg = (Expr *) lfirst(lc);
+
+			if (IsA(orarg, Const))
+				reeval_const = true;
+			else
+			{
+				Expr   *newexpr = transform_clause(root, orarg);
+
+				if (newexpr != orarg)
+				{
+					reeval_const = true;
+					lfirst(lc) = newexpr;
+				}
+			}
+		}
+
+		/*
+		 * If we managed to transform any clauses or found a Const in the OR clause
+		 * then let's try constant folding again as it may allow us to simplify (or
+		 * delete) the OR clause.
+		 */
+		if (reeval_const)
+			clause = (Expr *) eval_const_expressions(root, (Node *) clause);
+	}
+
+	return clause;
+}
+
+void
+transform_join_clauses(PlannerInfo *root)
+{
+	if (root->simple_rel_array_size == 1)
+		return;
+
+	for (int i = 1; i < root->simple_rel_array_size; i++)
+	{
+		RelOptInfo *rel = root->simple_rel_array[i];
+		ListCell *lc;
+
+		if (rel == NULL || rel->reloptkind != RELOPT_BASEREL)
+			continue;
+
+		foreach(lc, rel->joininfo)
+		{
+			RestrictInfo *restrictinfo = lfirst(lc);
+			Expr *newclause = transform_clause(root, restrictinfo->clause);
+
+			if (newclause != restrictinfo->clause)
+			{
+				restrictinfo =
+						make_restrictinfo(root,
+										  newclause,
+										  restrictinfo->is_pushed_down,
+										  restrictinfo->has_clone,
+										  restrictinfo->is_clone,
+										  restrictinfo->pseudoconstant,
+										  restrictinfo->security_level,
+										  restrictinfo->required_relids,
+										  restrictinfo->incompatible_relids,
+										  restrictinfo->outer_relids);
+				lfirst(lc) = restrictinfo;
+			}
+		}
+	}
+}
+
 /*
  * distribute_restrictinfo_to_rels
  *	  Push a completed RestrictInfo into the proper restriction or join
@@ -2632,58 +2851,39 @@ distribute_restrictinfo_to_rels(PlannerInfo *root,
 								RestrictInfo *restrictinfo)
 {
 	Relids		relids = restrictinfo->required_relids;
-	RelOptInfo *rel;
+	int			relid;
 
-	switch (bms_membership(relids))
+	if (relids == NULL)
 	{
-		case BMS_SINGLETON:
-
-			/*
-			 * There is only one relation participating in the clause, so it
-			 * is a restriction clause for that relation.
-			 */
-			rel = find_base_rel(root, bms_singleton_member(relids));
-
-			/* Add clause to rel's restriction list */
-			rel->baserestrictinfo = lappend(rel->baserestrictinfo,
-											restrictinfo);
-			/* Update security level info */
-			rel->baserestrict_min_security = Min(rel->baserestrict_min_security,
-												 restrictinfo->security_level);
-			break;
-		case BMS_MULTIPLE:
-
-			/*
-			 * The clause is a join clause, since there is more than one rel
-			 * in its relid set.
-			 */
-
-			/*
-			 * Check for hashjoinable operators.  (We don't bother setting the
-			 * hashjoin info except in true join clauses.)
-			 */
-			check_hashjoinable(restrictinfo);
+		/*
+		 * clause references no rels, and therefore we have no place to
+		 * attach it.  Shouldn't get here if callers are working properly.
+		 */
+		elog(ERROR, "cannot cope with variable-free clause");
+	}
+	else if (bms_get_singleton_member(relids, &relid))
+		add_baserestrictinfo_to_rel(root, relid, restrictinfo);
+	else
+	{
+		/*
+		 * The clause is a join clause, since there is more than one rel in
+		 * its relid set.
+		 */
 
-			/*
-			 * Likewise, check if the clause is suitable to be used with a
-			 * Memoize node to cache inner tuples during a parameterized
-			 * nested loop.
-			 */
-			check_memoizable(restrictinfo);
+		/*
+		 * Check for hashjoinable operators.  (We don't bother setting the
+		 * hashjoin info except in true join clauses.)
+		 */
+		check_hashjoinable(restrictinfo);
 
-			/*
-			 * Add clause to the join lists of all the relevant relations.
-			 */
-			add_join_clause_to_rels(root, restrictinfo, relids);
-			break;
-		default:
+		/*
+		 * Likewise, check if the clause is suitable to be used with a Memoize
+		 * node to cache inner tuples during a parameterized nested loop.
+		 */
+		check_memoizable(restrictinfo);
 
-			/*
-			 * clause references no rels, and therefore we have no place to
-			 * attach it.  Shouldn't get here if callers are working properly.
-			 */
-			elog(ERROR, "cannot cope with variable-free clause");
-			break;
+		/* Add clause to the join lists of all the relevant relations. */
+		add_join_clause_to_rels(root, restrictinfo, relids);
 	}
 }
 
diff --git a/src/backend/optimizer/plan/planmain.c b/src/backend/optimizer/plan/planmain.c
index fcc0eacd25..e24887f505 100644
--- a/src/backend/optimizer/plan/planmain.c
+++ b/src/backend/optimizer/plan/planmain.c
@@ -195,6 +195,8 @@ query_planner(PlannerInfo *root,
 	 */
 	reconsider_outer_join_clauses(root);
 
+	transform_join_clauses(root);
+
 	/*
 	 * If we formed any equivalence classes, generate additional restriction
 	 * clauses as appropriate.  (Implied join clauses are formed on-the-fly
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 243c8fb1e4..4677f479c5 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -163,6 +163,23 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 	rel->attr_widths = (int32 *)
 		palloc0((rel->max_attr - rel->min_attr + 1) * sizeof(int32));
 
+	/* record which columns are defined as NOT NULL */
+	for (int i = 0; i < relation->rd_att->natts; i++)
+	{
+		FormData_pg_attribute *attr = &relation->rd_att->attrs[i];
+
+		if (attr->attnotnull)
+		{
+			rel->notnullattnums = bms_add_member(rel->notnullattnums,
+												 attr->attnum);
+			/*
+			 * Per ATExecDropNotNull(), dropped columns will have their attnotnull
+			 * unset, so we needn't check for dropped columns in the above condition.
+			 */
+			Assert(!attr->attisdropped);
+		}
+	}
+
 	/*
 	 * Estimate relation size --- unless it's an inheritance parent, in which
 	 * case the size we want is not the rel's own size but the size of its
@@ -1549,16 +1566,20 @@ relation_excluded_by_constraints(PlannerInfo *root,
 
 	/*
 	 * Regardless of the setting of constraint_exclusion, detect
-	 * constant-FALSE-or-NULL restriction clauses.  Because const-folding will
-	 * reduce "anything AND FALSE" to just "FALSE", any such case should
-	 * result in exactly one baserestrictinfo entry.  This doesn't fire very
-	 * often, but it seems cheap enough to be worth doing anyway.  (Without
-	 * this, we'd miss some optimizations that 9.5 and earlier found via much
-	 * more roundabout methods.)
+	 * constant-FALSE-or-NULL restriction clauses.  Const-folding will reduce
+	 * "anything AND FALSE" to just "FALSE", any such case should result in
+	 * exactly one baserestrictinfo entry.  The transform_clause()
+	 * infrastructure isn't quite as careful and may leave a "FALSE" within a
+	 * List of RestrictInfos.  Here we loop over each restrictinfo to check
+	 * and return true if we find any impossible RestrictInfos.
+	 *
+	 * This doesn't fire very often, but it seems cheap enough to be worth
+	 * doing anyway.  (Without this, we'd miss some optimizations that 9.5 and
+	 * earlier found via much more roundabout methods.)
 	 */
-	if (list_length(rel->baserestrictinfo) == 1)
+	foreach(lc, rel->baserestrictinfo)
 	{
-		RestrictInfo *rinfo = (RestrictInfo *) linitial(rel->baserestrictinfo);
+		RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc);
 		Expr	   *clause = rinfo->clause;
 
 		if (clause && IsA(clause, Const) &&
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 76dad17e33..5d6d97c111 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -221,6 +221,7 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent)
 	rel->relid = relid;
 	rel->rtekind = rte->rtekind;
 	/* min_attr, max_attr, attr_needed, attr_widths are set below */
+	rel->notnullattnums = NULL;
 	rel->lateral_vars = NIL;
 	rel->indexlist = NIL;
 	rel->statlist = NIL;
@@ -707,6 +708,7 @@ build_join_rel(PlannerInfo *root,
 	joinrel->max_attr = 0;
 	joinrel->attr_needed = NULL;
 	joinrel->attr_widths = NULL;
+	joinrel->notnullattnums = NULL;
 	joinrel->nulling_relids = NULL;
 	joinrel->lateral_vars = NIL;
 	joinrel->lateral_referencers = NULL;
@@ -905,6 +907,7 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
 	joinrel->max_attr = 0;
 	joinrel->attr_needed = NULL;
 	joinrel->attr_widths = NULL;
+	joinrel->notnullattnums = NULL;
 	joinrel->nulling_relids = NULL;
 	joinrel->lateral_vars = NIL;
 	joinrel->lateral_referencers = NULL;
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 5702fbba60..d4f6bbcd2c 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -913,6 +913,8 @@ typedef struct RelOptInfo
 	Relids	   *attr_needed pg_node_attr(read_write_ignore);
 	/* array indexed [min_attr .. max_attr] */
 	int32	   *attr_widths pg_node_attr(read_write_ignore);
+	/* zero-based set containing attnums of NOT NULL columns */
+	Bitmapset  *notnullattnums;
 	/* relids of outer joins that can null this baserel */
 	Relids		nulling_relids;
 	/* LATERAL Vars and PHVs referenced by rel */
diff --git a/src/include/optimizer/planmain.h b/src/include/optimizer/planmain.h
index 31c188176b..f8b794b0e3 100644
--- a/src/include/optimizer/planmain.h
+++ b/src/include/optimizer/planmain.h
@@ -75,6 +75,8 @@ extern void add_vars_to_targetlist(PlannerInfo *root, List *vars,
 extern void find_lateral_references(PlannerInfo *root);
 extern void create_lateral_join_info(PlannerInfo *root);
 extern List *deconstruct_jointree(PlannerInfo *root);
+extern Expr *transform_clause(PlannerInfo *root, Expr *clause);
+extern void transform_join_clauses(PlannerInfo *root);
 extern void distribute_restrictinfo_to_rels(PlannerInfo *root,
 											RestrictInfo *restrictinfo);
 extern RestrictInfo *process_implied_equality(PlannerInfo *root,
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 9b8638f286..b95d30f658 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -5788,13 +5788,11 @@ explain (costs off)
 select p.* from
   parent p left join child c on (p.k = c.k)
   where p.k = 1 and p.k = 2;
-                   QUERY PLAN                   
-------------------------------------------------
+        QUERY PLAN        
+--------------------------
  Result
    One-Time Filter: false
-   ->  Index Scan using parent_pkey on parent p
-         Index Cond: (k = 1)
-(4 rows)
+(2 rows)
 
 select p.* from
   (parent p left join child c on (p.k = c.k)) join parent x on p.k = x.k
diff --git a/src/test/regress/expected/predicate.out b/src/test/regress/expected/predicate.out
new file mode 100644
index 0000000000..5c4914dc80
--- /dev/null
+++ b/src/test/regress/expected/predicate.out
@@ -0,0 +1,146 @@
+--
+-- Tests for predicate transformation
+--
+CREATE TABLE pred_tab (a INT NOT NULL, b INT);
+--
+-- test that restrictions that we detect as always true are ignored
+--
+-- An IS NOT NULL qual in restriction clauses can be ignored if it's on a NOT
+-- NULL column
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.a IS NOT NULL;
+       QUERY PLAN       
+------------------------
+ Seq Scan on pred_tab t
+(1 row)
+
+-- A more complex variant of the above.  Ensure we're left only with the
+-- t.b = 1 qual since t.a IS NOT NULL is always true
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.b = 1 AND t.a IS NOT NULL;
+       QUERY PLAN       
+------------------------
+ Seq Scan on pred_tab t
+   Filter: (b = 1)
+(2 rows)
+
+-- Ensure t.b IS NOT NULL is not removed as t.b allows NULL values.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.b IS NOT NULL;
+        QUERY PLAN         
+---------------------------
+ Seq Scan on pred_tab t
+   Filter: (b IS NOT NULL)
+(2 rows)
+
+-- Ensure the t.a IS NOT NULL is detected as always true.  We shouldn't see
+-- any quals in the scan.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.a IS NOT NULL OR t.b = 1;
+       QUERY PLAN       
+------------------------
+ Seq Scan on pred_tab t
+(1 row)
+
+-- Ensure the quals remain as t.b allows NULL values.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.b IS NOT NULL OR t.a = 1;
+               QUERY PLAN               
+----------------------------------------
+ Seq Scan on pred_tab t
+   Filter: ((b IS NOT NULL) OR (a = 1))
+(2 rows)
+
+-- An IS NOT NULL NullTest in join clauses can be ignored if
+-- a) it's on a NOT NULL column, and;
+-- b) its Var is not nulled by any outer joins
+-- Ensure t2.a IS NOT NULL is not seen in the plan
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON true
+LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Nested Loop Left Join
+   ->  Seq Scan on pred_tab t1
+   ->  Materialize
+         ->  Nested Loop Left Join
+               ->  Seq Scan on pred_tab t2
+               ->  Materialize
+                     ->  Seq Scan on pred_tab t3
+(7 rows)
+
+-- When some t2 rows are missing due to the left join we cannot forego
+-- including the t2.a is not null in the plan.  Ensure it remains.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON t1.a = 1
+LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL;
+                QUERY PLAN                 
+-------------------------------------------
+ Nested Loop Left Join
+   Join Filter: (t2.a IS NOT NULL)
+   ->  Nested Loop Left Join
+         Join Filter: (t1.a = 1)
+         ->  Seq Scan on pred_tab t1
+         ->  Materialize
+               ->  Seq Scan on pred_tab t2
+   ->  Materialize
+         ->  Seq Scan on pred_tab t3
+(9 rows)
+
+-- Ensure t2.a IS NULL is detected as constantly TRUE and results in none of
+-- the quals appearing in the plan.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON true
+LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b = 1;
+                   QUERY PLAN                    
+-------------------------------------------------
+ Nested Loop Left Join
+   ->  Seq Scan on pred_tab t1
+   ->  Materialize
+         ->  Nested Loop Left Join
+               ->  Seq Scan on pred_tab t2
+               ->  Materialize
+                     ->  Seq Scan on pred_tab t3
+(7 rows)
+
+-- Ensure that the IS NOT NULL qual isn't removed as t2.a is nullable from
+-- t2's left join to t1.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON t1.a = 1
+LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b = 1;
+                    QUERY PLAN
+---------------------------------------------------
+ Nested Loop Left Join
+   Join Filter: ((t2.a IS NOT NULL) OR (t2.b = 1))
+   ->  Nested Loop Left Join
+         Join Filter: (t1.a = 1)
+         ->  Seq Scan on pred_tab t1
+         ->  Materialize
+               ->  Seq Scan on pred_tab t2
+   ->  Materialize
+         ->  Seq Scan on pred_tab t3
+(9 rows)
+
+--
+-- Ensure that impossible IS NULL NullTests are detected when the Var cannot
+-- be NULL
+--
+-- Ensure we detect t.a IS NULL is impossible AND forego the scan
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.a IS NULL;
+        QUERY PLAN        
+--------------------------
+ Result
+   One-Time Filter: false
+(2 rows)
+
+-- As above, but add an additional qual
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.a = 1234 AND t.a IS NULL;
+        QUERY PLAN        
+--------------------------
+ Result
+   One-Time Filter: false
+(2 rows)
+
+DROP TABLE pred_tab;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 4df9d8503b..3816efc7b3 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -119,7 +119,7 @@ test: plancache limit plpgsql copy2 temp domain rangefuncs prepare conversion tr
 # The stats test resets stats, so nothing else needing stats access can be in
 # this group.
 # ----------
-test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats
+test: partition_join partition_prune reloptions hash_part indexing partition_aggregate partition_info tuplesort explain compression memoize stats predicate
 
 # event_trigger depends on create_am and cannot run concurrently with
 # any test that runs DDL
diff --git a/src/test/regress/sql/predicate.sql b/src/test/regress/sql/predicate.sql
new file mode 100644
index 0000000000..faeb54aee7
--- /dev/null
+++ b/src/test/regress/sql/predicate.sql
@@ -0,0 +1,75 @@
+--
+-- Tests for predicate transformation
+--
+
+CREATE TABLE pred_tab (a INT NOT NULL, b INT);
+
+--
+-- test that restrictions that we detect as always true are ignored
+--
+
+-- An IS NOT NULL qual in restriction clauses can be ignored if it's on a NOT
+-- NULL column
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.a IS NOT NULL;
+
+-- A more complex variant of the above.  Ensure we're left only with the
+-- t.b = 1 qual since t.a IS NOT NULL is always true
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.b = 1 AND t.a IS NOT NULL;
+
+-- Ensure t.b IS NOT NULL is not removed as t.b allows NULL values.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.b IS NOT NULL;
+
+-- Ensure the t.a IS NOT NULL is detected as always true.  We shouldn't see
+-- any quals in the scan.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.a IS NOT NULL OR t.b = 1;
+
+-- Ensure the quals remain as t.b allows NULL values.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.b IS NOT NULL OR t.a = 1;
+
+-- An IS NOT NULL NullTest in join clauses can be ignored if
+-- a) it's on a NOT NULL column, and;
+-- b) its Var is not nulled by any outer joins
+
+-- Ensure t2.a IS NOT NULL is not seen in the plan
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON true
+LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL;
+
+-- When some t2 rows are missing due to the left join we cannot forego
+-- including the t2.a is not null in the plan.  Ensure it remains.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON t1.a = 1
+LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL;
+
+-- Ensure t2.a IS NULL is detected as constantly TRUE and results in none of
+-- the quals appearing in the plan.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON true
+LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b = 1;
+
+-- Ensure that the IS NOT NULL qual isn't removed as t2.a is nullable from
+-- t2's left join to t1.
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t1 LEFT JOIN pred_tab t2 ON t1.a = 1
+LEFT JOIN pred_tab t3 ON t2.a IS NOT NULL OR t2.b = 1;
+
+--
+-- Ensure that impossible IS NULL NullTests are detected when the Var cannot
+-- be NULL
+--
+
+-- Ensure we detect t.a IS NULL is impossible AND forego the scan
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.a IS NULL;
+
+-- As above, but add an additional qual
+EXPLAIN (COSTS OFF)
+SELECT * FROM pred_tab t WHERE t.a = 1234 AND t.a IS NULL;
+
+DROP TABLE pred_tab;
+
