From 2f4706a83869a21ade8c9d818b3c9826a610ea50 Mon Sep 17 00:00:00 2001
From: Andy Fan <zhihui.fan1213@gmail.com>
Date: Fri, 20 May 2022 11:17:52 +0800
Subject: [PATCH v1] Introduce notnull_attrs for RelOptInfo.

RelOptInfo.notnull_attrs records which Vars are not nullable
for the given RelOptInfo after all the quals on this RelOptInfo
are executed.

For a joinrel, the length of notnull_attrs array equals the
(max value + 1) in relids, the array index is varno and the
value is a Bitmapset* which presents which varattnos are
not nullable for the varno.

For baserel, to save space, notnull_attrs[0] is always used.

We calculate the notnull_attrs mainly from 2 places. one is
catalog. which is set during get_relation_info. the other one
is the qual filters, I set them during set_rel_size & set_joinrel_size
to covers as many as relkind as possible. Inherit can be handled
automatically in this way. nulls introduced by outer join
is also handled during build_join_rel.

I assumed upper relation except SETOP would have the same
notnull_attrs as the input relation, like SORT / GROUP
and so on.

SubQuery is handled but SetOp upper relation is not handled so far.

XXX: There are some code like

if (!enable_geqo)
{
elog(INFO, XXXX);
}

They are just there for easily review & testing, will be removed
at last.
---
 src/backend/nodes/bitmapset.c             |  14 ++
 src/backend/optimizer/path/allpaths.c     | 104 +++++++-
 src/backend/optimizer/util/plancat.c      |  12 +
 src/backend/optimizer/util/relnode.c      | 186 +++++++++++++-
 src/include/nodes/bitmapset.h             |   1 +
 src/include/nodes/pathnodes.h             |  14 ++
 src/test/regress/expected/notnulltest.out | 283 ++++++++++++++++++++++
 src/test/regress/parallel_schedule        |   2 +
 src/test/regress/sql/notnulltest.sql      |  87 +++++++
 9 files changed, 697 insertions(+), 6 deletions(-)
 create mode 100644 src/test/regress/expected/notnulltest.out
 create mode 100644 src/test/regress/sql/notnulltest.sql

diff --git a/src/backend/nodes/bitmapset.c b/src/backend/nodes/bitmapset.c
index 0a6c30e4ebc..8bc8034c3fb 100644
--- a/src/backend/nodes/bitmapset.c
+++ b/src/backend/nodes/bitmapset.c
@@ -665,6 +665,20 @@ bms_num_members(const Bitmapset *a)
 	return result;
 }
 
+/*
+ * bms_max_member - the max member in this bitmap.
+ */
+int
+bms_max_member(const Bitmapset *a)
+{
+	int result;
+	if (a == NULL || bms_is_empty(a))
+		elog(ERROR, "Must be an non-empty bitmapset.");
+	result = (a->nwords - 1) * BITS_PER_BITMAPWORD;
+	result += bmw_leftmost_one_pos(a->words[a->nwords - 1]);
+	return result;
+}
+
 /*
  * bms_membership - does a set have zero, one, or multiple members?
  *
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 7ac116a791f..c40b94a725e 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -143,7 +143,8 @@ static void subquery_push_qual(Query *subquery,
 static void recurse_push_qual(Node *setOp, Query *topquery,
 							  RangeTblEntry *rte, Index rti, Node *qual);
 static void remove_unused_subquery_outputs(Query *subquery, RelOptInfo *rel);
-
+static void set_baserel_notnull_attrs_for_quals(RelOptInfo *rel);
+static void set_subquery_rel_notnull_attrs(RelOptInfo *rel);
 
 /*
  * make_one_rel
@@ -422,6 +423,9 @@ set_rel_size(PlannerInfo *root, RelOptInfo *rel,
 				 * and build their paths immediately.
 				 */
 				set_subquery_pathlist(root, rel, rti, rte);
+
+				set_subquery_rel_notnull_attrs(rel);
+
 				break;
 			case RTE_FUNCTION:
 				set_function_size_estimates(root, rel);
@@ -458,6 +462,13 @@ set_rel_size(PlannerInfo *root, RelOptInfo *rel,
 		}
 	}
 
+	/*
+	 * Set the notnull attributes for all the kinds of baserel, including
+	 * RTE_RELATION(foreign table, partitioned table), RTE_SUBQUERY etc
+	 * Here doesn't handle the notnull attributes from catalog.
+	 */
+	set_baserel_notnull_attrs_for_quals(rel);
+
 	/*
 	 * We insist that all non-dummy rels have a nonzero rowcount estimate.
 	 */
@@ -3270,6 +3281,15 @@ make_rel_from_joinlist(PlannerInfo *root, List *joinlist)
 		initial_rels = lappend(initial_rels, thisrel);
 	}
 
+	/*
+	 * We put the initial_rels list into a PlannerInfo field because
+	 * has_legal_joinclause() needs to look at it (ugly :-().
+	 *
+	 * XXX: I kept this since I need to access the last join rel
+	 * during set_uppper_rel_notnull_attrs stage.
+	 */
+	root->initial_rels = initial_rels;
+
 	if (levels_needed == 1)
 	{
 		/*
@@ -3282,11 +3302,7 @@ make_rel_from_joinlist(PlannerInfo *root, List *joinlist)
 		/*
 		 * Consider the different orders in which we could join the rels,
 		 * using a plugin, GEQO, or the regular join search code.
-		 *
-		 * We put the initial_rels list into a PlannerInfo field because
-		 * has_legal_joinclause() needs to look at it (ugly :-().
 		 */
-		root->initial_rels = initial_rels;
 
 		if (join_search_hook)
 			return (*join_search_hook) (root, levels_needed, initial_rels);
@@ -4558,3 +4574,81 @@ debug_print_rel(PlannerInfo *root, RelOptInfo *rel)
 }
 
 #endif							/* OPTIMIZER_DEBUG */
+
+/*
+ * set_baserel_notnull_attrs_for_quals
+ *
+ *	Set baserel's notnullattrs based on baserestrictinfo.
+ */
+static void
+set_baserel_notnull_attrs_for_quals(RelOptInfo *rel)
+{
+	List *clauses = extract_actual_clauses(rel->baserestrictinfo, false);
+	ListCell	*lc;
+	foreach(lc, find_nonnullable_vars((Node *)clauses))
+	{
+		Var *var = (Var *) lfirst(lc);
+		if (var->varno != rel->relid)
+		{
+			/* Lateral Join */
+			continue;
+		}
+		Assert(var->varno == rel->relid);
+		rel->notnull_attrs[0] = bms_add_member(rel->notnull_attrs[0],
+											   var->varattno - FirstLowInvalidHeapAttributeNumber);
+	}
+
+	/* Debug Only, Will be removed at last. */
+	if (!enable_geqo)
+	{
+		elog(INFO, "FirstLowInvalidHeapAttributeNumber = %d, BaseRel(%d), notnull_attrs = %s",
+			 FirstLowInvalidHeapAttributeNumber,
+			 rel->relid,
+			 bmsToString(rel->notnull_attrs[0]));
+	}
+}
+
+/*
+ * set_subquery_rel_notnull_attrs
+ *
+ *	We only maintained Var level notnull_attrs, that means we can only
+ * know the VAR ONLY expr in subroot->processed_tlist is nullable or not.
+ * Build notnull_attrs with the position of these Vars in
+ * subroot->processed_tlist for subquery rel.
+ */
+static void
+set_subquery_rel_notnull_attrs(RelOptInfo *rel)
+{
+	PlannerInfo *subroot = rel->subroot;
+	RelOptInfo *subrel = fetch_upper_rel(subroot, UPPERREL_FINAL, NULL);
+	bool sub_single_rel = subroot->join_rel_list == NIL;
+	int attno = 0;
+	ListCell *lc;
+
+	if (subroot->initial_rels == NIL)
+		return;
+
+	foreach(lc, subroot->processed_tlist)
+	{
+		Var *inner_var;
+		int idx;
+		attno += 1;
+
+		inner_var = (Var *)lfirst_node(TargetEntry, lc)->expr;
+
+		if (!inner_var || !IsA(inner_var, Var))
+			continue;
+
+		/*
+		 * If the subquery is from a single baserel, the Bitmapset's
+		 * index is 0, or else, it is varno.
+		 */
+		idx = sub_single_rel ? 0 : inner_var->varno;
+
+		if (bms_is_member(inner_var->varattno - FirstLowInvalidHeapAttributeNumber,
+						  subrel->notnull_attrs[idx]))
+			/* Set the notnull_attrs for upper subquery_rel. */
+			rel->notnull_attrs[0] = bms_add_member(rel->notnull_attrs[0],
+												   inner_var->varattno - FirstLowInvalidHeapAttributeNumber);
+	}
+}
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 5012bfe1425..4715845c180 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -118,6 +118,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 	Relation	relation;
 	bool		hasindex;
 	List	   *indexinfos = NIL;
+	int		i;
 
 	/*
 	 * We need not lock the relation since it was already locked, either by
@@ -472,6 +473,17 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 	if (inhparent && relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
 		set_relation_partition_info(root, rel, relation);
 
+	/*
+	 * Set baserel's notnull_attrs based on catalog.
+	 */
+	for (i = 0; i < relation->rd_att->natts; i++)
+	{
+		FormData_pg_attribute attr = relation->rd_att->attrs[i];
+		if (attr.attnotnull)
+			rel->notnull_attrs[0] = bms_add_member(rel->notnull_attrs[0],
+												   attr.attnum - FirstLowInvalidHeapAttributeNumber);
+	}
+
 	table_close(relation, NoLock);
 
 	/*
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 520409f4ba0..302f60c5397 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -16,6 +16,7 @@
 
 #include <limits.h>
 
+#include "access/sysattr.h"
 #include "miscadmin.h"
 #include "nodes/nodeFuncs.h"
 #include "optimizer/appendinfo.h"
@@ -72,7 +73,18 @@ static void build_child_join_reltarget(PlannerInfo *root,
 									   RelOptInfo *childrel,
 									   int nappinfos,
 									   AppendRelInfo **appinfos);
-
+static void set_uppper_rel_notnull_attrs(PlannerInfo *root,
+										 RelOptInfo *upper_rel,
+										 UpperRelationKind kind);
+
+static void copy_notnull_attrs_to_joinrel(RelOptInfo *joinrel, RelOptInfo *rel);
+static void set_joinrel_notnull_attrs(RelOptInfo *joinrel,
+									  RelOptInfo *outer_rel,
+									  RelOptInfo *inner_rel,
+									  List *restrictlist,
+									  SpecialJoinInfo *sjinfo);
+static void set_uppper_rel_notnull_attrs(PlannerInfo *root, RelOptInfo *upper_rel,
+										 UpperRelationKind kind);
 
 /*
  * setup_simple_rel_arrays
@@ -259,6 +271,7 @@ build_simple_rel(PlannerInfo *root, int relid, RelOptInfo *parent)
 	rel->all_partrels = NULL;
 	rel->partexprs = NULL;
 	rel->nullable_partexprs = NULL;
+	rel->notnull_attrs = palloc0(sizeof(Bitmapset *) * 1);
 
 	/*
 	 * Pass assorted information down the inheritance hierarchy.
@@ -674,6 +687,7 @@ build_join_rel(PlannerInfo *root,
 	joinrel->all_partrels = NULL;
 	joinrel->partexprs = NULL;
 	joinrel->nullable_partexprs = NULL;
+	joinrel->notnull_attrs = palloc0(sizeof(Bitmapset *) * (bms_max_member(joinrel->relids) + 1));
 
 	/* Compute information relevant to the foreign relations. */
 	set_foreign_rel_properties(joinrel, outer_rel, inner_rel);
@@ -765,6 +779,8 @@ build_join_rel(PlannerInfo *root,
 			lappend(root->join_rel_level[root->join_cur_level], joinrel);
 	}
 
+	set_joinrel_notnull_attrs(joinrel, outer_rel, inner_rel, restrictlist, sjinfo);
+
 	return joinrel;
 }
 
@@ -857,6 +873,8 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
 	joinrel->top_parent_relids = bms_union(outer_rel->top_parent_relids,
 										   inner_rel->top_parent_relids);
 
+	joinrel->notnull_attrs = palloc0(sizeof(Bitmapset *) * (bms_max_member(joinrel->relids) + 1));
+
 	/* Compute information relevant to foreign relations. */
 	set_foreign_rel_properties(joinrel, outer_rel, inner_rel);
 
@@ -916,6 +934,8 @@ build_child_join_rel(PlannerInfo *root, RelOptInfo *outer_rel,
 
 	pfree(appinfos);
 
+	set_joinrel_notnull_attrs(joinrel, outer_rel, inner_rel, restrictlist, sjinfo);
+
 	return joinrel;
 }
 
@@ -1245,6 +1265,8 @@ fetch_upper_rel(PlannerInfo *root, UpperRelationKind kind, Relids relids)
 
 	root->upper_rels[kind] = lappend(root->upper_rels[kind], upperrel);
 
+	set_uppper_rel_notnull_attrs(root, upperrel, kind);
+
 	return upperrel;
 }
 
@@ -2045,3 +2067,165 @@ build_child_join_reltarget(PlannerInfo *root,
 	childrel->reltarget->cost.per_tuple = parentrel->reltarget->cost.per_tuple;
 	childrel->reltarget->width = parentrel->reltarget->width;
 }
+
+/*
+ * copy_notnull_attrs_to_joinrel
+ *
+ *	Copy the notnull_attrs from rel to joinrel's notnull_attrs.
+ */
+static void
+copy_notnull_attrs_to_joinrel(RelOptInfo *joinrel, RelOptInfo *rel)
+{
+	int relid;
+	if (bms_get_singleton_member(rel->relids, &relid))
+		joinrel->notnull_attrs[relid] = bms_copy(rel->notnull_attrs[0]);
+	else
+	{
+		relid = -1;
+		while ((relid = bms_next_member(rel->relids, relid)) >= 0)
+			joinrel->notnull_attrs[relid] = bms_copy(rel->notnull_attrs[relid]);
+	}
+}
+
+/*
+ * set_joinrel_notnull_attrs
+ *
+ *	Set joinrel's notnull attrs infomation based on the both sides
+ * notnull_attrs info, join type, join quals.
+ */
+static void
+set_joinrel_notnull_attrs(RelOptInfo *joinrel,
+						  RelOptInfo *outer_rel,
+						  RelOptInfo *inner_rel,
+						  List *restrictlist,
+						  SpecialJoinInfo *sjinfo)
+{
+	if (sjinfo->jointype == JOIN_FULL)
+		/* Both sides are nullable. */
+		return;
+
+	/* If it is not FULL join, the outer side is not changed. */
+	copy_notnull_attrs_to_joinrel(joinrel, outer_rel);
+	switch(sjinfo->jointype)
+	{
+		case JOIN_ANTI:
+		case JOIN_SEMI:
+		case JOIN_INNER:
+			/*
+			 * No chances to add extra nullable Vars for ANTI/SEMI/INNER
+			 * join. So copy the inner_rel's notnull_attrs as well.
+			 */
+			copy_notnull_attrs_to_joinrel(joinrel, inner_rel);
+			break;
+		case JOIN_LEFT:
+			break;
+		default:
+			elog(ERROR, "Unexpected join type %d", sjinfo->jointype);
+	}
+
+	/* The join clauses may produce more not null vars. */
+	{
+		ListCell	*lc;
+		List *clauses = extract_actual_clauses(restrictlist, false);
+		Relids not_nullable_reilds = sjinfo->jointype == JOIN_LEFT ?
+				outer_rel->relids : joinrel->relids;
+
+		foreach(lc, find_nonnullable_vars((Node *) clauses))
+		{
+			Var *var = lfirst_node(Var, lc);
+			if (!bms_is_member(var->varno, not_nullable_reilds))
+			{
+				continue;
+			}
+			joinrel->notnull_attrs[var->varno] = bms_add_member(
+				joinrel->notnull_attrs[var->varno],
+				var->varattno - FirstLowInvalidHeapAttributeNumber);
+		}
+	}
+
+	/* Debug Only, will be removed at last. */
+	if (!enable_geqo)
+	{
+		int relid = -1;
+		int eLevel = INFO;
+		elog(eLevel, "Dump notnull for JoinRel(%s)", bmsToString(joinrel->relids));
+		while((relid = bms_next_member(joinrel->relids, relid)) >= 0)
+		{
+			Bitmapset *notnullattrs = joinrel->notnull_attrs[relid];
+			if (notnullattrs != NULL)
+				elog(eLevel, "FirstLowInvalidHeapAttributeNumber = %d, RELID = (%d), notnull_attrs: %s",
+					 FirstLowInvalidHeapAttributeNumber,
+					 relid,
+					 bmsToString(notnullattrs));
+		}
+	}
+}
+
+/*
+ * set_uppper_rel_notnull_attrs
+ *
+ *	Set notnull_attrs for upper relation.
+ *
+ *	Upper rel is impossible to generate new nullable vars.
+ * Just let upper_rel use the same notnull_attrs as the
+ * last joinrel.
+ *
+ * SetOp is an exception since we needs consider both sides.
+ */
+static void
+set_uppper_rel_notnull_attrs(PlannerInfo *root, RelOptInfo *upper_rel, UpperRelationKind kind)
+{
+	RelOptInfo *final_joinrel;
+	int relid;
+
+	if (kind == UPPERREL_SETOP)
+		/*
+		 * Need to handle specially for SETOP, both sides need considering
+		 * to set a SETOP rel.
+		 */
+		return;
+
+	if (root->initial_rels == NIL)
+		/* there is no initial rels at all, so impossible to have notnull vars. */
+		return;
+
+	if (root->join_rel_list == NIL)
+		final_joinrel = linitial_node(RelOptInfo, root->initial_rels);
+	else
+		final_joinrel = llast_node(RelOptInfo, root->join_rel_list);
+
+	upper_rel->notnull_attrs = final_joinrel->notnull_attrs;
+
+	/* Debug Only, should be removed from code review. */
+	if (!enable_geqo)
+	{
+		int eLevel = INFO;
+		elog(eLevel, "Dump notnull for UpperRel(%s) for kind %d",
+			 bmsToString(upper_rel->relids),
+			 kind);
+		if (bms_get_singleton_member(final_joinrel->relids, &relid))
+		{
+			Bitmapset *notnullattrs = upper_rel->notnull_attrs[0];
+			if (notnullattrs != NULL)
+				elog(eLevel, "kind = %d FirstLowInvalidHeapAttributeNumber = %d, RELID = (%d), notnull_attrs: %s",
+					 kind,
+					 FirstLowInvalidHeapAttributeNumber,
+					 relid,
+					 bmsToString(notnullattrs));
+		}
+		else
+		{
+			relid = -1;
+			while((relid = bms_next_member(final_joinrel->relids, relid)) >= 0)
+			{
+				Bitmapset *notnullattrs = upper_rel->notnull_attrs[relid];
+				if (notnullattrs != NULL)
+					elog(eLevel, "kind = %d FirstLowInvalidHeapAttributeNumber = %d, RELID = (%d), notnull_attrs: %s",
+						 kind,
+						 FirstLowInvalidHeapAttributeNumber,
+						 relid,
+						 bmsToString(notnullattrs));
+			}
+		}
+	}
+}
diff --git a/src/include/nodes/bitmapset.h b/src/include/nodes/bitmapset.h
index 75b5ce1a8e4..e1d44e14246 100644
--- a/src/include/nodes/bitmapset.h
+++ b/src/include/nodes/bitmapset.h
@@ -94,6 +94,7 @@ extern bool bms_nonempty_difference(const Bitmapset *a, const Bitmapset *b);
 extern int	bms_singleton_member(const Bitmapset *a);
 extern bool bms_get_singleton_member(const Bitmapset *a, int *member);
 extern int	bms_num_members(const Bitmapset *a);
+extern int	bms_max_member(const Bitmapset *a);
 
 /* optimized tests when we don't need to know exact membership count: */
 extern BMS_Membership bms_membership(const Bitmapset *a);
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index a6e5db4eecc..9bc729a9802 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -692,6 +692,20 @@ typedef struct RelOptInfo
 	/* default result targetlist for Paths scanning this relation */
 	struct PathTarget *reltarget;	/* list of Vars/Exprs, cost, width */
 
+	/*
+	 * Record which Var is not nullable for current RelOptInfo
+	 * after all the quals on this RelOptInfo are executed.
+	 *
+	 * For a joinrel, the length of notnull_attrs array equals the
+	 * max value in relids, the array index is varno and the value
+	 * is a Bitmapset * which presents which varattnos are not
+	 * nullable for the varno.
+	 *
+	 * For baserel, to save space, notnull_attrs[0] is always used
+	 * for current RelOptInfo.
+	 */
+	Bitmapset	**notnull_attrs;
+
 	/* materialization information */
 	List	   *pathlist;		/* Path structures */
 	List	   *ppilist;		/* ParamPathInfos used in pathlist */
diff --git a/src/test/regress/expected/notnulltest.out b/src/test/regress/expected/notnulltest.out
new file mode 100644
index 00000000000..cacae16edff
--- /dev/null
+++ b/src/test/regress/expected/notnulltest.out
@@ -0,0 +1,283 @@
+set geqo to off;
+create table t1(a int, b int not null, c int, d int);
+create table t2(a int, b int not null, c int, d int);
+-- single rel
+select * from t1;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+ a | b | c | d 
+---+---+---+---
+(0 rows)
+
+select * from t1 where a > 1;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+ a | b | c | d 
+---+---+---+---
+(0 rows)
+
+select * from t2 where a > 1 or c > 1;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+ a | b | c | d 
+---+---+---+---
+(0 rows)
+
+-- partitioned relation.
+-- append rel: all the childrel are not nullable.
+create table p (a int, b int, c int not null) partition by range(a);
+create table p_1 partition of p for values from (0) to (10000) partition by list(b);
+create table p_1_1(b int,  c int not null, a int);
+alter table p_1 attach partition p_1_1 for values in (1);
+create table p_2 partition of p for values from (10001) to (20000);
+-- p(1)  - 3(c)
+-- p_1(2) - 3(c)
+-- p_1_1(3) - 2(c)
+select * from p;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(3), notnull_attrs = (b 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(2), notnull_attrs = (b 10)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(4), notnull_attrs = (b 10)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 10)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 10)
+ a | b | c 
+---+---+---
+(0 rows)
+
+-- p(1)  - 3(c) 1(a)
+-- p_1(2) - 3(c) 1(a)
+-- p_1_1(3) - 2(c) 3(a)
+select * from p where a > 1;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(3), notnull_attrs = (b 9 10)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(2), notnull_attrs = (b 8 10)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(4), notnull_attrs = (b 8 10)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 8 10)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 10)
+ a | b | c 
+---+---+---
+(0 rows)
+
+-- test join:
+-- t1: b
+-- t2: b
+-- t{1, 2}:  t1.a, t1.b t2.b t2.c
+select * from t1, t2 where t1.a = t2.c;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(2), notnull_attrs = (b 9)
+INFO:  Dump notnull for JoinRel((b 1 2))
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, RELID = (2), notnull_attrs: (b 9 10)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (2), notnull_attrs: (b 9 10)
+ a | b | c | d | a | b | c | d 
+---+---+---+---+---+---+---+---
+(0 rows)
+
+-- t1: b
+-- t2: b
+-- t{1, 2}:  none due to full join
+select * from t1 full join t2 on t1.a = t2.a;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(2), notnull_attrs = (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+ a | b | c | d | a | b | c | d 
+---+---+---+---+---+---+---+---
+(0 rows)
+
+-- t1: b
+-- t2: b
+-- t{1, 2}:  t1.b t1.a   (t2.a t2.b is nullable due to outer join)
+select * from t1 left join t2 on t1.a = t2.a;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(2), notnull_attrs = (b 9)
+INFO:  Dump notnull for JoinRel((b 1 2))
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+ a | b | c | d | a | b | c | d 
+---+---+---+---+---+---+---+---
+(0 rows)
+
+-- test upper rel.
+select * from t1 order by a;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 6
+INFO:  kind = 6 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+ a | b | c | d 
+---+---+---+---
+(0 rows)
+
+select * from t1 where a > 1 order by a;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 6
+INFO:  kind = 6 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+ a | b | c | d 
+---+---+---+---
+(0 rows)
+
+select * from t2 where a > 1 or c > 1 order by a;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 6
+INFO:  kind = 6 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+ a | b | c | d 
+---+---+---+---
+(0 rows)
+
+select a, count(*) from t1 group by a;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 2
+INFO:  kind = 2 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+ a | count 
+---+-------
+(0 rows)
+
+select a, count(*) from t1 where a > 1 group by a;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 2
+INFO:  kind = 2 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+ a | count 
+---+-------
+(0 rows)
+
+select a, count(*) from t2 where a > 1 or c > 1 group by a;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 2
+INFO:  kind = 2 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+ a | count 
+---+-------
+(0 rows)
+
+select DISTINCT * from t1 order by a;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 5
+INFO:  kind = 5 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 6
+INFO:  kind = 6 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+ a | b | c | d 
+---+---+---+---
+(0 rows)
+
+select DISTINCT * from t1 where a > 1 order by a;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 5
+INFO:  kind = 5 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 6
+INFO:  kind = 6 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+ a | b | c | d 
+---+---+---+---
+(0 rows)
+
+select DISTINCT * from t2 where a > 1 or c > 1 order by a;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 5
+INFO:  kind = 5 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 6
+INFO:  kind = 6 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+ a | b | c | d 
+---+---+---+---
+(0 rows)
+
+select * from t1 left join t2 on t1.a = t2.a order by t2.a;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(2), notnull_attrs = (b 9)
+INFO:  Dump notnull for JoinRel((b 1 2))
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 6
+INFO:  kind = 6 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+ a | b | c | d | a | b | c | d 
+---+---+---+---+---+---+---+---
+(0 rows)
+
+select * from t1 join t2 on t1.a = t2.a order by t2.a;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(2), notnull_attrs = (b 9)
+INFO:  Dump notnull for JoinRel((b 1 2))
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, RELID = (2), notnull_attrs: (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 6
+INFO:  kind = 6 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  kind = 6 FirstLowInvalidHeapAttributeNumber = -7, RELID = (2), notnull_attrs: (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (2), notnull_attrs: (b 8 9)
+ a | b | c | d | a | b | c | d 
+---+---+---+---+---+---+---+---
+(0 rows)
+
+--  subquery
+select * from
+(select t1.a as t1a, t1.b as t1b, t1.c as t1c,
+ t2.* from t1 join t2 on t1.a = t2.a order by t2.a limit 3) as x
+where t1c > 3;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(2), notnull_attrs = (b 9)
+INFO:  Dump notnull for JoinRel((b 1 2))
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, RELID = (2), notnull_attrs: (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 6
+INFO:  kind = 6 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  kind = 6 FirstLowInvalidHeapAttributeNumber = -7, RELID = (2), notnull_attrs: (b 8 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9)
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (2), notnull_attrs: (b 8 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 8 9 10)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 8 9 10)
+ t1a | t1b | t1c | a | b | c | d 
+-----+-----+-----+---+---+---+---
+(0 rows)
+
+-- SetOp RelOptInfo.
+-- simple union all
+select * from t1
+union all
+select * from t2;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(4), notnull_attrs = (b 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(5), notnull_attrs = (b 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+ a | b | c | d 
+---+---+---+---
+(0 rows)
+
+select * from t1
+union
+select * from t2;
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+INFO:  FirstLowInvalidHeapAttributeNumber = -7, BaseRel(1), notnull_attrs = (b 9)
+INFO:  Dump notnull for UpperRel((b)) for kind 7
+INFO:  kind = 7 FirstLowInvalidHeapAttributeNumber = -7, RELID = (1), notnull_attrs: (b 9)
+ a | b | c | d 
+---+---+---+---
+(0 rows)
+
+drop table t1;
+drop table t2;
+drop table p;
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 103e11483d2..f1493cdb66b 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -135,3 +135,5 @@ test: event_trigger oidjoins
 
 # this test also uses event triggers, so likewise run it by itself
 test: fast_default
+
+test: notnulltest
diff --git a/src/test/regress/sql/notnulltest.sql b/src/test/regress/sql/notnulltest.sql
new file mode 100644
index 00000000000..e6c127b7355
--- /dev/null
+++ b/src/test/regress/sql/notnulltest.sql
@@ -0,0 +1,87 @@
+set geqo to off;
+create table t1(a int, b int not null, c int, d int);
+create table t2(a int, b int not null, c int, d int);
+
+-- single rel
+select * from t1;
+select * from t1 where a > 1;
+select * from t2 where a > 1 or c > 1;
+
+-- partitioned relation.
+-- append rel: all the childrel are not nullable.
+create table p (a int, b int, c int not null) partition by range(a);
+create table p_1 partition of p for values from (0) to (10000) partition by list(b);
+create table p_1_1(b int,  c int not null, a int);
+alter table p_1 attach partition p_1_1 for values in (1);
+create table p_2 partition of p for values from (10001) to (20000);
+
+-- p(1)  - 3(c)
+-- p_1(2) - 3(c)
+-- p_1_1(3) - 2(c)
+select * from p;
+-- p(1)  - 3(c) 1(a)
+-- p_1(2) - 3(c) 1(a)
+-- p_1_1(3) - 2(c) 3(a)
+select * from p where a > 1;
+
+-- test join:
+-- t1: b
+-- t2: b
+-- t{1, 2}:  t1.a, t1.b t2.b t2.c
+select * from t1, t2 where t1.a = t2.c;
+
+-- t1: b
+-- t2: b
+-- t{1, 2}:  none due to full join
+select * from t1 full join t2 on t1.a = t2.a;
+
+-- t1: b
+-- t2: b
+-- t{1, 2}:  t1.b t1.a   (t2.a t2.b is nullable due to outer join)
+select * from t1 left join t2 on t1.a = t2.a;
+
+
+-- test upper rel.
+select * from t1 order by a;
+select * from t1 where a > 1 order by a;
+select * from t2 where a > 1 or c > 1 order by a;
+
+select a, count(*) from t1 group by a;
+select a, count(*) from t1 where a > 1 group by a;
+select a, count(*) from t2 where a > 1 or c > 1 group by a;
+
+select DISTINCT * from t1 order by a;
+select DISTINCT * from t1 where a > 1 order by a;
+select DISTINCT * from t2 where a > 1 or c > 1 order by a;
+
+select * from t1 left join t2 on t1.a = t2.a order by t2.a;
+
+select * from t1 join t2 on t1.a = t2.a order by t2.a;
+
+--  subquery
+
+select * from
+(select t1.a as t1a, t1.b as t1b, t1.c as t1c,
+ t2.* from t1 join t2 on t1.a = t2.a order by t2.a limit 3) as x
+where t1c > 3;
+
+
+-- SetOp RelOptInfo.
+
+
+-- simple union all
+
+select * from t1
+union all
+select * from t2;
+
+select * from t1
+union
+select * from t2;
+
+
+
+
+drop table t1;
+drop table t2;
+drop table p;
-- 
2.21.0

