Subject: Re: Support NOT VALID / VALIDATE constraint options for named NOT NULL constraints

From: Alvaro Herrera <[email-masked]>
Date: Fri, 21 Feb 2025 07:13:39 +0100

Hello,

Thanks!

I noticed a typo 'constrint' in several places; fixed in the attached.

I discovered that this sequence, taken from added regression tests,

CREATE TABLE notnull_tbl1 (a int);
ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent not null a not valid;
CREATE TABLE notnull_chld (a int);
ALTER TABLE notnull_chld ADD CONSTRAINT nn_child not null a not valid;
ALTER TABLE notnull_chld INHERIT notnull_tbl1;
ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;

does mark the constraint as validated in the child, but only in
pg_constraint -- pg_attribute continues to be marked as 'i', so if you
try to use it for a PK, it fails:

alter table notnull_chld add constraint foo primary key (a);
ERROR:  column "a" of table "notnull_chld" is marked as NOT VALID NOT NULL constrint

I thought that was going to be a quick fix, so I tried to do so; since
we already have a function 'set_attnotnull', I thought it was the
perfect tool to changing attnotnull.  However, it's not great, because
since that part of the code is already doing the validation, I don't
want it to queue the validation again, so the API needs a tweak; I
changed it to receiving separately which new value to update attnotnull
to, and whether to queue validation.  With that change it works
correctly, but it is a bit ugly at the callers' side.  Maybe it works to
pass two booleans instead?  Please have a look at whether that can be
improved.

I also noticed the addition of function getNNConnameForAttnum(), which
does pretty much the same as findNotNullConstraintAttnum(), only it
ignores all validate constraints instead of ignoring all non-validated
constraints.  So after looking at the callers of the existing function
and wondering which ones of them really wanted only the validated
constraints?  It turns that the answer is none of them.  So I decided to
remove the check for that, and instead we need to add checks to every
caller of both findNotNullConstraintAttnum() and findNotNullConstraint()
so that it acts appropriately when a non-validated constraint is
returned.  I added a few elog(WARNING)s when this happens; running the
tests I notice that none of them fire.  I'm pretty sure this indicates
holes in testing: we have no test cases for these scenarios, and we
should have them for assurance that we're doing the right things.  I
recommend that you go over those WARNINGs, add test cases that make them
fire, and then fix the code so that the test cases do the right thing.
Also, just to be sure, please go over _all_ the callers of
those two functions and make sure all cases are covered by tests that
catch invalid constraints.

I also noticed that in the one place where getNNConnameForAttnum() was
called, we were passing the parent table's column number.  But in child
tables, even in partitions, the column numbers can differ from parent to
children.  So we need to walk down the hierarchy using the column name,
not the column number.  This would have become visible if the test cases
had included inheritance trees with varying column shapes.

The docs continue to say this:
      This form adds a new constraint to a table using the same constraint
      syntax as <link linkend="sql-createtable"><command>CREATE TABLE</command></link>, plus the option <literal>NOT
      VALID</literal>, which is currently only allowed for foreign key
      and CHECK constraints.
which is missing to indicate that NOT VALID is valid for NOT NULL.

Also I think the docs for attnotnull in catalogs.sgml are a bit too
terse; I would write "The value 't' indicates that a not-null constraint
exists for the column; 'i' for an invalid constraint, 'f' for none."
which please feel free to use if you want, but if you want to come up
with your own wording, that's great too.

The InsertOneNull() function used in bootstrap would not test values for
nullness in presence of invalid constraints.  This change is mostly
pro-forma, since we don't expect invalid constraints during bootstrap,
but it seemed better to be tidy.

I have not looked at the pg_dump code yet, so the changes included here
are just pgindent.

Thank you!

-- 
Álvaro Herrera               48°01'N 7°57'E  —  https://www.EnterpriseDB.com/
>From ac8e690c64d354faad421a48f5adf0e26a15202b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <[email-masked]>
Date: Thu, 20 Feb 2025 13:05:35 +0100
Subject: [PATCH] fixup

---
 src/backend/bootstrap/bootstrap.c         |   4 +-
 src/backend/catalog/pg_constraint.c       |  17 +-
 src/backend/commands/tablecmds.c          | 229 ++++++++++------------
 src/bin/pg_dump/pg_dump.c                 |  15 +-
 src/bin/pg_dump/pg_dump.h                 |   2 +-
 src/test/regress/expected/constraints.out |  10 +-
 src/test/regress/sql/constraints.sql      |   8 +-
 7 files changed, 131 insertions(+), 154 deletions(-)

diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 1e95dc32f46..919972dc409 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -582,10 +582,8 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
 
 	attrtypes[attnum]->atttypmod = -1;
 	attrtypes[attnum]->attislocal = true;
-	/* set default to false */
 	attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
 
-
 	if (nullness == BOOTCOL_NULL_FORCE_NOT_NULL)
 	{
 		attrtypes[attnum]->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
@@ -699,7 +697,7 @@ InsertOneNull(int i)
 {
 	elog(DEBUG4, "inserting column %d NULL", i);
 	Assert(i >= 0 && i < MAXATTR);
-	if (TupleDescAttr(boot_reldesc->rd_att, i)->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
+	if (TupleDescAttr(boot_reldesc->rd_att, i)->attnotnull != ATTRIBUTE_NOTNULL_FALSE)
 		elog(ERROR,
 			 "NULL value specified for not-null column \"%s\" of relation \"%s\"",
 			 NameStr(TupleDescAttr(boot_reldesc->rd_att, i)->attname),
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..f856f387502 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -574,7 +574,7 @@ ChooseConstraintName(const char *name1, const char *name2,
 
 /*
  * Find and return a copy of the pg_constraint tuple that implements a
- * validated not-null constraint for the given column of the given relation.
+ * not-null constraint for the given column of the given relation.
  * If no such constraint exists, return NULL.
  *
  * XXX This would be easier if we had pg_attribute.notnullconstr with the OID
@@ -604,13 +604,11 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
 		AttrNumber	conkey;
 
 		/*
-		 * We're looking for a NOTNULL constraint that's marked validated,
-		 * with the column we're looking for as the sole element in conkey.
+		 * We're looking for a NOTNULL constraint with the column we're
+		 * looking for as the sole element in conkey.
 		 */
 		if (con->contype != CONSTRAINT_NOTNULL)
 			continue;
-		if (!con->convalidated)
-			continue;
 
 		conkey = extractNotNullColumn(conTup);
 		if (conkey != attnum)
@@ -628,9 +626,9 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
 }
 
 /*
- * Find and return the pg_constraint tuple that implements a validated
- * not-null constraint for the given column of the given relation.  If
- * no such column or no such constraint exists, return NULL.
+ * Find and return the pg_constraint tuple that implements a
+ * not-null constraint for the given column of the given relation.
+ * If no such column or no such constraint exists, return NULL.
  */
 HeapTuple
 findNotNullConstraint(Oid relid, const char *colname)
@@ -743,6 +741,9 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
 		pg_constraint = table_open(ConstraintRelationId, RowExclusiveLock);
 		conform = (Form_pg_constraint) GETSTRUCT(tup);
 
+		if (!conform->convalidated)
+			elog(WARNING, "got an unvalidated constraint");
+
 		/*
 		 * If the NO INHERIT flag we're asked for doesn't match what the
 		 * existing constraint has, throw an error.
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1edda86fafd..793b81fc6c8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -406,7 +406,7 @@ static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relat
 										   char *constrName, HeapTuple contuple,
 										   bool recurse, bool recursing, LOCKMODE lockmode);
 static void QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
-										char *constrName, HeapTuple contuple,
+										HeapTuple contuple,
 										bool recurse, bool recursing, LOCKMODE lockmode);
 static int	transformColumnNameList(Oid relId, List *colList,
 									int16 *attnums, Oid *atttypids, Oid *attcollids);
@@ -471,7 +471,8 @@ static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid)
 static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
 									   LOCKMODE lockmode);
 static void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
-						   bool skip_validation, LOCKMODE lockmode);
+						   char newvalue, bool queue_validation,
+						   LOCKMODE lockmode);
 static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel,
 									  char *constrname, char *colName,
 									  bool recurse, bool recursing,
@@ -701,7 +702,6 @@ static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, const char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
 static bool check_for_invalid_notnull(Oid relid, const char *attname);
-static char *getNNConnameForAttnum(Oid relid, AttrNumber  attnum);

 /* ----------------------------------------------------------------
@@ -1319,7 +1319,8 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints,
 										   old_notnulls);
 	foreach_int(attrnum, nncols)
-		set_attnotnull(NULL, rel, attrnum, false, NoLock);
+		set_attnotnull(NULL, rel, attrnum, ATTRIBUTE_NOTNULL_TRUE,
+					   false, NoLock);
 
 	ObjectAddressSet(address, RelationRelationId, relationId);
 
@@ -7717,10 +7718,13 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
  */
 static void
 set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
-			   bool skip_validation, LOCKMODE lockmode)
+			   char newvalue, bool queue_validation, LOCKMODE lockmode)
 {
 	Form_pg_attribute attr;
 
+
+	Assert(!queue_validation || wqueue != NULL);
+
 	CheckAlterTableIsSafe(rel);
 
 	/*
@@ -7731,7 +7735,7 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
 	if (attr->attisdropped)
 		return;
 
-	if (attr->attnotnull == ATTRIBUTE_NOTNULL_FALSE)
+	if (attr->attnotnull != newvalue)
 	{
 		Relation	attr_rel;
 		HeapTuple	tuple;
@@ -7744,20 +7748,17 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
 				 attnum, RelationGetRelid(rel));
 
 		attr = (Form_pg_attribute) GETSTRUCT(tuple);
-		Assert(attr->attnotnull == ATTRIBUTE_NOTNULL_FALSE);
+		attr->attnotnull = newvalue;
 
-		if (skip_validation)
-			attr->attnotnull = ATTRIBUTE_NOTNULL_INVALID;
-		else
-			attr->attnotnull = ATTRIBUTE_NOTNULL_TRUE;
 		CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
 
 		/*
-		 * If the nullness isn't already proven by validated constraints, have
-		 * ALTER TABLE phase 3 test for it.
+		 * Queue later validation of this constraint, if necessary and
+		 * requested by caller.
 		 */
-		if (!skip_validation &&
-			wqueue && !NotNullImpliedByRelConstraints(rel, attr))
+		if (queue_validation &&
+			newvalue == ATTRIBUTE_NOTNULL_TRUE &&
+			!NotNullImpliedByRelConstraints(rel, attr))
 		{
 			AlteredTableInfo *tab;
 
@@ -7844,6 +7845,9 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 						   NameStr(conForm->conname),
 						   RelationGetRelationName(rel)));
 
+		if (!conForm->convalidated)
+			elog(WARNING, "trying to add a constraint where an invalid one already exists");
+
 		/*
 		 * If we find an appropriate constraint, we're almost done, but just
 		 * need to change some properties on it: if we're recursing, increment
@@ -7925,8 +7929,12 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 	InvokeObjectPostAlterHook(RelationRelationId,
 							  RelationGetRelid(rel), attnum);
 
-	/* Mark pg_attribute.attnotnull for the column */
-	set_attnotnull(wqueue, rel, attnum, constraint->skip_validation, lockmode);
+	/* Mark pg_attribute.attnotnull for the column and request validation */
+	set_attnotnull(wqueue, rel, attnum,
+				   constraint->skip_validation ?
+				   ATTRIBUTE_NOTNULL_INVALID :
+				   ATTRIBUTE_NOTNULL_TRUE,
+				   !constraint->skip_validation, lockmode);
 
 	/*
 	 * Recurse to propagate the constraint to children that don't have one.
@@ -9368,22 +9376,19 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
 	}
 
 	/* Insert not-null constraints in the queue for the PK columns */
-	foreach(lc, pkconstr->keys)
+	foreach_node(String, colname, pkconstr->keys)
 	{
 		AlterTableCmd *newcmd;
 		Constraint *nnconstr;
-		String *colname = lfirst(lc);
 
-		/*
-		 * Throw an error if relation key column has invalid not null
-		 * constraint.
-		 */
-		if (check_for_invalid_notnull(RelationGetRelid(rel), colname->sval))
+		/* Verify that the not-null constraint has been validated */
+		if (check_for_invalid_notnull(RelationGetRelid(rel), strVal(colname)))
 			ereport(ERROR,
-					errmsg("column \"%s\" of table \"%s\" is marked as NOT VALID NOT NULL constrint",
-						   colname->sval, RelationGetRelationName(rel)));
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("column \"%s\" of table \"%s\" is marked as NOT VALID NOT NULL constraint",
+						   strVal(colname), RelationGetRelationName(rel)));
 
-		nnconstr = makeNotNullConstraint(lfirst(lc));
+		nnconstr = makeNotNullConstraint(colname);
 
 		newcmd = makeNode(AlterTableCmd);
 		newcmd->subtype = AT_AddConstraint;
@@ -9785,7 +9790,12 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		 * phase 3 to verify existing rows, if needed.
 		 */
 		if (constr->contype == CONSTR_NOTNULL)
-			set_attnotnull(wqueue, rel, ccon->attnum, ccon->skip_validation, lockmode);
+			set_attnotnull(wqueue, rel, ccon->attnum,
+						   ccon->skip_validation ?
+						   ATTRIBUTE_NOTNULL_INVALID :
+						   ATTRIBUTE_NOTNULL_TRUE,
+						   !ccon->skip_validation,
+						   lockmode);
 
 		ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
 	}
@@ -12203,7 +12213,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 		con->contype != CONSTRAINT_NOTNULL)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
-				 errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
+				 errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key, not-null, or check constraint",
 						constrName, RelationGetRelationName(rel))));
 
 	if (!con->conenforced)
@@ -12224,7 +12234,7 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 		}
 		else if (con->contype == CONSTRAINT_NOTNULL)
 		{
-			QueueNNConstraintValidation(wqueue, conrel, rel, constrName,
+			QueueNNConstraintValidation(wqueue, conrel, rel,
 										tuple, recurse, recursing, lockmode);
 		}
 
@@ -12356,9 +12366,7 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 	AlteredTableInfo *tab;
 	HeapTuple	copyTuple;
 	Form_pg_constraint copy_con;
-
 	List	   *children = NIL;
-	ListCell   *child;
 	NewConstraint *newcon;
 	Datum		val;
 	char	   *conbin;
@@ -12367,24 +12375,19 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 	Assert(con->contype == CONSTRAINT_CHECK);
 
 	/*
-	 * If we're recursing, the parent has already done this, so skip it. Also,
-	 * if the constraint is a NO INHERIT constraint, we shouldn't try to look
-	 * for it in the children.
-	 */
-	if (!recursing && !con->connoinherit)
-		children = find_all_inheritors(RelationGetRelid(rel),
-									   lockmode, NULL);
-
-	/*
-	 * For CHECK constraints, we must ensure that we only mark the constraint
-	 * as validated on the parent if it's already validated on the children.
+	 * For constraints that aren't NO INHERIT, we must ensure that we only
+	 * mark the constraint as validated on the parent if it's already
+	 * validated on the children.
+	 *
+	 * If we're recursing, the parent has already done this, so skip it.
 	 *
 	 * We recurse before validating on the parent, to reduce risk of
 	 * deadlocks.
 	 */
-	foreach(child, children)
+	if (!recursing && !con->connoinherit)
+		children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+	foreach_oid(childoid, children)
 	{
-		Oid			childoid = lfirst_oid(child);
 		Relation	childrel;
 
 		if (childoid == RelationGetRelid(rel))
@@ -12446,23 +12449,22 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 /*
  * QueueNNConstraintValidation
  *
- * Add an entry to the wqueue to validate the given notnull constraint in Phase 3
- * and update the convalidated field in the pg_constraint catalog for the
- * specified relation and all its inheriting children.
+ * Add an entry to the wqueue to validate the given not-null constraint in
+ * Phase 3 and update the convalidated field in the pg_constraint catalog for
+ * the specified relation and all its inheriting children.
  */
 static void
 QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
-						  char *constrName, HeapTuple contuple,
-						  bool recurse, bool recursing, LOCKMODE lockmode)
+							HeapTuple contuple, bool recurse, bool recursing,
+							LOCKMODE lockmode)
 {
 	Form_pg_constraint con;
 	AlteredTableInfo *tab;
 	HeapTuple	copyTuple;
 	Form_pg_constraint copy_con;
-
 	List	   *children = NIL;
-	ListCell   *child;
-	AttrNumber  attnum;
+	AttrNumber	attnum;
+	char	   *colname;
 
 	con = (Form_pg_constraint) GETSTRUCT(contuple);
 	Assert(con->contype == CONSTRAINT_NOTNULL);
@@ -12470,25 +12472,24 @@ QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 	attnum = extractNotNullColumn(contuple);
 
 	/*
-	 * If we're recursing, the parent has already done this, so skip it. Also,
-	 * if the constraint is a NO INHERIT constraint, we shouldn't try to look
-	 * for it in the children.
-	 */
-	if (!recursing && !con->connoinherit)
-		children = find_all_inheritors(RelationGetRelid(rel),
-									   lockmode, NULL);
-
-	/*
-	 * For CHECK constraints, we must ensure that we only mark the constraint
-	 * as validated on the parent if it's already validated on the children.
+	 * For constraints that aren't NO INHERIT, we must ensure that we only
+	 * mark the constraint as validated on the parent if it's already
+	 * validated on the children.
+	 *
+	 * If we're recursing, the parent has already done this, so skip it.
 	 *
 	 * We recurse before validating on the parent, to reduce risk of
 	 * deadlocks.
 	 */
-	foreach(child, children)
+	if (!recursing && !con->connoinherit)
+		children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+
+	colname = get_attname(RelationGetRelid(rel), attnum, false);
+	foreach_oid(childoid, children)
 	{
-		Oid			childoid = lfirst_oid(child);
 		Relation	childrel;
+		HeapTuple	contup;
+		Form_pg_constraint childcon;
 		char	   *conname;
 
 		if (childoid == RelationGetRelid(rel))
@@ -12501,21 +12502,31 @@ QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 		 */
 		if (!recurse)
 			ereport(ERROR,
-					(errcode(ERRCODE_INVALID_TABLE_DEFINITION),
-					 errmsg("constraint must be validated on child tables too")));
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("constraint must be validated on child tables too"));
+
+		/*
+		 * The column on child might have a different attnum, so search by
+		 * column name.
+		 */
+		contup = findNotNullConstraint(childoid, colname);
+		if (!contup)
+			elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
+				 colname, get_rel_name(childoid));
+		childcon = (Form_pg_constraint) GETSTRUCT(contup);
+		if (childcon->convalidated)
+			continue;
 
 		/* find_all_inheritors already got lock */
 		childrel = table_open(childoid, NoLock);
-		conname = getNNConnameForAttnum(childoid, attnum);
-		if (conname == NULL)
-			continue;
+		conname = pstrdup(NameStr(childcon->conname));
 
+		/* XXX improve ATExecValidateConstraint API to avoid double search */
 		ATExecValidateConstraint(wqueue, childrel, conname,
 								 false, true, lockmode);
 		table_close(childrel, NoLock);
 	}
 
-
 	tab = ATGetQueueEntry(wqueue, rel);
 	tab->verify_new_notnull = true;
 
@@ -12525,64 +12536,22 @@ QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 	CacheInvalidateRelcache(rel);
 
 	/*
-	 * Now update the catalog, while we have the door open.
+	 * Now update the catalogs, while we have the door open.
 	 */
 	copyTuple = heap_copytuple(contuple);
 	copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
 	copy_con->convalidated = true;
 	CatalogTupleUpdate(conrel, &copyTuple->t_self, copyTuple);
 
+	/* Also flip attnotnull */
+	set_attnotnull(wqueue, rel, attnum, ATTRIBUTE_NOTNULL_TRUE, false,
+				   lockmode);
+
 	InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
 
 	heap_freetuple(copyTuple);
 }
 
-/*
- * Function returns the invalid not null constrint name for the given
- * relation and attnumber.
- */
-static char *
-getNNConnameForAttnum(Oid relid, AttrNumber  attnum)
-{
-	Relation	constrRel;
-	HeapTuple	htup;
-	SysScanDesc conscan;
-	ScanKeyData skey;
-	char	   *conname = NULL;
-
-	constrRel = table_open(ConstraintRelationId, AccessShareLock);
-	ScanKeyInit(&skey,
-				Anum_pg_constraint_conrelid,
-				BTEqualStrategyNumber, F_OIDEQ,
-				ObjectIdGetDatum(relid));
-	conscan = systable_beginscan(constrRel, ConstraintRelidTypidNameIndexId, true,
-								 NULL, 1, &skey);
-
-	while (HeapTupleIsValid(htup = systable_getnext(conscan)))
-	{
-		Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(htup);
-		AttrNumber	colnum;
-
-		if (conForm->contype != CONSTRAINT_NOTNULL)
-			continue;
-		if (conForm->convalidated == true)
-			continue;
-
-		colnum = extractNotNullColumn(htup);
-
-		if (colnum != attnum)
-			continue;
-
-		conname = pstrdup(NameStr(conForm->conname));
-		break;
-	}
-
-	systable_endscan(conscan);
-	table_close(constrRel, AccessShareLock);
-
-	return conname;
-}
-
 /*
  * transformColumnNameList - transform list of column names
  *
@@ -13454,7 +13423,7 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
 						   RelationGetRelationName(rel)));
 
 		/* All good -- reset attnotnull if needed */
-		if (attForm->attnotnull == ATTRIBUTE_NOTNULL_TRUE)
+		if (attForm->attnotnull != ATTRIBUTE_NOTNULL_FALSE)
 		{
 			attForm->attnotnull = ATTRIBUTE_NOTNULL_FALSE;
 			CatalogTupleUpdate(attrel, &atttup->t_self, atttup);
@@ -16790,12 +16759,20 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 
 				contup = findNotNullConstraintAttnum(RelationGetRelid(parent_rel),
 													 parent_att->attnum);
-				if (HeapTupleIsValid(contup) &&
-					!((Form_pg_constraint) GETSTRUCT(contup))->connoinherit)
-					ereport(ERROR,
-							errcode(ERRCODE_DATATYPE_MISMATCH),
-							errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL",
-								   parent_attname, RelationGetRelationName(child_rel)));
+				if (HeapTupleIsValid(contup))
+				{
+					Form_pg_constraint	childcon;
+
+					childcon = (Form_pg_constraint) GETSTRUCT(contup);
+					if (!childcon->connoinherit)
+						ereport(ERROR,
+								errcode(ERRCODE_DATATYPE_MISMATCH),
+								errmsg("column \"%s\" in child table \"%s\" must be marked NOT NULL",
+									   parent_attname, RelationGetRelationName(child_rel)));
+
+					if (!childcon->convalidated)
+						elog(WARNING, "found an invalid constraint");
+				}
 			}
 
 			/*
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c63b4504cee..7f0c39db88d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -9124,7 +9124,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			tbinfo->notnull_valid[j] = (PQgetvalue(res, r, i_notnull_valid)[0] == 't');
 
 			/*
-			 * Dump the invalid NOT NULL constrint like the Check constraints
+			 * Dump the invalid NOT NULL constraint like the Check constraints
 			 */
 			if (tbinfo->notnull_valid[j])
 				/* Handle not-null constraint name and flags */
@@ -9132,13 +9132,14 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 									  tbinfo, j,
 									  i_notnull_name, i_notnull_noinherit,
 									  i_notnull_islocal);
-			else if(!PQgetisnull(res, r, i_notnull_name))
+			else if (!PQgetisnull(res, r, i_notnull_name))
 			{
 				/*
-				 * Add the entry into invalidnotnull list so NOT NULL constraint get
-				 * dump as separate constrints.
+				 * Add the entry into invalidnotnull list so NOT NULL
+				 * constraint get dump as separate constraints.
 				 */
-				if (invalidnotnulloids->len > 1) /* do we have more than the '{'? */
+				if (invalidnotnulloids->len > 1)	/* do we have more than
+													 * the '{'? */
 					appendPQExpBufferChar(invalidnotnulloids, ',');
 				appendPQExpBuffer(invalidnotnulloids, "%u", tbinfo->dobj.catId.oid);
 
@@ -9429,8 +9430,8 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	}
 
 	/*
-	 * Get info about table INVALID NOT NULL constraints.  This is skipped for a
-	 * data-only dump, as it is only needed for table schemas.
+	 * Get info about table INVALID NOT NULL constraints.  This is skipped for
+	 * a data-only dump, as it is only needed for table schemas.
 	 */
 	if (dopt->dumpSchema && invalidnotnulloids->len > 2)
 	{
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 25d65b357a4..a393d993330 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -361,7 +361,7 @@ typedef struct _tableInfo
 	char	   *attcompression; /* per-attribute compression method */
 	char	  **attfdwoptions;	/* per-attribute fdw options */
 	char	  **attmissingval;	/* per attribute missing value */
-	bool       *notnull_valid;  /* NOT NULL status */
+	bool	   *notnull_valid;	/* NOT NULL status */
 	char	  **notnull_constrs;	/* NOT NULL constraint names. If null,
 									 * there isn't one on this column. If
 									 * empty string, unnamed constraint
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index a16422b681b..341956f1cea 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -941,7 +941,7 @@ SELECT * FROM notnull_tbl1;
 -- Try to add primary key on table column marked as NOT VALID NOT NULL
 -- constraint. This should throw an error.
 ALTER TABLE notnull_tbl1 add primary key (a);
-ERROR:  column "a" of table "notnull_tbl1" is marked as NOT VALID NOT NULL constrint
+ERROR:  column "a" of table "notnull_tbl1" is marked as NOT VALID NOT NULL constraint
 -- INHERITS table having NOT VALID NOT NULL constraints.
 CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
 NOTICE:  merging column "a" with inherited definition
@@ -965,13 +965,13 @@ Not-null constraints:
     "nn" NOT NULL "a"
 
 DROP TABLE notnull_tbl1;
--- Test the different Not null constrint name for parent and child table
+-- Test the different Not null constraint name for parent and child table
 CREATE TABLE notnull_tbl1 (a int);
 ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent not null a not valid;
 CREATE TABLE notnull_chld (a int);
 ALTER TABLE notnull_chld ADD CONSTRAINT nn_child not null a not valid;
 ALTER TABLE notnull_chld INHERIT notnull_tbl1;
--- This statement should validate not null constrint for parent as well as
+-- This statement should validate not null constraint for parent as well as
 -- child.
 ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
 SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
@@ -984,7 +984,7 @@ in ('notnull_tbl1'::regclass, 'notnull_chld'::regclass);
 
 DROP TABLE notnull_tbl1 CASCADE;
 NOTICE:  drop cascades to table notnull_chld
---Create table with NOT NULL INVALID constrint, for pg_upgrade.
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
 CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
 INSERT INTO notnull_tbl1_upg VALUES (NULL, 1);
 INSERT INTO notnull_tbl1_upg VALUES (NULL, 2);
@@ -995,7 +995,7 @@ CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY RANGE (a);
 ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID;
 CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES FROM (0) TO (10);
 CREATE TABLE notnull_tbl1_2 PARTITION OF notnull_tbl1 FOR VALUES FROM (20) TO (30);
--- Parent table NOT NULL constraints will be market as validated false, where
+-- Parent table NOT NULL constraints will be marked as validated false, where
 -- for child table it will be true
 SELECT conrelid::regclass, conname, convalidated FROM pg_catalog.pg_constraint WHERE
 conrelid IN ('notnull_tbl1'::regclass, 'notnull_tbl1_1'::regclass, 'notnull_tbl1_2'::regclass);
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index b62d8c69ff4..6dd7e4c79c7 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -670,19 +670,19 @@ DROP TABLE notnull_tbl1_child;
 ALTER TABLE notnull_tbl1 validate constraint nn;
 \d+ notnull_tbl1
 DROP TABLE notnull_tbl1;
--- Test the different Not null constrint name for parent and child table
+-- Test the different Not null constraint name for parent and child table
 CREATE TABLE notnull_tbl1 (a int);
 ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent not null a not valid;
 CREATE TABLE notnull_chld (a int);
 ALTER TABLE notnull_chld ADD CONSTRAINT nn_child not null a not valid;
 ALTER TABLE notnull_chld INHERIT notnull_tbl1;
--- This statement should validate not null constrint for parent as well as
+-- This statement should validate not null constraint for parent as well as
 -- child.
 ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
 SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
 in ('notnull_tbl1'::regclass, 'notnull_chld'::regclass);
 DROP TABLE notnull_tbl1 CASCADE;
---Create table with NOT NULL INVALID constrint, for pg_upgrade.
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
 CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
 INSERT INTO notnull_tbl1_upg VALUES (NULL, 1);
 INSERT INTO notnull_tbl1_upg VALUES (NULL, 2);
@@ -694,7 +694,7 @@ CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY RANGE (a);
 ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID;
 CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES FROM (0) TO (10);
 CREATE TABLE notnull_tbl1_2 PARTITION OF notnull_tbl1 FOR VALUES FROM (20) TO (30);
--- Parent table NOT NULL constraints will be market as validated false, where
+-- Parent table NOT NULL constraints will be marked as validated false, where
 -- for child table it will be true
 SELECT conrelid::regclass, conname, convalidated FROM pg_catalog.pg_constraint WHERE
 conrelid IN ('notnull_tbl1'::regclass, 'notnull_tbl1_1'::regclass, 'notnull_tbl1_2'::regclass);
-- 
2.39.5

---

From: Ashutosh Bapat <[email-masked]>
Date: Fri, 21 Feb 2025 14:59:20 +0530

> I have not looked at the pg_dump code yet, so the changes included here

I ran pg_upgrade suite with the patches. 002_pg_upgrade fails with
following test log
not ok 14 - run of pg_upgrade for new instance
not ok 15 - pg_upgrade_output.d/ removed after pg_upgrade success
# === pg_upgrade logs found - appending to
/build/dev/testrun/pg_upgrade/002_pg_upgrade/log/regress_log_002_pg_upgrade
===
# === appending
/build/dev/testrun/pg_upgrade/002_pg_upgrade/data/t_002_pg_upgrade_new_node_data/pgdata/pg_upgrade_output.d/20250221T144705.724/log/pg_upgrade_dump_16384.log
===
# === appending
/build/dev/testrun/pg_upgrade/002_pg_upgrade/data/t_002_pg_upgrade_new_node_data/pgdata/pg_upgrade_output.d/20250221T144705.724/log/pg_upgrade_dump_1.log
===
# === appending
/build/dev/testrun/pg_upgrade/002_pg_upgrade/data/t_002_pg_upgrade_new_node_data/pgdata/pg_upgrade_output.d/20250221T144705.724/log/pg_upgrade_server.log
===
# === appending
/build/dev/testrun/pg_upgrade/002_pg_upgrade/data/t_002_pg_upgrade_new_node_data/pgdata/pg_upgrade_output.d/20250221T144705.724/log/pg_upgrade_utility.log
===
# === appending
/build/dev/testrun/pg_upgrade/002_pg_upgrade/data/t_002_pg_upgrade_new_node_data/pgdata/pg_upgrade_output.d/20250221T144705.724/log/pg_upgrade_dump_16387.log
===
# === appending
/build/dev/testrun/pg_upgrade/002_pg_upgrade/data/t_002_pg_upgrade_new_node_data/pgdata/pg_upgrade_output.d/20250221T144705.724/log/pg_upgrade_dump_16385.log
===
# === appending
/build/dev/testrun/pg_upgrade/002_pg_upgrade/data/t_002_pg_upgrade_new_node_data/pgdata/pg_upgrade_output.d/20250221T144705.724/log/pg_upgrade_dump_16386.log
===
# === appending
/build/dev/testrun/pg_upgrade/002_pg_upgrade/data/t_002_pg_upgrade_new_node_data/pgdata/pg_upgrade_output.d/20250221T144705.724/log/pg_upgrade_internal.log
===
# === appending
/build/dev/testrun/pg_upgrade/002_pg_upgrade/data/t_002_pg_upgrade_new_node_data/pgdata/pg_upgrade_output.d/20250221T144705.724/log/pg_upgrade_dump_5.log
===
not ok 16 - check that locales in new cluster match original cluster
ok 17 - dump after running pg_upgrade
not ok 18 - old and new dumps match after pg_upgrade
1..18
# test failed
stderr:
#   Failed test 'run of pg_upgrade for new instance'
#   at /pg/src/bin/pg_upgrade/t/002_pg_upgrade.pl line 478.
#   Failed test 'pg_upgrade_output.d/ removed after pg_upgrade success'
#   at /pg/src/bin/pg_upgrade/t/002_pg_upgrade.pl line 487.
#   Failed test 'check that locales in new cluster match original cluster'
#   at /pg/src/bin/pg_upgrade/t/002_pg_upgrade.pl line 522.
#          got: '0|c|en_US.UTF-8|en_US.UTF-8|'
#     expected: '6|b|C|C|C.UTF-8'
#   Failed test 'old and new dumps match after pg_upgrade'
#   at /pg/src/test/perl/PostgreSQL/Test/Utils.pm line 797.
#          got: '1'
#     expected: '0'
# Looks like you failed 4 tests of 18.

(test program exited with status code 4)
------------------------------------------------------------------------------

-- 
Best Wishes,
Ashutosh Bapat

---

From: Rushabh Lathia <[email-masked]>
Date: Fri, 21 Feb 2025 15:36:52 +0530

> I ran pg_upgrade suite with the patches. 002_pg_upgrade fails with

This is strange because when I run the tests it's all passing for me.

# +++ tap check in src/bin/pg_upgrade +++
t/001_basic.pl .......... ok
t/002_pg_upgrade.pl ..... ok
t/003_logical_slots.pl .. ok
t/004_subscription.pl ... ok
All tests successful.
Files=4, Tests=53, 38 wallclock secs ( 0.01 usr  0.00 sys +  2.88 cusr
 2.11 csys =  5.00 CPU)
Result: PASS

Note: I applied the patch on
commit 7202d72787d3b93b692feae62ee963238580c877.

-- 
Rushabh Lathia

---

From: Ashutosh Bapat <[email-masked]>
Date: Fri, 21 Feb 2025 16:11:59 +0530

On Fri, Feb 21, 2025 at 3:37 PM Rushabh Lathia <[email-masked]> wrote:
>
>
>
> On Fri, Feb 21, 2025 at 2:59 PM Ashutosh Bapat <[email-masked]> wrote:
>>
> Files=4, Tests=53, 38 wallclock secs ( 0.01 usr  0.00 sys +  2.88 cusr  2.11 csys =  5.00 CPU)
> Result: PASS
>
> Note: I applied the patch on commit 7202d72787d3b93b692feae62ee963238580c877.

I applied patches on 984410b923263cac901fa81e0efbe523e9c36df3 and I am
using meson.

If I apply your patches, build binaries, I see failure. I reverted
your patches, built binaries, I don't see failure. I apply your
patches again, built binaries, it fails again.

--
Best Wishes,
Ashutosh Bapat

---

From: Rushabh Lathia <[email-masked]>
Date: Thu, 27 Feb 2025 15:25:51 +0530

Thank Alvaro for the fixup patch.

> pass two booleans instead?  Please have a look at whether that can be

I haven't given much thought to improving the API, but I'll look into it
now. Also,
I'm considering renaming AdjustNotNullInheritance() since it now also
checks for invalid NOT NULL constraints. What do you think?

> catch invalid constraints.
I reviewed the added WARNING in the fixup patch and addressed the case in
AdjustNotNullInheritance() and ATExecSetNotNull(). I also added the related
test case to the test suite.

Regarding the WARNING in MergeAttributeIntoExisting(), it won’t be
triggered
since this block executes only when the child table has
ATTRIBUTE_NOTNULL_FALSE.
Let me know if I’m missing anything.

>       syntax as <link linkend="sql-createtable"><command>CREATE
> TABLE</command></link>, plus the option <literal>NOT
Done changes, as per the suggestions.

Regards,
Rushabh Lathia
www.EnterpriseDB.com

---

From: Rushabh Lathia <[email-masked]>
Date: Mon, 10 Mar 2025 16:56:14 +0530

> checks for invalid NOT NULL constraints. What do you think?

I adjusted the set_attnotnull() API and removed the added queue_validation
parameter.  Rather, the function start using wqueue input parameter as a
check.
If wqueue is NULL, skip the queue_validation.  Attaching patch here, but not
sure how clear it is, in term of understanding the API.  Your
thoughts/inputs?

Looking further for AdjustNotNullInheritance() I don't see need of rename
this
API as it's just adding new error check now for an existing NOT NULL
constraint.

JFYI, I can reproduce the failure Ashutosh Bapat reported, while running
the pg_upgrade test through meson commands.  I am investigating that
further.

Thanks,
Rushabh Lathia

---

From: Alvaro Herrera <[email-masked]>
Date: Mon, 10 Mar 2025 12:50:23 +0100

On 2025-Mar-10, Rushabh Lathia wrote:

> I adjusted the set_attnotnull() API and removed the added
> queue_validation parameter.  Rather, the function start using wqueue
> input parameter as a check.
> If wqueue is NULL, skip the queue_validation.  Attaching patch here,
> but not sure how clear it is, in term of understanding the API.  Your
> thoughts/inputs?

Yeah, I think this makes sense, if it supports all the cases correctly.
(If in your code you have any calls of set_attnotnull that pass a NULL
wqueue during ALTER TABLE in order to avoid queueing a check, you had
better have comments to explain this.)

Kindly do not send patches standalone, because the CFbot doesn't
understand that it needs to apply it on top of the three previous
patches.  You need to send all 4 patches every time.  You can see the
result here:
https://commitfest.postgresql.org/patch/5554/
If you click on the "need rebase!" label you'll see that it tried to
apply patch 0004 to the top of the master branch, and that obviously
failed.  (FWIW if you click on the "summary" label you'll also see that
the patch has been failing the CI tests since Feb 27.)

> Looking further for AdjustNotNullInheritance() I don't see need of
> rename this API as it's just adding new error check now for an
> existing NOT NULL constraint.

OK.

> JFYI, I can reproduce the failure Ashutosh Bapat reported, while
> running the pg_upgrade test through meson commands.  I am
> investigating that further.

Ah good, thanks.

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/
"Industry suffers from the managerial dogma that for the sake of stability
and continuity, the company should be independent of the competence of
individual employees."                                      (E. Dijkstra)

---

From: Rushabh Lathia <[email-masked]>
Date: Tue, 11 Mar 2025 15:45:46 +0530

> better have comments to explain this.)

Done.

> the patch has been failing the CI tests since Feb 27.)

Sure, attaching the rebased patch here.

> > investigating that further.

Somehow, I am now not able to reproduce after the clean build.  Yesterday
I was able to reproduce, so I was happy, but again trying to analyze the
issue
when I start with the

Thanks,
Rushabh Lathia
www.EnterpriseDB.com

---

From: Ashutosh Bapat <[email-masked]>
Date: Wed, 12 Mar 2025 13:57:35 +0530

On Tue, Mar 11, 2025 at 3:46 PM Rushabh Lathia <[email-masked]> wrote:

> I was able to reproduce, so I was happy, but again trying to analyze the issue
> when I start with the

If the test passes for you, can you please try the patches at [1] on
top of your patches? Please apply those, set and export environment
variable PG_TEST_EXTRA=regress_dump_test, and run 002_pg_upgrade test?
I intended to do this but can not do it since the test always fails
with your patches applied.

[1] https://www.postgresql.org/message-id/CAExHW5uQoyOddBKLBBJpfxXqqok%3DBTeMvt5OpnM6gw0SroiUUw%40mail.gmail.com

-- 
Best Wishes,
Ashutosh Bapat

---

From: Alvaro Herrera <[email-masked]>
Date: Wed, 12 Mar 2025 10:50:13 +0100

> with your patches applied.

Oh, I need to enable a PG_TEST_EXTRA option in order for the test to
run?  FFS.  That explains why the tests passed just fine for me.
I'll re-run.

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/
"La conclusión que podemos sacar de esos estudios es que
no podemos sacar ninguna conclusión de ellos" (Tanenbaum)

---

From: Rushabh Lathia <[email-masked]>
Date: Wed, 12 Mar 2025 15:34:12 +0530

> run?  FFS.  That explains why the tests passed just fine for me.

I can reproduce the issue and will be sending the patch soon.

Thanks Alvaro, Ashutosh.

Rushabh Lathia

---

From: Ashutosh Bapat <[email-masked]>
Date: Wed, 12 Mar 2025 15:38:21 +0530

> run?  FFS.  That explains why the tests passed just fine for me.

You need PG_TEST_EXTRA to run the testcase I am adding there. Rest of
the testcases run without PG_TEST_EXTRA.

-- 
Best Wishes,
Ashutosh Bapat

---

From: Rushabh Lathia <[email-masked]>
Date: Wed, 12 Mar 2025 15:52:38 +0530

Hi Alvaro,

Here are the latest patches, which includes the regression fix.

Thanks,

-- 
Rushabh Lathia

---

From: Ashutosh Bapat <[email-masked]>
Date: Wed, 12 Mar 2025 17:21:04 +0530

> Here are the latest patches, which includes the regression fix.

The 002_pg_upgrade test passes with and without my patches now. But
then the tests added here do not leave behind any parent-child table.
Previously we have found problems in dumping and restoring constraints
in an inheritance hierarchy. I think the test should leave behind all
the combinations of parent and child NOT NULL constraints so that
0002_pg_upgrade can test those.

We should add more scenarios for constraint inheritance. E.g.
#CREATE TABLE notnull_tbl1 (a int);
#ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent not null a not valid;
#CREATE TABLE notnull_chld (a int);
#ALTER TABLE notnull_chld ADD CONSTRAINT nn_child not null a;
#ALTER TABLE notnull_chld INHERIT notnull_tbl1;
#SELECT conname, convalidated FROM pg_catalog.pg_constraint WHERE conrelid
in ('notnull_tbl1'::regclass, 'notnull_chld'::regclass);
  conname  | convalidated
-----------+--------------
 nn_parent | f
 nn_child  | t
(2 rows)

Is it expected that a child may have VALID constraint but parent has
not valid constraint?

Same case with partitioned table. We should leave partitioned table
hierarchy behind for 002_pg_upgrade to test. And we need tests to test
scenarios where a partitioned table has valid constraint but we try to
change constraint on a partition to not valid and vice versa. I think
we shouldn't allow such assymetry in partitioned table hierarchy and
having a test would be better.

-- 
Best Wishes,
Ashutosh Bapat

---

From: Alvaro Herrera <[email-masked]>
Date: Wed, 12 Mar 2025 12:57:57 +0100

> 0002_pg_upgrade can test those.

I agree.

> Is it expected that a child may have VALID constraint but parent has

Sure.  Consider: if the parent has an unvalidated constraint, we cannot
make any assertions about the state of its children.  The children may
have additional constraints of their own -- in this case, a child can
have a validated constraint even though the parent has none, or only an
unvalidatec constraint.

But the opposite is not allowed: if you know something to be true about
a parent table (to wit: that no row in it is NULL), then this must
necessarily apply to its children as well.  Therefore, if there's a
valid constraint in the parent, then all children must have the same
constraint, and all such constraints must be known valid.

> having a test would be better.

Agreed.

---

From: Alvaro Herrera <[email-masked]>
Date: Wed, 12 Mar 2025 19:20:13 +0100

> Here are the latest patches, which includes the regression fix.

Thank you.

Taking a step back after discussing this with some colleagues, I need to
contradict what I said at the start of this thread.  There's a worry
that changing pg_attribute.attnotnull in the way I initially suggested
might not be such a great idea after all.  I did a quick search using
codesearch.debian.net for applications reading that column and thinking
about how they would react to this change; I think in the end it's going
to be quite disastrous.  We would break a vast number of these apps, and
there are probably countless other apps and frameworks that we would
also break.  Everybody would hate us forever.  Upgrading to Postgres 18
would become as bad an experience as the drastic change of implicit
casts to text in 8.3.  Nothing else in the intervening 17 years strikes
me as so problematic as this change would be.

So I think we may need to go back and somehow leave pg_attribute alone,
to avoid breaking the whole world.  Maybe it would be sufficient to
change the CompactAttribute->attnotnull to no longer be a boolean, but
the new char representation instead.  I'm not sure if this would
actually work.

---

From: Rushabh Lathia <[email-masked]>
Date: Thu, 13 Mar 2025 14:07:47 +0530

> the new char representation instead.  I'm not sure if this would

Thank you for your feedback. I understand that this change could be
problematic

for existing applications, as attnotnull is a highly generic catalog
column. I will

revisit the patch, make the necessary adjustments, and submit a revised
version

accordingly.

I appreciate your insights and will try to submit the new patch.

Regards,
Rushabh Lathia
www.EnterpriseDB.com

---

From: jian he <[email-masked]>
Date: Mon, 17 Mar 2025 19:14:52 +0800

hi.
I played around with it.

current syntax, we don't need to deal with column constraint grammar.
like the following can fail directly:
create table t0(a int constraint nn not null a not valid);
we only support table constraint cases like:
alter table lp add constraint nc1 not null a not valid;

since CREATE TABLE with invalid constraint does not make sense, so
we can just issue a warning. like:
create table t0(a int, constraint nn not null a not valid);
WARNING:  CREATE TABLE NOT NULL NOT VALID CONSTRAINT WILL SET TO VALID
the wording needs to change...

for not null not valid syntax, we only need to support:
ALTER TABLE ADD CONSTRAINT conname NOT NULL column_name NOT VALID
ALTER TABLE ADD NOT NULL column_name NOT VALID
ALTER TABLE VALIDATE CONSTRAINT conname

The attached is what I came up with:
--------------------------------------------------------------------
ALTER TABLE ADD CONSTRAINT conname NOT NULL column_name NOT VALID
will create an invalidated check constraint. like
ALTER TABLE ADD CONSTRAINT conname CHECK (column_name IS NOT NULL) NOT VALID

when you validate the not-null constraint (internally it's a check constraint)
it will drop the check constraint and install a not-null constraint
with the same name.

drop a check constraint, it will call RemoveConstraintById.
within RemoveConstraintById it will lock pg_constraint.conrelid in
AccessExclusiveLock mode,
which is not ideal, because
ALTER TABLE VALIDATE CONSTRAINT only needs ShareUpdateExclusiveLock.
so we have to find a way to release that AccessExclusiveLock.

because we have converted a not-null constraint to a check constraint,
we need to somehow distinguish this case,
so pg_constraint adds another column: coninternaltype.
(the naming is not good, i guess)
because we dropped a invalid check constraint, but the inherited
constraint cannot be dropped.
so this ALTER TABLE VALIDATE CONSTRAINT will not work for partitions,
but it will work for the root partitioned table. (same logic for table
inheritance).
----------------------------------
demo:

create table t(a int);
alter table t add constraint nc1 not null a not valid;
\d t
                 Table "public.t"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 a      | integer |           |          |
Check constraints:
    "nc1" CHECK (a IS NOT NULL) NOT VALID

insert into t default values;
ERROR:  new row for relation "t" violates check constraint "nc1"
DETAIL:  Failing row contains (null).

alter table t validate constraint nc1;
\d+ t
                                            Table "public.t"
 Column |  Type   | Collation | Nullable | Default | Storage |
Compression | Stats target | Description
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
 a      | integer |           | not null |         | plain   |
    |              |
Not-null constraints:
    "nc1" NOT NULL "a"
Access method: heap

-------------------------------------------------------------------
some regress tests added.
need more polishing, but overall it works as the above described.

not sure if this idea is crazy or not,
what do you think?
From 5a66a8da2a8c5fef88d7730e127900e5b83e0cde Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Sun, 16 Mar 2025 20:50:53 +0800
Subject: [PATCH v4 1/1] not null not valid implementation

---
 src/backend/catalog/heap.c                |  14 +-
 src/backend/catalog/index.c               |   1 +
 src/backend/catalog/pg_constraint.c       |   3 +
 src/backend/commands/tablecmds.c          | 168 +++++++++++++++++++++-
 src/backend/commands/trigger.c            |   1 +
 src/backend/commands/typecmds.c           |   2 +
 src/backend/parser/gram.y                 |   5 +-
 src/backend/parser/parse_utilcmd.c        |  63 +++++++-
 src/include/catalog/heap.h                |   1 +
 src/include/catalog/pg_constraint.h       |   3 +
 src/include/nodes/parsenodes.h            |   1 +
 src/test/regress/expected/constraints.out |  63 ++++++++
 src/test/regress/sql/constraints.sql      |  41 ++++++
 13 files changed, 355 insertions(+), 11 deletions(-)

diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..6dc0267bd5a 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -104,7 +104,7 @@ static ObjectAddress AddNewRelationType(const char *typeName,
 static void RelationRemoveInheritance(Oid relid);
 static Oid	StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 						  bool is_enforced, bool is_validated, bool is_local,
-						  int16 inhcount, bool is_no_inherit, bool is_internal);
+						  int16 inhcount, bool is_no_inherit, char coninternaltype, bool is_internal);
 static void StoreConstraints(Relation rel, List *cooked_constraints,
 							 bool is_internal);
 static bool MergeWithExistingConstraint(Relation rel, const char *ccname, Node *expr,
@@ -2136,7 +2136,7 @@ SetAttrMissing(Oid relid, char *attname, char *value)
 static Oid
 StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 			  bool is_enforced, bool is_validated, bool is_local,
-			  int16 inhcount, bool is_no_inherit, bool is_internal)
+			  int16 inhcount, bool is_no_inherit, char coninternaltype, bool is_internal)
 {
 	char	   *ccbin;
 	List	   *varList;
@@ -2228,6 +2228,7 @@ StoreRelCheck(Relation rel, const char *ccname, Node *expr,
 							  inhcount, /* coninhcount */
 							  is_no_inherit,	/* connoinherit */
 							  false,	/* conperiod */
+							  coninternaltype,	/* coninternaltype */
 							  is_internal); /* internally constructed? */
 
 	pfree(ccbin);
@@ -2282,6 +2283,7 @@ StoreRelNotNull(Relation rel, const char *nnname, AttrNumber attnum,
 							  inhcount,
 							  is_no_inherit,
 							  false,
+							  '\0',		/* coninternaltype */
 							  false);
 	return constrOid;
 }
@@ -2327,7 +2329,9 @@ StoreConstraints(Relation rel, List *cooked_constraints, bool is_internal)
 					StoreRelCheck(rel, con->name, con->expr,
 								  con->is_enforced, !con->skip_validation,
 								  con->is_local, con->inhcount,
-								  con->is_no_inherit, is_internal);
+								  con->is_no_inherit,
+								  con->coninternaltype,
+								  is_internal);
 				numchecks++;
 				break;
 
@@ -2460,6 +2464,7 @@ AddRelationNewConstraints(Relation rel,
 		cooked->is_local = is_local;
 		cooked->inhcount = is_local ? 0 : 1;
 		cooked->is_no_inherit = false;
+		cooked->coninternaltype = '\0';
 		cookedConstraints = lappend(cookedConstraints, cooked);
 	}
 
@@ -2579,6 +2584,7 @@ AddRelationNewConstraints(Relation rel,
 				StoreRelCheck(rel, ccname, expr, cdef->is_enforced,
 							  cdef->initially_valid, is_local,
 							  is_local ? 0 : 1, cdef->is_no_inherit,
+							  cdef->contype_internal,
 							  is_internal);
 
 			numchecks++;
@@ -2594,6 +2600,7 @@ AddRelationNewConstraints(Relation rel,
 			cooked->is_local = is_local;
 			cooked->inhcount = is_local ? 0 : 1;
 			cooked->is_no_inherit = cdef->is_no_inherit;
+			cooked->coninternaltype = cdef->contype_internal;
 			cookedConstraints = lappend(cookedConstraints, cooked);
 		}
 		else if (cdef->contype == CONSTR_NOTNULL)
@@ -2670,6 +2677,7 @@ AddRelationNewConstraints(Relation rel,
 			nncooked->is_local = is_local;
 			nncooked->inhcount = inhcount;
 			nncooked->is_no_inherit = cdef->is_no_inherit;
+			nncooked->coninternaltype = '\0';
 
 			cookedConstraints = lappend(cookedConstraints, nncooked);
 		}
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 739a92bdcc1..e5bc679a850 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -1986,6 +1986,7 @@ index_constraint_create(Relation heapRelation,
 								   inhcount,
 								   noinherit,
 								   is_without_overlaps,
+								   '\0',	/* coninternaltype */
 								   is_internal);
 
 	/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..3f3a276e3aa 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -80,6 +80,7 @@ CreateConstraintEntry(const char *constraintName,
 					  int16 conInhCount,
 					  bool conNoInherit,
 					  bool conPeriod,
+					  char coninternaltype,
 					  bool is_internal)
 {
 	Relation	conDesc;
@@ -202,6 +203,7 @@ CreateConstraintEntry(const char *constraintName,
 	values[Anum_pg_constraint_coninhcount - 1] = Int16GetDatum(conInhCount);
 	values[Anum_pg_constraint_connoinherit - 1] = BoolGetDatum(conNoInherit);
 	values[Anum_pg_constraint_conperiod - 1] = BoolGetDatum(conPeriod);
+	values[Anum_pg_constraint_coninternaltype - 1] = CharGetDatum(coninternaltype);
 
 	if (conkeyArray)
 		values[Anum_pg_constraint_conkey - 1] = PointerGetDatum(conkeyArray);
@@ -834,6 +836,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
 			cooked->is_local = true;
 			cooked->inhcount = 0;
 			cooked->is_no_inherit = conForm->connoinherit;
+			cooked->coninternaltype = '\0';
 
 			notnulls = lappend(notnulls, cooked);
 		}
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 129c97fdf28..b3df5b913a5 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -604,6 +604,9 @@ static void GetForeignKeyCheckTriggers(Relation trigrel,
 static void ATExecDropConstraint(Relation rel, const char *constrName,
 								 DropBehavior behavior, bool recurse,
 								 bool missing_ok, LOCKMODE lockmode);
+static void DropConstraintByOid(Relation rel, Oid constroid,
+								DropBehavior behavior, bool recurse,
+								bool missing_ok, LOCKMODE lockmode);
 static ObjectAddress dropconstraint_internal(Relation rel,
 											 HeapTuple constraintTup, DropBehavior behavior,
 											 bool recurse, bool recursing,
@@ -991,6 +994,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 			cooked->is_local = true;	/* not used for defaults */
 			cooked->inhcount = 0;	/* ditto */
 			cooked->is_no_inherit = false;
+			cooked->coninternaltype = '\0';
 			cookedDefaults = lappend(cookedDefaults, cooked);
 		}
 	}
@@ -10581,6 +10585,7 @@ addFkConstraint(addFkConstraintSides fkside,
 									  coninhcount,	/* inhcount */
 									  connoinherit, /* conNoInherit */
 									  with_period,	/* conPeriod */
+									  '\0',			/* coninternaltype */
 									  is_internal); /* is_internal */
 
 	ObjectAddressSet(address, ConstraintRelationId, constrOid);
@@ -12337,6 +12342,9 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 	HeapTuple	tuple;
 	Form_pg_constraint con;
 	ObjectAddress address;
+	Oid				constroid = InvalidOid;
+	AttrNumber		attnum	= -1;
+	char			*conname = NULL;
 
 	conrel = table_open(ConstraintRelationId, RowExclusiveLock);
 
@@ -12367,7 +12375,8 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 
 	con = (Form_pg_constraint) GETSTRUCT(tuple);
 	if (con->contype != CONSTRAINT_FOREIGN &&
-		con->contype != CONSTRAINT_CHECK)
+		con->contype != CONSTRAINT_CHECK &&
+		con->contype != CONSTRAINT_NOTNULL)
 		ereport(ERROR,
 				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
 				 errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint",
@@ -12383,14 +12392,30 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 		if (con->contype == CONSTRAINT_FOREIGN)
 		{
 			QueueFKConstraintValidation(wqueue, conrel, rel, tuple, lockmode);
+			ObjectAddressSet(address, ConstraintRelationId, con->oid);
 		}
-		else if (con->contype == CONSTRAINT_CHECK)
+		else if (con->contype == CONSTRAINT_CHECK && con->coninternaltype == '\0')
 		{
 			QueueCheckConstraintValidation(wqueue, conrel, rel, constrName,
 										   tuple, recurse, recursing, lockmode);
+			ObjectAddressSet(address, ConstraintRelationId, con->oid);
+		}
+		else if (con->contype == CONSTRAINT_CHECK && con->coninternaltype == CONSTRAINT_NOTNULL)
+		{
+			Datum		adatum;
+			ArrayType  *arr;
+			adatum = SysCacheGetAttrNotNull(CONSTROID, tuple,
+											Anum_pg_constraint_conkey);
+			arr = DatumGetArrayTypeP(adatum);	/* ensure not toasted */
+			if (ARR_NDIM(arr) != 1 ||
+				ARR_DIMS(arr)[0] != 1 ||
+				ARR_HASNULL(arr) ||
+				ARR_ELEMTYPE(arr) != INT2OID)
+				elog(ERROR, "confkey is not a 1-D smallint array");
+			memcpy(&attnum, ARR_DATA_PTR(arr), 1 * sizeof(int16));
+			constroid = con->oid;
+			conname =  pstrdup(NameStr(con->conname));
 		}
-
-		ObjectAddressSet(address, ConstraintRelationId, con->oid);
 	}
 	else
 		address = InvalidObjectAddress; /* already validated */
@@ -12399,6 +12424,80 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 
 	table_close(conrel, RowExclusiveLock);
 
+	if (OidIsValid(constroid))
+	{
+		AlteredTableInfo *tab;
+		Constraint *n;
+		String	   *val;
+		List	   *children;
+
+		if (rel->rd_rel->relispartition)
+		{
+			char	   *relname;
+			List *ancestors = NIL;
+			Oid	rootrelid = InvalidOid;
+
+			ancestors = get_partition_ancestors(RelationGetRelid(rel));
+			Assert(ancestors != NIL);
+
+			rootrelid = llast_oid(ancestors);
+			list_free(ancestors);
+
+			relname = get_rel_name(rootrelid);
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot validate not-null constraint from partition \"%s\"",
+							RelationGetRelationName(rel)),
+					errhint("You might need validate not-null constraint through root partition \"%s\"", relname));
+		}
+		else if (has_superclass(RelationGetRelid(rel)))
+		{
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot validate not-null constraint on inheritance children \"%s\"",
+							RelationGetRelationName(rel)));
+		}
+
+		children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+		DropConstraintByOid(rel, constroid,
+							DROP_CASCADE, true,
+							false, lockmode);
+
+		tab = ATGetQueueEntry(wqueue, rel);
+		n = makeNode(Constraint);
+		n->contype = CONSTR_NOTNULL;
+		n->conname = conname;
+		n->location = -1;
+		n->is_no_inherit = false;
+		n->raw_expr = NULL;
+		n->is_enforced = true;
+		n->initially_valid = true;
+		n->skip_validation = false;
+		val = makeString(get_attname(RelationGetRelid(rel), attnum,
+									 false));
+		n->keys = list_make1(val);
+		ATAddCheckNNConstraint(wqueue, tab, rel, n,
+							   recurse, recursing, false, lockmode);
+
+		/*
+		 * DropConstraintByOid will call RemoveConstraintById.
+		 * Inside RemoveConstraintById, the relation to which this constraint belongs
+		 * will be locked in AccessExclusiveLock mode.
+		 *
+		 * ATAddCheckNNConstraint will install not-null constraint for rel and it's children.
+		 * ALTER TABLE VALIDATE CONSTRAINT generally take ShareUpdateExclusiveLock lock.
+		 * removing the logical equivalent CHECK constraint will not impact the data integrity.
+		 * thus here release the AccessExclusiveLock on rel and its children.
+		*/
+		foreach_oid(childrelid, children)
+			UnlockRelationOid(childrelid, AccessExclusiveLock);
+
+		if (CheckRelationLockedByMe(rel, AccessExclusiveLock, true))
+			elog(ERROR, "relation \"%s\" was lock in AccessExclusiveLock", RelationGetRelationName(rel));
+
+		address = InvalidObjectAddress;
+	}
+
 	return address;
 }
 
@@ -13298,6 +13397,67 @@ createForeignKeyCheckTriggers(Oid myRelOid, Oid refRelOid,
 										  parentUpdTrigger, false);
 }
 
+
+
+/*
+ * DROP CONSTRAINT by Oid
+ *
+ * Like DROP COLUMN, we can't use the normal ALTER TABLE recursion mechanism.
+ * The constroid is the OID of the constraint to be dropped. If the constraint
+ * has any inherited child constraints, it will recursively drop them too.
+ */
+static void
+DropConstraintByOid(Relation rel, Oid constroid,
+					DropBehavior behavior, bool recurse,
+					bool missing_ok, LOCKMODE lockmode)
+{
+	Relation	conrel;
+	SysScanDesc scan;
+	ScanKeyData skey[1];
+	HeapTuple	tuple;
+	bool		found = false;
+	char	   *constrName = NULL;
+	Form_pg_constraint con;
+
+	conrel = table_open(ConstraintRelationId, RowExclusiveLock);
+	/*
+	 * Find and drop the target constraint
+	 */
+	ScanKeyInit(&skey[0],
+				Anum_pg_constraint_oid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(constroid));
+	scan = systable_beginscan(conrel, ConstraintOidIndexId,
+							  true, NULL, 1, skey);
+
+	/* There can be at most one matching row */
+	if (HeapTupleIsValid(tuple = systable_getnext(scan)))
+	{
+		con = (Form_pg_constraint) GETSTRUCT(tuple);
+		constrName = pstrdup(NameStr(con->conname));
+		dropconstraint_internal(rel, tuple, behavior, recurse, false,
+								missing_ok, lockmode);
+		found = true;
+	}
+
+	systable_endscan(scan);
+
+	if (!found)
+	{
+		if (!missing_ok)
+			ereport(ERROR,
+					errcode(ERRCODE_UNDEFINED_OBJECT),
+					errmsg("constraint \"%s\" of relation \"%s\" does not exist",
+						   constrName, RelationGetRelationName(rel)));
+		else
+			ereport(NOTICE,
+					errmsg("constraint \"%s\" of relation \"%s\" does not exist, skipping",
+						   constrName, RelationGetRelationName(rel)));
+	}
+
+	table_close(conrel, RowExclusiveLock);
+}
+
 /*
  * ALTER TABLE DROP CONSTRAINT
  *
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index c9f61130c69..e99b91d3840 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -839,6 +839,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 											  0,	/* inhcount */
 											  true, /* noinherit */
 											  false,	/* conperiod */
+											  '\0',		/* coninternaltype */
 											  isInternal);	/* is_internal */
 	}
 
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 3cb3ca1cca1..457d47b7ba4 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3606,6 +3606,7 @@ domainAddCheckConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
 							  0,	/* inhcount */
 							  false,	/* connoinherit */
 							  false,	/* conperiod */
+							  '\0',		/* coninternaltype */
 							  false);	/* is_internal */
 	if (constrAddr)
 		ObjectAddressSet(*constrAddr, ConstraintRelationId, ccoid);
@@ -3714,6 +3715,7 @@ domainAddNotNullConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
 							  0,	/* inhcount */
 							  false,	/* connoinherit */
 							  false,	/* conperiod */
+							  '\0',		/* coninternaltype */
 							  false);	/* is_internal */
 
 	if (constrAddr)
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..0af53b6fbf3 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4222,11 +4222,10 @@ ConstraintElem:
 					n->contype = CONSTR_NOTNULL;
 					n->location = @1;
 					n->keys = list_make1(makeString($3));
-					/* no NOT VALID support yet */
 					processCASbits($4, @4, "NOT NULL",
-								   NULL, NULL, NULL, NULL,
+								   NULL, NULL, NULL, &n->skip_validation,
 								   &n->is_no_inherit, yyscanner);
-					n->initially_valid = true;
+					n->initially_valid = !n->skip_validation;
 					$$ = (Node *) n;
 				}
 			| UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index abbe1bb45a3..d5fb9b70c9b 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1090,7 +1090,68 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
 						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						errmsg("not-null constraints on partitioned tables cannot be NO INHERIT"));
 
-			cxt->nnconstraints = lappend(cxt->nnconstraints, constraint);
+			if (constraint->skip_validation && (strcmp(cxt->stmtType, "ALTER TABLE") == 0))
+			{
+				Constraint *constr;
+				NullTest   *nnulltest;
+				AttrNumber	colnum;
+				TupleDesc	tupdesc;
+				Form_pg_attribute attr;
+
+				constr = makeNode(Constraint);
+				constr->contype = CONSTR_CHECK;
+				if (constraint->conname)
+					constr->conname = pstrdup(constraint->conname);
+				constr->deferrable = false;
+				constr->initdeferred = false;
+				constr->is_enforced = true;
+				constr->skip_validation = true;
+				constr->initially_valid = false;
+				constr->is_no_inherit = false;
+				constr->nulls_not_distinct = false;
+				constr->location = constraint->location;
+				constr->contype_internal = CONSTRAINT_NOTNULL;
+
+				colnum = get_attnum(RelationGetRelid(cxt->rel), strVal(linitial(constraint->keys)));
+				if (colnum == InvalidAttrNumber)
+					ereport(ERROR,
+							errcode(ERRCODE_UNDEFINED_COLUMN),
+							errmsg("column \"%s\" of relation \"%s\" does not exist",
+								   strVal(linitial(constraint->keys)), RelationGetRelationName(cxt->rel)));
+				if (colnum < InvalidAttrNumber)
+					ereport(ERROR,
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("cannot add not-null constraint on system column \"%s\"",
+									strVal(linitial(constraint->keys))));
+
+				tupdesc = RelationGetDescr(cxt->rel);
+				attr = TupleDescAttr(tupdesc, colnum - 1);
+
+				nnulltest = makeNode(NullTest);
+				nnulltest->arg = (Expr *) makeVar(1,
+												  attr->attnum,
+												  attr->atttypid,
+												  attr->atttypmod,
+												  attr->attcollation,
+												  0);
+
+				nnulltest->nulltesttype = IS_NOT_NULL;
+				nnulltest->argisrow = false;
+				nnulltest->location = constraint->location;
+				constr->cooked_expr = nodeToString(nnulltest);
+
+				cxt->ckconstraints = lappend(cxt->ckconstraints, constr);
+			}
+			else if (constraint->skip_validation && (strcmp(cxt->stmtType, "CREATE TABLE") == 0))
+			{
+				constraint->skip_validation = false;
+				constraint->initially_valid = true;
+				ereport(WARNING,
+						errmsg("CREATE TABLE NOT NULL NOT VALID CONSTRAINT WILL SET TO VALID"));
+				cxt->nnconstraints = lappend(cxt->nnconstraints, constraint);
+			}
+			else
+				cxt->nnconstraints = lappend(cxt->nnconstraints, constraint);
 			break;
 
 		case CONSTR_FOREIGN:
diff --git a/src/include/catalog/heap.h b/src/include/catalog/heap.h
index dbd339e9df4..70b61b12464 100644
--- a/src/include/catalog/heap.h
+++ b/src/include/catalog/heap.h
@@ -42,6 +42,7 @@ typedef struct CookedConstraint
 	Node	   *expr;			/* transformed default or check expr */
 	bool		is_enforced;	/* is enforced? (only for CHECK) */
 	bool		skip_validation;	/* skip validation? (only for CHECK) */
+	char		coninternaltype;	/* (only for NOT NULL) */
 	bool		is_local;		/* constraint has local (non-inherited) def */
 	int16		inhcount;		/* number of times constraint is inherited */
 	bool		is_no_inherit;	/* constraint has local def and cannot be
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 6da164e7e4d..5740a277d8f 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -114,6 +114,8 @@ CATALOG(pg_constraint,2606,ConstraintRelationId)
 	 */
 	bool		conperiod;
 
+	char		coninternaltype; /* constraint internal type; see codes below */
+
 #ifdef CATALOG_VARLEN			/* variable-length fields start here */
 
 	/*
@@ -250,6 +252,7 @@ extern Oid	CreateConstraintEntry(const char *constraintName,
 								  int16 conInhCount,
 								  bool conNoInherit,
 								  bool conPeriod,
+								  char coninternaltype,
 								  bool is_internal);
 
 extern bool ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 23c9e3c5abf..01a72589084 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2824,6 +2824,7 @@ typedef struct Constraint
 								 * untransformed parse tree */
 	char	   *cooked_expr;	/* CHECK or DEFAULT expression, as
 								 * nodeToString representation */
+	char		contype_internal; /**/
 	char		generated_when; /* ALWAYS or BY DEFAULT */
 	char		generated_kind; /* STORED or VIRTUAL */
 	bool		nulls_not_distinct; /* null treatment for UNIQUE constraints */
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..cdba544bb8e 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -1392,3 +1392,66 @@ DROP TABLE constraint_comments_tbl;
 DROP DOMAIN constraint_comments_dom;
 DROP ROLE regress_constraint_comments;
 DROP ROLE regress_constraint_comments_noaccess;
+-- test cases for not null not valid and validate it.
+create table t0_ok(a int, constraint nn not null a not valid);
+WARNING:  CREATE TABLE NOT NULL NOT VALID CONSTRAINT WILL SET TO VALID
+create table t_notnull_notvalid(a int);
+insert into t_notnull_notvalid values(null);
+alter table t_notnull_notvalid add constraint nc1 not null a not valid;
+\d t_notnull_notvalid
+         Table "public.t_notnull_notvalid"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+Check constraints:
+    "nc1" CHECK (a IS NOT NULL) NOT VALID
+
+alter table t_notnull_notvalid validate constraint nc1; --fail.
+ERROR:  column "a" of relation "t_notnull_notvalid" contains null values
+truncate t_notnull_notvalid;
+insert into t_notnull_notvalid values(1);
+alter table t_notnull_notvalid validate constraint nc1; --ok
+\d+ t_notnull_notvalid
+                            Table "public.t_notnull_notvalid"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "nc1" NOT NULL "a"
+
+create table lp (a int) partition by list (a);
+create table lp_default partition of lp default;
+create table lp_ef partition of lp for values in (1,2,3,4);
+create table lp_g partition of lp for values in (5,6,7,8);
+create table lp_null partition of lp for values in (null);
+alter table lp add constraint nc1 not null a not valid;
+insert into lp select g from generate_Series(2, 9) g;
+insert into lp_null select NULL; --fail
+ERROR:  new row for relation "lp_null" violates check constraint "nc1"
+DETAIL:  Failing row contains (null).
+alter table lp_default validate constraint nc1; --fail
+ERROR:  cannot validate not-null constraint from partition "lp_default"
+HINT:  You might need validate not-null constraint through root partition "lp"
+alter table lp_g validate constraint nc1; --fail
+ERROR:  cannot validate not-null constraint from partition "lp_g"
+HINT:  You might need validate not-null constraint through root partition "lp"
+alter table lp validate constraint nc1; --ok.
+alter table lp validate constraint nc1; --ok.
+\d lp
+           Partitioned table "public.lp"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           | not null | 
+Partition key: LIST (a)
+Number of partitions: 4 (Use \d+ to list them.)
+
+\d lp_default
+             Table "public.lp_default"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           | not null | 
+Partition of: lp DEFAULT
+
+drop table t0_ok;
+drop table lp;
+drop table t_notnull_notvalid;
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 21ce4177de4..06c3895e745 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -836,3 +836,44 @@ DROP DOMAIN constraint_comments_dom;
 
 DROP ROLE regress_constraint_comments;
 DROP ROLE regress_constraint_comments_noaccess;
+
+
+-- test cases for not null not valid and validate it.
+create table t0_ok(a int, constraint nn not null a not valid);
+
+create table t_notnull_notvalid(a int);
+insert into t_notnull_notvalid values(null);
+alter table t_notnull_notvalid add constraint nc1 not null a not valid;
+\d t_notnull_notvalid
+alter table t_notnull_notvalid validate constraint nc1; --fail.
+
+truncate t_notnull_notvalid;
+insert into t_notnull_notvalid values(1);
+alter table t_notnull_notvalid validate constraint nc1; --ok
+\d+ t_notnull_notvalid
+
+
+create table lp (a int) partition by list (a);
+create table lp_default partition of lp default;
+create table lp_ef partition of lp for values in (1,2,3,4);
+create table lp_g partition of lp for values in (5,6,7,8);
+create table lp_null partition of lp for values in (null);
+alter table lp add constraint nc1 not null a not valid;
+insert into lp select g from generate_Series(2, 9) g;
+
+insert into lp_null select NULL; --fail
+alter table lp_default validate constraint nc1; --fail
+alter table lp_g validate constraint nc1; --fail
+
+alter table lp validate constraint nc1; --ok.
+alter table lp validate constraint nc1; --ok.
+
+\d lp
+\d lp_default
+
+
+
+drop table t0_ok;
+drop table lp;
+drop table t_notnull_notvalid;
+
-- 
2.34.1

---

From: Alvaro Herrera <[email-masked]>
Date: Mon, 17 Mar 2025 12:23:25 +0100

> the wording needs to change...

Yeah, we discussed this elsewhere.  I have an alpha-quality patch for
that, but I wasn't too sure about it ...

[1] https://postgr.es/m/[email-masked]

---

From: Rushabh Lathia <[email-masked]>
Date: Thu, 20 Mar 2025 13:54:45 +0400

Hi Alvaro,

Thank you for the offline discussion.

As we all agree, changing the attnotnull datatype would not be a good idea
since it is
a commonly used catalog column, and many applications and extensions depend
on it.

Attached is another version of the patch (WIP), where I have introduced a
new catalog column,
pg_attribute.attinvalidnotnull (boolean). This column will default to FALSE
but will be set to TRUE
when an INVALID NOT NULL constraint is created.  With this approach, we can
avoid performing
extra scans on the catalog table to identify INVALID NOT NULL constraints,
ensuring there is no
performance impact.

Also updated the pg_dump implementation patch and attaching the same here.

Thanks,

On Mon, Mar 17, 2025 at 3:23 PM Alvaro Herrera <[email-masked]>
wrote:

> [1]
> https://postgr.es/m/[email-masked]
>
> --

-- 
Rushabh Lathia

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 20 Mar 2025 11:26:26 +0100

Hello

On 2025-Mar-20, Rushabh Lathia wrote:

> Attached is another version of the patch (WIP), where I have
> introduced a new catalog column, pg_attribute.attinvalidnotnull
> (boolean). This column will default to FALSE but will be set to TRUE
> when an INVALID NOT NULL constraint is created.  With this approach,
> we can avoid performing extra scans on the catalog table to identify
> INVALID NOT NULL constraints, ensuring there is no performance impact.

I find this a reasonable way forward.  We won't impact any applications
that are currently relying on the boolean semantics of attnotnull, while
still allowing the backend to know the real status of the constraint
without having to scan pg_constraint for it.

I also checked the struct layout, which is currently

struct FormData_pg_attribute {
    Oid                        attrelid;             /*     0     4 */
    NameData                   attname;              /*     4    64 */
    /* --- cacheline 1 boundary (64 bytes) was 4 bytes ago --- */
    Oid                        atttypid;             /*    68     4 */
    int16                      attlen;               /*    72     2 */
    int16                      attnum;               /*    74     2 */
    int32                      atttypmod;            /*    76     4 */
    int16                      attndims;             /*    80     2 */
    _Bool                      attbyval;             /*    82     1 */
    char                       attalign;             /*    83     1 */
    char                       attstorage;           /*    84     1 */
    char                       attcompression;       /*    85     1 */
    _Bool                      attnotnull;           /*    86     1 */
    _Bool                      atthasdef;            /*    87     1 */
    _Bool                      atthasmissing;        /*    88     1 */
    char                       attidentity;          /*    89     1 */
    char                       attgenerated;         /*    90     1 */
    _Bool                      attisdropped;         /*    91     1 */
    _Bool                      attislocal;           /*    92     1 */

    /* XXX 1 byte hole, try to pack */

    int16                      attinhcount;          /*    94     2 */
    Oid                        attcollation;         /*    96     4 */

    /* size: 100, cachelines: 2, members: 20 */
    /* sum members: 99, holes: 1, sum holes: 1 */
    /* last cacheline: 36 bytes */
};

Since there's a one-byte hole after the lot of bools/chars, the new bool
will use it and this won't enlarge the storage, neither in memory nor on
disk.

I have not reviewed this patch in detail yet.

Thank you!

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/

---

From: jian he <[email-masked]>
Date: Thu, 20 Mar 2025 18:34:23 +0800

On Thu, Mar 20, 2025 at 5:54 PM Rushabh Lathia <[email-masked]> wrote:
hi. looking at the regress tests.

+-- verify NOT NULL VALID/NOT VALID
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1);
+INSERT INTO notnull_tbl1 VALUES (NULL, 2);
+INSERT INTO notnull_tbl1 VALUES (300, 3);
+-- Below statement should throw an error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+ERROR:  column "a" of relation "notnull_tbl1" contains null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID;
+\d+ notnull_tbl1
+                               Table "public.notnull_tbl1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats
target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              |
+ b      | integer |           |          |         | plain   |              |
+Not-null constraints:
+    "nn" NOT NULL "a" NOT VALID

as I mentioned in an offline discussion.
as you can see the output of `\d+ notnull_tbl1`
That means the pg_attribute.attnotnull definition is changed.

current description in
https://www.postgresql.org/docs/current/catalog-pg-attribute.html
attnotnull bool
This represents a not-null constraint.

now the "attnotnull" column means:
This represents a not-null constraint, it may not be validated.
This attribute column may already contain NULLs on it.

so doc/src/sgml/catalogs.sgml the following part will need to adjust
accordingly.

     <row>
      <entry role="catalog_table_entry"><para role="column_definition">
       <structfield>attnotnull</structfield> <type>bool</type>
      </para>
      <para>
       This column has a not-null constraint.
      </para></entry>
     </row>

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 20 Mar 2025 12:07:29 +0100

>        This column has a not-null constraint.

"This column has a possibly unvalidated not-null constraint".  The
description for the new column would say "the not-null constraint for
this column is validated" (or the opposite).  My recommendation is to
rename the other column from attinvalidnotnull (Rushabh's proposal) to
"attnotnullvalid" and invert the sense of the boolean.  I think that's
less confusing.

Thanks

-- 
Álvaro Herrera        Breisgau, Deutschland  —  https://www.EnterpriseDB.com/
"If you want to have good ideas, you must have many ideas.  Most of them
will be wrong, and what you have to learn is which ones to throw away."
                                                         (Linus Pauling)

---

From: Ashutosh Bapat <[email-masked]>
Date: Thu, 20 Mar 2025 17:49:57 +0530

On Thu, Mar 20, 2025 at 3:25 PM Rushabh Lathia <[email-masked]> wrote:
> As we all agree, changing the attnotnull datatype would not be a good idea since it is
> a commonly used catalog column, and many applications and extensions depend on it.
>
> Attached is another version of the patch (WIP), where I have introduced a new catalog column,
> pg_attribute.attinvalidnotnull (boolean). This column will default to FALSE but will be set to TRUE
> when an INVALID NOT NULL constraint is created.  With this approach, we can avoid performing
> extra scans on the catalog table to identify INVALID NOT NULL constraints, ensuring there is no
> performance impact.
>

These patches do not address comments discussed in [1]. Since there
was a change in design, I am assuming that those will be addressed
once the design change is accepted.

[1] https://www.postgresql.org/message-id/[email-masked]

-- 
Best Wishes,
Ashutosh Bapat

---

From: jian he <[email-masked]>
Date: Thu, 20 Mar 2025 22:59:28 +0800

> Is it expected that a child may have VALID constraint but parent has
but the MergeConstraintsIntoExisting logic is when
ALTER TABLE ATTACH PARTITION,
it expects the child table to also have an equivalent constraint
definition on it.
see MergeConstraintsIntoExisting:
            ereport(ERROR,
                    (errcode(ERRCODE_DATATYPE_MISMATCH),
                     errmsg("child table is missing constraint \"%s\"",
                            NameStr(parent_con->conname))));

So I decided not to support it.

main idea:
NOT NULL NOT VALID
* one column one NOT NULL, if you want to change status, it's not
allowed, it will error out, give you hints.
* it will logically be equivalent to CHECK(x IS NOT NULL) NOT VALID.
* it can only be added using ALTER TABLE, not with CREATE TABLE (a
warning will be issued)
* pg_attribute.attinvalidnotnull meaning: this attnum has a
(convalidated == false) NOT NULL pg_constraint entry to it.

* if attnotnull is true, then attinvalidnotnull should be false.
  Conversely, if attinvalidnotnull is true, then attnotnull should be false.
* an invalid not-null cannot be used while adding a primary key.
* if attinvalidnotnull is true, this column can not accept NULL values,
  but the existing column value may contain NULLs, we need to
  VALIDATE the not-null constraint to check if this column exists NULL
values or not.
* partitioned table can not have NOT NULL NOT VALID.
From 0106f2ded55a699324369412de859ab93f374960 Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Thu, 20 Mar 2025 22:43:08 +0800
Subject: [PATCH v3 1/1] NOT NULL NOT VALID
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* it will logically be equivalent to CHECK(x IS NOT NULL) NOT VALID.
* it can only be added using ALTER TABLE, not with CREATE TABLE (a warning will issued)
* if attnotnull is true, then attinvalidnotnull should be false.
  Conversely, if attinvalidnotnull is true, then attnotnull should be false.
* an invalid not-null cannot be used while adding a primary key.
* if attinvalidnotnull is true, this column can not accept NULL values,
  but the existing column value may contain NULLs, we need to
  VALIDATE the not-null constraint to check if this column exists NULL values or not.
* TODO: currently, partitioned table can not mark as NO VALID. maybe it's doable.
  partition with not valid not-null will error out while attach to the partitioned table.
---
 doc/src/sgml/catalogs.sgml                |  10 +
 src/backend/access/common/tupdesc.c       |  11 ++
 src/backend/bootstrap/bootstrap.c         |   1 +
 src/backend/catalog/heap.c                |   8 +-
 src/backend/catalog/pg_constraint.c       |  45 ++++-
 src/backend/commands/tablecmds.c          | 211 +++++++++++++++++++++-
 src/backend/executor/execMain.c           | 130 ++++++++-----
 src/backend/parser/gram.y                 |   4 +-
 src/backend/parser/parse_utilcmd.c        |   7 +-
 src/backend/utils/cache/relcache.c        |   3 +
 src/bin/psql/describe.c                   |   9 +-
 src/include/access/tupdesc.h              |   1 +
 src/include/catalog/pg_attribute.h        |   3 +
 src/include/catalog/pg_constraint.h       |   8 +-
 src/test/regress/expected/constraints.out | 128 +++++++++++++
 src/test/regress/sql/constraints.sql      |  92 ++++++++++
 16 files changed, 593 insertions(+), 78 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..299d2a46f4e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1264,6 +1264,16 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attinvalidnotnull</structfield> <type>bool</type>
+      </para>
+      <para>
+       This column has a attinvalidnotnull not-null constraint.
+       If <structfield>attnotnull</structfield> is true, this has to be false.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>atthasdef</structfield> <type>bool</type>
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index ed2195f14b2..634cfddff23 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -252,6 +252,7 @@ CreateTupleDescCopy(TupleDesc tupdesc)
 		Form_pg_attribute att = TupleDescAttr(desc, i);
 
 		att->attnotnull = false;
+		att->attinvalidnotnull = false;
 		att->atthasdef = false;
 		att->atthasmissing = false;
 		att->attidentity = '\0';
@@ -298,6 +299,7 @@ CreateTupleDescTruncatedCopy(TupleDesc tupdesc, int natts)
 		Form_pg_attribute att = TupleDescAttr(desc, i);
 
 		att->attnotnull = false;
+		att->attinvalidnotnull = false;
 		att->atthasdef = false;
 		att->atthasmissing = false;
 		att->attidentity = '\0';
@@ -341,6 +343,7 @@ CreateTupleDescCopyConstr(TupleDesc tupdesc)
 		TupleConstr *cpy = (TupleConstr *) palloc0(sizeof(TupleConstr));
 
 		cpy->has_not_null = constr->has_not_null;
+		cpy->has_invalid_not_null = constr->has_invalid_not_null;
 		cpy->has_generated_stored = constr->has_generated_stored;
 		cpy->has_generated_virtual = constr->has_generated_virtual;
 
@@ -418,6 +421,7 @@ TupleDescCopy(TupleDesc dst, TupleDesc src)
 		Form_pg_attribute att = TupleDescAttr(dst, i);
 
 		att->attnotnull = false;
+		att->attinvalidnotnull = false;
 		att->atthasdef = false;
 		att->atthasmissing = false;
 		att->attidentity = '\0';
@@ -464,6 +468,7 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno,
 
 	/* since we're not copying constraints or defaults, clear these */
 	dstAtt->attnotnull = false;
+	dstAtt->attinvalidnotnull = false;
 	dstAtt->atthasdef = false;
 	dstAtt->atthasmissing = false;
 	dstAtt->attidentity = '\0';
@@ -613,6 +618,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attnotnull != attr2->attnotnull)
 			return false;
+		if (attr1->attinvalidnotnull != attr2->attinvalidnotnull)
+			return false;
 		if (attr1->atthasdef != attr2->atthasdef)
 			return false;
 		if (attr1->attidentity != attr2->attidentity)
@@ -639,6 +646,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (constr1->has_not_null != constr2->has_not_null)
 			return false;
+		if (constr1->has_invalid_not_null != constr2->has_invalid_not_null)
+			return false;
 		if (constr1->has_generated_stored != constr2->has_generated_stored)
 			return false;
 		if (constr1->has_generated_virtual != constr2->has_generated_virtual)
@@ -841,6 +850,7 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attndims = attdim;
 
 	att->attnotnull = false;
+	att->attinvalidnotnull = false;
 	att->atthasdef = false;
 	att->atthasmissing = false;
 	att->attidentity = '\0';
@@ -904,6 +914,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attndims = attdim;
 
 	att->attnotnull = false;
+	att->attinvalidnotnull = false;
 	att->atthasdef = false;
 	att->atthasmissing = false;
 	att->attidentity = '\0';
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 6db864892d0..9077ed46d33 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -582,6 +582,7 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
 
 	attrtypes[attnum]->atttypmod = -1;
 	attrtypes[attnum]->attislocal = true;
+	attrtypes[attnum]->attinvalidnotnull = false;
 
 	if (nullness == BOOTCOL_NULL_FORCE_NOT_NULL)
 	{
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..e3ae6993c95 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -753,6 +753,7 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attstorage - 1] = CharGetDatum(attrs->attstorage);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcompression - 1] = CharGetDatum(attrs->attcompression);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attnotnull - 1] = BoolGetDatum(attrs->attnotnull);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attinvalidnotnull - 1] = BoolGetDatum(attrs->attinvalidnotnull);
 		slot[slotCount]->tts_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(attrs->atthasdef);
 		slot[slotCount]->tts_values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(attrs->atthasmissing);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(attrs->attidentity);
@@ -2621,12 +2622,17 @@ AddRelationNewConstraints(Relation rel,
 						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						errmsg("not-null constraints are not supported on virtual generated columns"));
 
+			if (cdef->initially_valid)
+				Assert(!cdef->skip_validation);
+			else
+				Assert(cdef->skip_validation);
+
 			/*
 			 * If the column already has a not-null constraint, we don't want
 			 * to add another one; just adjust inheritance status as needed.
 			 */
 			if (AdjustNotNullInheritance(RelationGetRelid(rel), colnum,
-										 is_local, cdef->is_no_inherit))
+										 is_local, cdef->is_no_inherit, cdef->skip_validation))
 				continue;
 
 			/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..64f2e04859a 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -576,13 +576,14 @@ ChooseConstraintName(const char *name1, const char *name2,
  * Find and return a copy of the pg_constraint tuple that implements a
  * validated not-null constraint for the given column of the given relation.
  * If no such constraint exists, return NULL.
+ * if include_invalid is true, it may return an invalid not-null tuple.
  *
  * XXX This would be easier if we had pg_attribute.notnullconstr with the OID
  * of the constraint that implements the not-null constraint for that column.
  * I'm not sure it's worth the catalog bloat and de-normalization, however.
  */
 HeapTuple
-findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
+findNotNullConstraintAttnum(Oid relid, AttrNumber attnum, bool include_invalid)
 {
 	Relation	pg_constraint;
 	HeapTuple	conTup,
@@ -609,7 +610,7 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
 		 */
 		if (con->contype != CONSTRAINT_NOTNULL)
 			continue;
-		if (!con->convalidated)
+		if (!con->convalidated && !include_invalid)
 			continue;
 
 		conkey = extractNotNullColumn(conTup);
@@ -631,9 +632,10 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
  * Find and return the pg_constraint tuple that implements a validated
  * not-null constraint for the given column of the given relation.  If
  * no such column or no such constraint exists, return NULL.
+ * if include_invalid is true, it may return an invalid not-null tuple.
  */
 HeapTuple
-findNotNullConstraint(Oid relid, const char *colname)
+findNotNullConstraint(Oid relid, const char *colname, bool include_invalid)
 {
 	AttrNumber	attnum;
 
@@ -641,7 +643,7 @@ findNotNullConstraint(Oid relid, const char *colname)
 	if (attnum <= InvalidAttrNumber)
 		return NULL;
 
-	return findNotNullConstraintAttnum(relid, attnum);
+	return findNotNullConstraintAttnum(relid, attnum, include_invalid);
 }
 
 /*
@@ -729,11 +731,11 @@ extractNotNullColumn(HeapTuple constrTup)
  */
 bool
 AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
-						 bool is_local, bool is_no_inherit)
+						 bool is_local, bool is_no_inherit, bool is_notvalid)
 {
 	HeapTuple	tup;
 
-	tup = findNotNullConstraintAttnum(relid, attnum);
+	tup = findNotNullConstraintAttnum(relid, attnum, true);
 	if (HeapTupleIsValid(tup))
 	{
 		Relation	pg_constraint;
@@ -753,6 +755,27 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
 					errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"",
 						   NameStr(conform->conname), get_rel_name(relid)));
 
+		/*
+		 * Throw an error if an invalid NOT NULL constraint exists on the
+		 * table column and an attempt is made to add another valid NOT NULL
+		 * constraint.
+		 */
+		if (is_notvalid && conform->convalidated)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot change VALID status of NOT NULL constraint \"%s\" on relation \"%s\"",
+						   NameStr(conform->conname), get_rel_name(relid)),
+					errhint("You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint \"%s\"",
+							 NameStr(conform->conname)));
+
+		if (!is_notvalid && !conform->convalidated)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot change VALID status of NOT NULL constraint \"%s\" on relation \"%s\"",
+						   NameStr(conform->conname), get_rel_name(relid)),
+					errhint("You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint \"%s\"",
+							 NameStr(conform->conname)));
+
 		if (!is_local)
 		{
 			if (pg_add_s16_overflow(conform->coninhcount, 1,
@@ -788,9 +811,10 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
  * This is seldom needed, so we just scan pg_constraint each time.
  *
  * 'include_noinh' determines whether to include NO INHERIT constraints or not.
+ * 'include_notvalid' determines whether to include NO VALID constraints or not.
  */
 List *
-RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
+RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh, bool include_notvalid)
 {
 	List	   *notnulls = NIL;
 	Relation	constrRel;
@@ -816,6 +840,9 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
 		if (conForm->connoinherit && !include_noinh)
 			continue;
 
+		if (!conForm->convalidated && !include_notvalid)
+			continue;
+
 		colnum = extractNotNullColumn(htup);
 
 		if (cooked)
@@ -830,7 +857,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
 			cooked->attnum = colnum;
 			cooked->expr = NULL;
 			cooked->is_enforced = true;
-			cooked->skip_validation = false;
+			cooked->skip_validation = !conForm->convalidated;
 			cooked->is_local = true;
 			cooked->inhcount = 0;
 			cooked->is_no_inherit = conForm->connoinherit;
@@ -850,7 +877,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
 			constr->keys = list_make1(makeString(get_attname(relid, colnum,
 															 false)));
 			constr->is_enforced = true;
-			constr->skip_validation = false;
+			constr->skip_validation = !conForm->convalidated;
 			constr->initially_valid = true;
 			constr->is_no_inherit = conForm->connoinherit;
 			notnulls = lappend(notnulls, constr);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 129c97fdf28..b1828ec0945 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -410,6 +410,9 @@ static void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation
 static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 										   char *constrName, HeapTuple contuple,
 										   bool recurse, bool recursing, LOCKMODE lockmode);
+static void QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+										HeapTuple contuple, bool recurse, bool recursing,
+										LOCKMODE lockmode);
 static int	transformColumnNameList(Oid relId, List *colList,
 									int16 *attnums, Oid *atttypids, Oid *attcollids);
 static int	transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
@@ -2713,10 +2716,10 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 
 		/*
 		 * Request attnotnull on columns that have a not-null constraint
-		 * that's not marked NO INHERIT.
+		 * that's not marked NO INHERIT. but we will include NOT VALID
 		 */
 		nnconstrs = RelationGetNotNullConstraints(RelationGetRelid(relation),
-												  true, false);
+												  true, false, true);
 		foreach_ptr(CookedConstraint, cc, nnconstrs)
 			nncols = bms_add_member(nncols, cc->attnum);
 
@@ -7711,7 +7714,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
 	 * Find the constraint that makes this column NOT NULL, and drop it.
 	 * dropconstraint_internal() resets attnotnull.
 	 */
-	conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
+	conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum, false);
 	if (conTup == NULL)
 		elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
 			 colName, RelationGetRelationName(rel));
@@ -7729,6 +7732,45 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
 	return address;
 }
 
+static void
+set_attinvalidnotnull(Relation rel, AttrNumber attnum,
+					  bool attinvalidnotnull)
+{
+	Form_pg_attribute attr;
+
+	CheckAlterTableIsSafe(rel);
+
+	/*
+	 * Exit quickly by testing attnotnull from the tupledesc's copy of the
+	 * attribute.
+	 */
+	attr = TupleDescAttr(RelationGetDescr(rel), attnum - 1);
+	if (attr->attisdropped)
+		return;
+
+	if (attr->attinvalidnotnull != attinvalidnotnull)
+	{
+		Relation	attr_rel;
+		HeapTuple	tuple;
+
+		attr_rel = table_open(AttributeRelationId, RowExclusiveLock);
+
+		tuple = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum);
+		if (!HeapTupleIsValid(tuple))
+			elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+				 attnum, RelationGetRelid(rel));
+
+		attr = (Form_pg_attribute) GETSTRUCT(tuple);
+		attr->attinvalidnotnull = attinvalidnotnull;
+
+		CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
+		CommandCounterIncrement();
+
+		table_close(attr_rel, RowExclusiveLock);
+		heap_freetuple(tuple);
+	}
+}
+
 /*
  * Helper to set pg_attribute.attnotnull if it isn't set, and to tell phase 3
  * to verify it.
@@ -7845,7 +7887,7 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 						   colName, RelationGetRelationName(rel))));
 
 	/* See if there's already a constraint */
-	tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
+	tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum, true);
 	if (HeapTupleIsValid(tuple))
 	{
 		Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
@@ -7880,6 +7922,19 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 			conForm->conislocal = true;
 			changed = true;
 		}
+		else if (!conForm->convalidated)
+		{
+		   /*
+			* Don't let a NO VALID constraint be changed into VALID.
+			* Only way to validate a not-nul constraint is through ALTER TABLE VALIDATE CONSTRAINT
+		   */
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot change VALID status of NOT NULL constraint \"%s\" on relation \"%s\"",
+						   NameStr(conForm->conname), get_rel_name(RelationGetRelid(rel))),
+					errhint("You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint \"%s\"",
+							NameStr(conForm->conname)));
+		}
 
 		if (changed)
 		{
@@ -9387,6 +9442,21 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
 	{
 		AlterTableCmd *newcmd;
 		Constraint *nnconstr;
+		HeapTuple	tuple;
+
+		tuple = findNotNullConstraint(RelationGetRelid(rel), strVal(lfirst(lc)), true);
+		if (tuple != NULL)
+		{
+			Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
+			if (!conForm->convalidated)
+				ereport(ERROR,
+						errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+						errmsg("column \"%s\" of table \"%s\" is marked as NOT VALID NOT NULL constraint",
+							   strVal(lfirst(lc)), RelationGetRelationName(rel)),
+						errhint("You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint \"%s\"",
+								NameStr(conForm->conname)));
+			heap_freetuple(tuple);
+		}
 
 		nnconstr = makeNotNullConstraint(lfirst(lc));
 
@@ -9765,9 +9835,12 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 		 * If adding a not-null constraint, set the pg_attribute flag and tell
 		 * phase 3 to verify existing rows, if needed.
 		 */
-		if (constr->contype == CONSTR_NOTNULL)
+		if (constr->contype == CONSTR_NOTNULL && !constr->skip_validation)
 			set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
 
+		if (constr->contype == CONSTR_NOTNULL && constr->skip_validation)
+			set_attinvalidnotnull(rel, ccon->attnum, lockmode);
+
 		ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
 	}
 
@@ -12176,7 +12249,7 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
 				HeapTuple	childtup;
 				Form_pg_constraint childcon;
 
-				childtup = findNotNullConstraint(childoid, colName);
+				childtup = findNotNullConstraint(childoid, colName, false);
 				if (!childtup)
 					elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
 						 colName, childoid);
@@ -12367,10 +12440,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 
+				 errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key or check constraint or not-null constraint",
 
 	if (!con->conenforced)
@@ -12389,6 +12463,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 			QueueCheckConstraintValidation(wqueue, conrel, rel, constrName,
 										   tuple, recurse, recursing, lockmode);
 		}
+		else if (con->contype == CONSTRAINT_NOTNULL)
+		{
+			QueueNNConstraintValidation(wqueue, conrel, rel,
+										tuple, recurse, recursing, lockmode);
+		}
 
 		ObjectAddressSet(address, ConstraintRelationId, con->oid);
 	}
@@ -12605,6 +12684,117 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 	heap_freetuple(copyTuple);
 }
 
+/*
+ * QueueNNConstraintValidation
+ *
+ * Add an entry to the wqueue to validate the given not-null constraint in
+ * Phase 3 and update the convalidated field in the pg_constraint catalog for
+ * the specified relation and all its inheriting children.
+ */
+static void
+QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+							HeapTuple contuple, bool recurse, bool recursing,
+							LOCKMODE lockmode)
+{
+	Form_pg_constraint con;
+	AlteredTableInfo *tab;
+	HeapTuple	copyTuple;
+	Form_pg_constraint copy_con;
+	List	   *children = NIL;
+	AttrNumber	attnum;
+	char	   *colname;
+
+	con = (Form_pg_constraint) GETSTRUCT(contuple);
+	Assert(con->contype == CONSTRAINT_NOTNULL);
+
+	attnum = extractNotNullColumn(contuple);
+
+	/*
+	 * For constraints that aren't NO INHERIT, we must ensure that we only
+	 * mark the constraint as validated on the parent if it's already
+	 * validated on the children.
+	 *
+	 * If we're recursing, the parent has already done this, so skip it.
+	 *
+	 * We recurse before validating on the parent, to reduce risk of
+	 * deadlocks.
+	 */
+	if (!recursing && !con->connoinherit)
+		children = find_all_inheritors(RelationGetRelid(rel), lockmode, NULL);
+
+	colname = get_attname(RelationGetRelid(rel), attnum, false);
+	foreach_oid(childoid, children)
+	{
+		Relation	childrel;
+		HeapTuple	contup;
+		Form_pg_constraint childcon;
+		char	   *conname;
+
+		if (childoid == RelationGetRelid(rel))
+			continue;
+
+		/*
+		 * If we are told not to recurse, there had better not be any child
+		 * tables, because we can't mark the constraint on the parent valid
+		 * unless it is valid for all child tables.
+		 */
+		if (!recurse)
+			ereport(ERROR,
+					errcode(ERRCODE_INVALID_TABLE_DEFINITION),
+					errmsg("constraint must be validated on child tables too"));
+
+		/*
+		 * The column on child might have a different attnum, so search by
+		 * column name.
+		 */
+		contup = findNotNullConstraint(childoid, colname, true);
+		if (!contup)
+			elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
+				 colname, get_rel_name(childoid));
+		childcon = (Form_pg_constraint) GETSTRUCT(contup);
+		if (childcon->convalidated)
+			continue;
+
+		/* find_all_inheritors already got lock */
+		childrel = table_open(childoid, NoLock);
+		conname = pstrdup(NameStr(childcon->conname));
+
+		/* XXX improve ATExecValidateConstraint API to avoid double search */
+		ATExecValidateConstraint(wqueue, childrel, conname,
+								 false, true, lockmode);
+		table_close(childrel, NoLock);
+	}
+
+	tab = ATGetQueueEntry(wqueue, rel);
+	tab->verify_new_notnull = true;
+
+	set_attnotnull(NULL, rel, attnum, lockmode);
+
+	/*
+	 * Invalidate relcache so that others see the new validated constraint.
+	 */
+	CacheInvalidateRelcache(rel);
+
+
+	/*
+	 * Now update the catalogs, while we have the door open.
+	 */
+	copyTuple = heap_copytuple(contuple);
+	copy_con = (Form_pg_constraint) GETSTRUCT(copyTuple);
+	copy_con->convalidated = true;
+	CatalogTupleUpdate(conrel, &copyTuple->t_self, copyTuple);
+
+	/*
+	 * Also flip attnotnull. call function with wqueue as NULL to
+	 * bypass validation, as it has already been performed.
+	 */
+	set_attinvalidnotnull(rel, attnum, false);
+
+	InvokeObjectPostAlterHook(ConstraintRelationId, con->oid, 0);
+
+	heap_freetuple(copyTuple);
+}
+
 /*
  * transformColumnNameList - transform list of column names
  *
@@ -13549,7 +13739,7 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
 		 */
 		if (con->contype == CONSTRAINT_NOTNULL)
 		{
-			tuple = findNotNullConstraint(childrelid, colname);
+			tuple = findNotNullConstraint(childrelid, colname, false);
 			if (!HeapTupleIsValid(tuple))
 				elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
 					 colname, RelationGetRelid(childrel));
@@ -16813,8 +17003,9 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 			{
 				HeapTuple	contup;
 
+				Assert(!parent_att->attinvalidnotnull);
 				contup = findNotNullConstraintAttnum(RelationGetRelid(parent_rel),
-													 parent_att->attnum);
+													 parent_att->attnum, false);
 				if (HeapTupleIsValid(contup) &&
 					!((Form_pg_constraint) GETSTRUCT(contup))->connoinherit)
 					ereport(ERROR,
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index e9bd98c7738..06226150f94 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -93,6 +93,9 @@ static bool ExecCheckPermissionsModified(Oid relOid, Oid userid,
 static void ExecCheckXactReadOnly(PlannedStmt *plannedstmt);
 static void EvalPlanQualStart(EPQState *epqstate, Plan *planTree);
 
+static void ReportNotNullViolationError(ResultRelInfo *resultRelInfo,
+										TupleTableSlot *slot,
+										EState *estate, int attrChk);
 /* end of local decls */

@@ -2071,57 +2074,21 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
 			Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
 
 			if (att->attnotnull && slot_attisnull(slot, attrChk))
-			{
-				char	   *val_desc;
-				Relation	orig_rel = rel;
-				TupleDesc	orig_tupdesc = RelationGetDescr(rel);
+				ReportNotNullViolationError(resultRelInfo, slot, estate, attrChk);
+		}
+	}
 
-				/*
-				 * If the tuple has been routed, it's been converted to the
-				 * partition's rowtype, which might differ from the root
-				 * table's.  We must convert it back to the root table's
-				 * rowtype so that val_desc shown error message matches the
-				 * input tuple.
-				 */
-				if (resultRelInfo->ri_RootResultRelInfo)
-				{
-					ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
-					AttrMap    *map;
+	if (constr->has_invalid_not_null)
+	{
+		int			natts = tupdesc->natts;
+		int			attrChk;
 
-					tupdesc = RelationGetDescr(rootrel->ri_RelationDesc);
-					/* a reverse map */
-					map = build_attrmap_by_name_if_req(orig_tupdesc,
-													   tupdesc,
-													   false);
+		for (attrChk = 1; attrChk <= natts; attrChk++)
+		{
+			Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
 
-					/*
-					 * Partition-specific slot's tupdesc can't be changed, so
-					 * allocate a new one.
-					 */
-					if (map != NULL)
-						slot = execute_attr_map_slot(map, slot,
-													 MakeTupleTableSlot(tupdesc, &TTSOpsVirtual));
-					modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate),
-											 ExecGetUpdatedCols(rootrel, estate));
-					rel = rootrel->ri_RelationDesc;
-				}
-				else
-					modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, estate),
-											 ExecGetUpdatedCols(resultRelInfo, estate));
-				val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
-														 slot,
-														 tupdesc,
-														 modifiedCols,
-														 64);
-
-				ereport(ERROR,
-						(errcode(ERRCODE_NOT_NULL_VIOLATION),
-						 errmsg("null value in column \"%s\" of relation \"%s\" violates not-null constraint",
-								NameStr(att->attname),
-								RelationGetRelationName(orig_rel)),
-						 val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
-						 errtablecol(orig_rel, attrChk)));
-			}
+			if (att->attinvalidnotnull && slot_attisnull(slot, attrChk))
+				ReportNotNullViolationError(resultRelInfo, slot, estate, attrChk);
 		}
 	}
 
@@ -2176,6 +2143,73 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
 	}
 }
 
+
+/*
+ * Report a violation of a not-null constraint that was already detected.
+ */
+static void
+ReportNotNullViolationError(ResultRelInfo *resultRelInfo, TupleTableSlot *slot,
+							EState *estate, int attrChk)
+{
+	Bitmapset  *modifiedCols;
+	char	   *val_desc;
+
+	Relation	rel = resultRelInfo->ri_RelationDesc;
+	Relation	orig_rel = rel;
+
+	TupleDesc	tupdesc = RelationGetDescr(rel);
+	TupleDesc	orig_tupdesc = RelationGetDescr(rel);
+	Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
+
+	Assert(attrChk > 0);
+
+	/*
+	 * If the tuple has been routed, it's been converted to the partition's
+	 * rowtype, which might differ from the root table's.  We must convert it
+	 * back to the root table's rowtype so that val_desc shown error message
+	 * matches the input tuple.
+	 */
+	if (resultRelInfo->ri_RootResultRelInfo)
+	{
+		ResultRelInfo *rootrel = resultRelInfo->ri_RootResultRelInfo;
+		AttrMap    *map;
+
+		tupdesc = RelationGetDescr(rootrel->ri_RelationDesc);
+		/* a reverse map */
+		map = build_attrmap_by_name_if_req(orig_tupdesc,
+										   tupdesc,
+										   false);
+
+		/*
+		 * Partition-specific slot's tupdesc can't be changed, so allocate a
+		 * new one.
+		 */
+		if (map != NULL)
+			slot = execute_attr_map_slot(map, slot,
+										 MakeTupleTableSlot(tupdesc, &TTSOpsVirtual));
+		modifiedCols = bms_union(ExecGetInsertedCols(rootrel, estate),
+								 ExecGetUpdatedCols(rootrel, estate));
+		rel = rootrel->ri_RelationDesc;
+	}
+	else
+		modifiedCols = bms_union(ExecGetInsertedCols(resultRelInfo, estate),
+								 ExecGetUpdatedCols(resultRelInfo, estate));
+
+	val_desc = ExecBuildSlotValueDescription(RelationGetRelid(rel),
+											 slot,
+											 tupdesc,
+											 modifiedCols,
+											 64);
+	ereport(ERROR,
+			errcode(ERRCODE_NOT_NULL_VIOLATION),
+			errmsg("null value in column \"%s\" of relation \"%s\" violates not-null constraint",
+				   NameStr(att->attname),
+				   RelationGetRelationName(orig_rel)),
+			val_desc ? errdetail("Failing row contains %s.", val_desc) : 0,
+			errtablecol(orig_rel, attrChk));
+}
+
+
 /*
  * ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs
  * of the specified kind.
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..715eb51300b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4224,9 +4224,9 @@ ConstraintElem:
 					n->keys = list_make1(makeString($3));
 					/* no NOT VALID support yet */
 					processCASbits($4, @4, "NOT NULL",
-								   NULL, NULL, NULL, NULL,
+								   NULL, NULL, NULL, &n->skip_validation,
 								   &n->is_no_inherit, yyscanner);
-					n->initially_valid = true;
+					n->initially_valid = !n->skip_validation;
 					$$ = (Node *) n;
 				}
 			| UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 896a7f2c59b..49fa91ea139 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -1089,6 +1089,11 @@ transformTableConstraint(CreateStmtContext *cxt, Constraint *constraint)
 				ereport(ERROR,
 						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						errmsg("not-null constraints on partitioned tables cannot be NO INHERIT"));
+			/* XXX TODO, this can be done */
+			if (cxt->ispartitioned && constraint->skip_validation)
+				ereport(ERROR,
+						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+						errmsg("not-null constraints on partitioned tables cannot be NO VALID"));
 
 			cxt->nnconstraints = lappend(cxt->nnconstraints, constraint);
 			break;
@@ -1291,7 +1296,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		List	   *lst;
 
 		lst = RelationGetNotNullConstraints(RelationGetRelid(relation), false,
-											true);
+											true, false);
 		cxt->nnconstraints = list_concat(cxt->nnconstraints, lst);
 	}
 
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9f54a9e72b7..d5cd93e55c3 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -593,6 +593,8 @@ RelationBuildTupleDesc(Relation relation)
 		/* Update constraint/default info */
 		if (attp->attnotnull)
 			constr->has_not_null = true;
+		if (attp->attinvalidnotnull)
+			constr->has_invalid_not_null = true;
 		if (attp->attgenerated == ATTRIBUTE_GENERATED_STORED)
 			constr->has_generated_stored = true;
 		if (attp->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
@@ -678,6 +680,7 @@ RelationBuildTupleDesc(Relation relation)
 	 * Set up constraint/default info
 	 */
 	if (constr->has_not_null ||
+		constr->has_invalid_not_null ||
 		constr->has_generated_stored ||
 		constr->has_generated_virtual ||
 		ndef > 0 ||
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..09f8f92a59c 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3114,7 +3114,8 @@ describeOneTableDetails(const char *schemaname,
 		{
 			printfPQExpBuffer(&buf,
 							  "SELECT c.conname, a.attname, c.connoinherit,\n"
-							  "  c.conislocal, c.coninhcount <> 0\n"
+							  "  c.conislocal, c.coninhcount <> 0,\n"
+							  "  c.convalidated\n"
 							  "FROM pg_catalog.pg_constraint c JOIN\n"
 							  "  pg_catalog.pg_attribute a ON\n"
 							  "    (a.attrelid = c.conrelid AND a.attnum = c.conkey[1])\n"
@@ -3137,14 +3138,16 @@ describeOneTableDetails(const char *schemaname,
 			{
 				bool		islocal = PQgetvalue(result, i, 3)[0] == 't';
 				bool		inherited = PQgetvalue(result, i, 4)[0] == 't';
+				bool		validated = PQgetvalue(result, i, 5)[0] == 't';
 
-				printfPQExpBuffer(&buf, "    \"%s\" NOT NULL \"%s\"%s",
+				printfPQExpBuffer(&buf, "    \"%s\" NOT NULL \"%s\"%s%s",
 								  PQgetvalue(result, i, 0),
 								  PQgetvalue(result, i, 1),
 								  PQgetvalue(result, i, 2)[0] == 't' ?
 								  " NO INHERIT" :
 								  islocal && inherited ? _(" (local, inherited)") :
-								  inherited ? _(" (inherited)") : "");
+								  inherited ? _(" (inherited)") : "",
+								  !validated ? " NO VALID": "");
 
 				printTableAddFooter(&cont, buf.data);
 			}
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 396eeb7a0bb..cc619477b63 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -43,6 +43,7 @@ typedef struct TupleConstr
 	uint16		num_defval;
 	uint16		num_check;
 	bool		has_not_null;
+	bool        has_invalid_not_null;
 	bool		has_generated_stored;
 	bool		has_generated_virtual;
 } TupleConstr;
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index deaa515fe53..9998b4abdef 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -120,6 +120,9 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	/* This flag represents the "NOT NULL" constraint */
 	bool		attnotnull;
 
+	/* This flag represents the "NOT NULL NOT VALID" constraint */
+	bool		attinvalidnotnull BKI_DEFAULT(f);
+
 	/* Has DEFAULT value or not */
 	bool		atthasdef BKI_DEFAULT(f);
 
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 6da164e7e4d..f00a8e25d5d 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -259,14 +259,14 @@ extern char *ChooseConstraintName(const char *name1, const char *name2,
 								  const char *label, Oid namespaceid,
 								  List *others);
 
-extern HeapTuple findNotNullConstraintAttnum(Oid relid, AttrNumber attnum);
-extern HeapTuple findNotNullConstraint(Oid relid, const char *colname);
+extern HeapTuple findNotNullConstraintAttnum(Oid relid, AttrNumber attnum, bool include_invalid);
+extern HeapTuple findNotNullConstraint(Oid relid, const char *colname, bool include_invalid);
 extern HeapTuple findDomainNotNullConstraint(Oid typid);
 extern AttrNumber extractNotNullColumn(HeapTuple constrTup);
 extern bool AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
-									 bool is_local, bool is_no_inherit);
+									 bool is_local, bool is_no_inherit, bool is_notvalid);
 extern List *RelationGetNotNullConstraints(Oid relid, bool cooked,
-										   bool include_noinh);
+										   bool include_noinh, bool include_notvalid);
 
 extern void RemoveConstraintById(Oid conId);
 extern void RenameConstraintById(Oid conId, const char *newname);
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..c8363a44523 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -896,6 +896,134 @@ Not-null constraints:
     "foobar" NOT NULL "a"
 
 DROP TABLE notnull_tbl1;
+-------tests for NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM  pg_constraint
+WHERE conrelid  = ANY($1)
+ORDER BY 1;
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1),(NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error
+ERROR:  column "a" of relation "notnull_tbl1" contains null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; --ok
+\d+ notnull_tbl1
+                               Table "public.notnull_tbl1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+Not-null constraints:
+    "nn" NOT NULL "a" NO VALID
+
+INSERT INTO notnull_tbl1 VALUES (NULL, 4); --error
+ERROR:  null value in column "a" of relation "notnull_tbl1" violates not-null constraint
+DETAIL:  Failing row contains (null, 4).
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1 ORDER BY a, b;
+  a  | b 
+-----+---
+ 100 | 1
+ 300 | 3
+(2 rows)
+
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+ERROR:  column "a" of table "notnull_tbl1" is marked as NOT VALID NOT NULL constraint
+HINT:  You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+-- INHERITS table having NOT VALID NOT NULL constraints.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+NOTICE:  merging column "a" with inherited definition
+NOTICE:  merging column "b" with inherited definition
+-- Child table NOT NULL constraints should be valid.
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+      conrelid      | conname | convalidated | coninhcount 
+--------------------+---------+--------------+-------------
+ notnull_tbl1       | nn      | f            |           0
+ notnull_tbl1_child | nn      | t            |           1
+(2 rows)
+
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+---now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1_child;
+DROP TABLE notnull_tbl1;
+-- Test the different Not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+   conrelid   |  conname  | convalidated | coninhcount 
+--------------+-----------+--------------+-------------
+ notnull_tbl1 | nn_parent | t            |           0
+ notnull_chld | nn_child  | t            |           1
+(2 rows)
+
+DROP TABLE notnull_tbl1 CASCADE;
+NOTICE:  drop cascades to table notnull_chld
+-- test to throw an error when trying to add another NOT NULL
+-- on the table column with INVALID NOT NULL constraint.
+CREATE TABLE notnull_tbl1 (a INTEGER);
+INSERT INTO notnull_tbl1 VALUES ( NULL );
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid;
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid no inherit; --error
+ERROR:  cannot change NO INHERIT status of NOT NULL constraint "nn1" on relation "notnull_tbl1"
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error can't change not-null status
+ERROR:  cannot change VALID status of NOT NULL constraint "nn1" on relation "notnull_tbl1"
+HINT:  You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn1"
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; --error can't change not-null status
+ERROR:  cannot change VALID status of NOT NULL constraint "nn1" on relation "notnull_tbl1"
+HINT:  You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn1"
+DROP TABLE notnull_tbl1;
+-- Test invalid not null on inheritance table.
+CREATE TABLE inh_parent ();
+CREATE TABLE inh_child (i int) INHERITS (inh_parent);
+CREATE TABLE inh_grandchild () INHERITS (inh_parent, inh_child);
+ALTER TABLE inh_parent ADD COLUMN i int;
+NOTICE:  merging definition of column "i" for child "inh_child"
+NOTICE:  merging definition of column "i" for child "inh_grandchild"
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --error
+ERROR:  cannot change VALID status of NOT NULL constraint "nn" on relation "inh_parent"
+HINT:  You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+ERROR:  cannot change VALID status of NOT NULL constraint "nn" on relation "inh_child"
+HINT:  You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+    conrelid    | conname | convalidated | coninhcount 
+----------------+---------+--------------+-------------
+ inh_parent     | nn      | f            |           0
+ inh_child      | nn      | f            |           1
+ inh_grandchild | nn      | f            |           2
+(3 rows)
+
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY RANGE (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --error
+ERROR:  not-null constraints on partitioned tables cannot be NO VALID
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a; --ok
+CREATE TABLE notnull_tbl1_1(a int, b int);
+INSERT INTO notnull_tbl1_1 DEFAULT VALUES;
+ALTER TABLE notnull_tbl1_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_1 FOR VALUES FROM (0) TO (10); --error
+ERROR:  column "a" in child table "notnull_tbl1_1" must be marked NOT NULL
+TRUNCATE notnull_tbl1_1;
+ALTER TABLE notnull_tbl1_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_1 FOR VALUES FROM (0) TO (10); --ok
+DROP TABLE notnull_tbl1, notnull_tbl1_1;
+DEALLOCATE get_nnconstraint_info;
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
 -- Verify that constraint names and NO INHERIT are properly considered when
 -- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
 -- and that conflicting cases are rejected.  Mind that table constraints
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 21ce4177de4..b708dded7d6 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -640,6 +640,98 @@ ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a;
 \d+ notnull_tbl1
 DROP TABLE notnull_tbl1;
 
+-------tests for NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM  pg_constraint
+WHERE conrelid  = ANY($1)
+ORDER BY 1;
+
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1),(NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; --ok
+\d+ notnull_tbl1
+
+INSERT INTO notnull_tbl1 VALUES (NULL, 4); --error
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1 ORDER BY a, b;
+
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+-- INHERITS table having NOT VALID NOT NULL constraints.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+-- Child table NOT NULL constraints should be valid.
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+---now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1_child;
+DROP TABLE notnull_tbl1;
+
+
+-- Test the different Not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+DROP TABLE notnull_tbl1 CASCADE;
+
+
+-- test to throw an error when trying to add another NOT NULL
+-- on the table column with INVALID NOT NULL constraint.
+CREATE TABLE notnull_tbl1 (a INTEGER);
+INSERT INTO notnull_tbl1 VALUES ( NULL );
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid;
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid no inherit; --error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error can't change not-null status
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; --error can't change not-null status
+DROP TABLE notnull_tbl1;
+
+
+-- Test invalid not null on inheritance table.
+CREATE TABLE inh_parent ();
+CREATE TABLE inh_child (i int) INHERITS (inh_parent);
+CREATE TABLE inh_grandchild () INHERITS (inh_parent, inh_child);
+ALTER TABLE inh_parent ADD COLUMN i int;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --error
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+
+
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY RANGE (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a; --ok
+
+CREATE TABLE notnull_tbl1_1(a int, b int);
+INSERT INTO notnull_tbl1_1 DEFAULT VALUES;
+ALTER TABLE notnull_tbl1_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_1 FOR VALUES FROM (0) TO (10); --error
+TRUNCATE notnull_tbl1_1;
+
+ALTER TABLE notnull_tbl1_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_1 FOR VALUES FROM (0) TO (10); --ok
+
+DROP TABLE notnull_tbl1, notnull_tbl1_1;
+
+DEALLOCATE get_nnconstraint_info;
+
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
+
+
 -- Verify that constraint names and NO INHERIT are properly considered when
 -- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
 -- and that conflicting cases are rejected.  Mind that table constraints
-- 
2.34.1

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 20 Mar 2025 16:52:57 +0100

> * partitioned table can not have NOT NULL NOT VALID.

I'm not sure I understand what you're saying here.  I think a
partitioned table can be allowed to have a NOT VALID constraint.  BUT if
it does, then all the children must have a corresponding constraint.  The
constraint on children may be valid or may be invalid; the parent
doesn't force the issue one way or the other.  But it has to exist.
Also, if you run ALTER TABLE VALIDATE CONSTRAINT on the parent, then at
that point you have to validate that all those corresponding constraints on
the children are also validated.

> allowed, it will error out, give you hints.

I think we discussed this already.  If you say 
ALTER TABLE .. ALTER COLUMN .. SET NOT NULL
and an invalid constraint exists, then we can simply validate that
constraint.

However, if you say
ALTER TABLE .. ADD CONSTRAINT foobar NOT NULL col;
and an invalid constraint exists whose name is different from foobar,
then we should raise an error, because the user's requirement that the
constraint is named foobar cannot be satisfied.  If the constraint is
named foobar, OR if the user doesn't specify a constraint name
  ALTER TABLE .. ADD NOT NULL col;
then it's okay to validate that constraint without raising an error.
The important thing being that the user requirement is satisfied.

> * it can only be added using ALTER TABLE, not with CREATE TABLE (a

I think the issue of adding constraints with NOT VALID during CREATE
TABLE is the topic of another thread.  We already silently ignore the
NOT VALID markers during CREATE TABLE for other types of constraints.

>   Conversely, if attinvalidnotnull is true, then attnotnull should be false.

I don't like this.  It seems baroque and it will break existing
applications, because they currently query for attnotnull and assume
that inserting a null value will work, but in reality it will fail
because attinvalidnotnull is true (meaning an invalid constraint exists,
which prevents inserting nulls).

I think the idea should be: attnotnull means that a constraint exists;
it doesn't imply anything regarding the constraint being valid or not.
attnotnullvalid will indicate whether the constraint is valid; this
column can only be true if attnotnull is already true.

> * an invalid not-null cannot be used while adding a primary key.

Check.

>   VALIDATE the not-null constraint to check if this column exists NULL

Check.

---

From: jian he <[email-masked]>
Date: Fri, 21 Mar 2025 22:36:32 +0800

> the children are also validated.

* if partitioned table have valid not-null, then partition with
invalid not-null can not attach to the partition tree.
  if partitioned table have not valid not-null, we *can* attach a
valid not-null to the partition tree.
  (inheritance hierarchy behaves the same).
this part does not require a lot of code changes.
However, to make the pg_dump working with partitioned table we need to
tweak AdjustNotNullInheritance a little bit.

> The important thing being that the user requirement is satisfied.
i changed this accordingly.
ALTER TABLE .. ALTER COLUMN .. SET NOT NULL
will validate not-null and set attnotnull, attinvalidnotnull accordingly.

> column can only be true if attnotnull is already true.
i basically model NOT NULL NOT VALID == CHECK (x IS NOT NULL).
i think your idea may need more refactoring?
all the "if (attr->attnotnull" need change to "if (attr->attnotnull &&
attr->attnotnullvalid)"
or am i missing something?

Anyway, I will just share my idea first, and will explore your idea later.

in my attached patch, you will only create an not-null not valid
pg_constraint entry
If `if (constr->contype == CONSTR_NOTNULL && constr->skip_validation)`
in ATAddCheckNNConstraint conditions are satisfied.

imho, my approach is less bug-prone, almost no need to refactor current code.
we can even add a assert in InsertPgAttributeTuples:
Assert(!attrs->attinvalidnotnull);

new patch attached:
* Rushabh's pg_dump relation code incorporated into a single one patch.
* pg_dump works fine, mainly by tweak AdjustNotNullInheritance
following the same logic in MergeConstraintsIntoExisting.
if not do it, pg_constraint.conislocal meta info will be wrong.
From 4c60297c019bebfd5cdcfbfdde3b4f57e168626c Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Fri, 21 Mar 2025 22:10:42 +0800
Subject: [PATCH v4 1/1] NOT NULL NOT VALID
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* it will logically be equivalent to CHECK(x IS NOT NULL) NOT VALID.
* it can only be added using ALTER TABLE. entry point is
 ATAddCheckNNConstraint. when we add a constraint, it will either valid or not valid.
``
    if (constr->contype == CONSTR_NOTNULL)
    {
        if (!constr->skip_validation)
            set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
        else
            set_attinvalidnotnull(rel, ccon->attnum, true);
    }
``
we will only change the state attinvalidnotnull in QueueNNConstraintValidation,
which we will validate the constraint, so attinvalidnotnull should be false.
when we insert a pg_attribute tuple, attinvalidnotnull will always false,
we can even add a assert in InsertPgAttributeTuples: Assert(!attrs->attinvalidnotnull);

* if attnotnull is true, then attinvalidnotnull should be false.
  Conversely, if attinvalidnotnull is true, then attnotnull should be false.
* an invalid not-null cannot be used while adding a primary key.
* if attinvalidnotnull is true, this column can not accept NULL values,
  but the existing column value may contain NULLs, we need to
  VALIDATE the not-null constraint to check if this column exists NULL values or not.
* if partitioned table have valid not-null, then can not attach a not valid to partition tree.
  if partitioned table have not valid not-null, we *can* attach a valid not-null to partition tree.
  (inheritance hierarchy behave the same).

  i tested loaclally, pg_dump will works fine with partitioned table, mainly by tweak
  AdjustNotNullInheritance.
  normal table not null not valid pg_dump also works fine.

discussion: https://postgr.es/m/CAGPqQf0KitkNack4F5CFkFi-9Dqvp29Ro=EpcWt=[email-masked]
---
 doc/src/sgml/catalogs.sgml                |  10 +
 doc/src/sgml/ref/alter_table.sgml         |   2 +
 src/backend/access/common/tupdesc.c       |  11 ++
 src/backend/bootstrap/bootstrap.c         |   1 +
 src/backend/catalog/heap.c                |  11 +-
 src/backend/catalog/pg_constraint.c       |  57 +++++-
 src/backend/commands/tablecmds.c          | 214 ++++++++++++++++++++--
 src/backend/executor/execMain.c           | 130 ++++++++-----
 src/backend/parser/gram.y                 |   4 +-
 src/backend/parser/parse_utilcmd.c        |  14 +-
 src/backend/utils/cache/relcache.c        |   3 +
 src/bin/pg_dump/pg_dump.c                 | 155 +++++++++++++++-
 src/bin/pg_dump/pg_dump.h                 |   1 +
 src/bin/psql/describe.c                   |   9 +-
 src/include/access/tupdesc.h              |   1 +
 src/include/catalog/pg_attribute.h        |   3 +
 src/include/catalog/pg_constraint.h       |  10 +-
 src/test/regress/expected/constraints.out | 169 +++++++++++++++++
 src/test/regress/sql/constraints.sql      | 114 ++++++++++++
 19 files changed, 827 insertions(+), 92 deletions(-)

+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attinvalidnotnull</structfield> <type>bool</type>
+      </para>
+      <para>
+       This column has a attinvalidnotnull not-null constraint.
+       If <structfield>attnotnull</structfield> is true, this has to be false.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>atthasdef</structfield> <type>bool</type>
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 4f15b89a98f..75d90654275 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -243,6 +243,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       entire table; however, if a valid <literal>CHECK</literal> constraint is
       found which proves no <literal>NULL</literal> can exist, then the
       table scan is skipped.
+      If a column is marked as <literal>NOT NULL NOT VALID</literal>,
+      <literal>SET NOT NULL</literal> will change it to a validated <literal>NOT NULL</literal> constraint.
      </para>
 
      <para>
 
 		att->attnotnull = false;
 
 		att->attnotnull = false;

 
 		att->attnotnull = false;

 	att->attnotnull = false;
 
 	att->attnotnull = false;

 	if (nullness == BOOTCOL_NULL_FORCE_NOT_NULL)
 	{
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..9b2315a27e7 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -753,6 +753,8 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attstorage - 1] = CharGetDatum(attrs->attstorage);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcompression - 1] = CharGetDatum(attrs->attcompression);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attnotnull - 1] = BoolGetDatum(attrs->attnotnull);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attinvalidnotnull - 1] = BoolGetDatum(attrs->attinvalidnotnull);
+		Assert(!attrs->attinvalidnotnull); /*test demo only*/
 		slot[slotCount]->tts_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(attrs->atthasdef);
 		slot[slotCount]->tts_values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(attrs->atthasmissing);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(attrs->attidentity);
@@ -2621,12 +2623,17 @@ AddRelationNewConstraints(Relation rel,
 						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						errmsg("not-null constraints are not supported on virtual generated columns"));
 
+			if (cdef->initially_valid)
+				Assert(!cdef->skip_validation);
+			else
+				Assert(cdef->skip_validation);
+
 			/*
 			 * If the column already has a not-null constraint, we don't want
 			 * to add another one; just adjust inheritance status as needed.
 			 */
-			if (AdjustNotNullInheritance(RelationGetRelid(rel), colnum,
-										 is_local, cdef->is_no_inherit))
+			if (AdjustNotNullInheritance(rel, colnum,
+										 is_local, cdef->is_no_inherit, cdef->skip_validation))
 				continue;
 
 			/*
index ac80652baf2..9b4d0427877 100644

 
 
 /*
@@ -728,12 +730,13 @@ extractNotNullColumn(HeapTuple constrTup)
  * nothing if it's already true; otherwise we increment coninhcount by 1.
  */
 bool
-AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
-						 bool is_local, bool is_no_inherit)
+AdjustNotNullInheritance(Relation rel, AttrNumber attnum,
+						 bool is_local, bool is_no_inherit, bool is_notvalid)
 {
 	HeapTuple	tup;
+	Oid	relid = RelationGetRelid(rel);
 
@@ -753,6 +756,36 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
 
+		/*
+		 * Throw an error if we want change the NOT NULL constraint from NOT
+		 * VALID to VALID.
+		 */
+		if (!is_notvalid && !conform->convalidated)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("cannot change VALID status of NOT NULL constraint \"%s\" on relation \"%s\"",
+						   NameStr(conform->conname), get_rel_name(relid)),
+					errhint("You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint \"%s\"",
+							NameStr(conform->conname)));
+
+		/* VALID TO NOT VALID seems no usage, so just issue a warning */
+		if (is_notvalid && conform->convalidated)
+		{
+			if (is_local)
+				ereport(WARNING,
+						errmsg("NOT NULL constraint \"%s\" on relation \"%s\" is already valid",
+							NameStr(conform->conname), get_rel_name(relid)));
+			else if (rel->rd_rel->relispartition)
+			{
+				/*
+				* In case of partitions, an inherited not null constraint
+				* is never considered local. See MergeConstraintsIntoExisting also.				
+				*/
+				conform->conislocal = false;
+				changed =true;
+			}
+		}
+
 		if (!is_local)
 		{
 			if (pg_add_s16_overflow(conform->coninhcount, 1,
@@ -788,9 +821,10 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
  * This is seldom needed, so we just scan pg_constraint each time.
  *
  * 'include_noinh' determines whether to include NO INHERIT constraints or not.
+ * 'include_notvalid' determines whether to include NO VALID constraints or not.
  */
 List *
-RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
+RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh, bool include_notvalid)
 {
 	List	   *notnulls = NIL;
 	Relation	constrRel;
@@ -816,6 +850,9 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
 		if (conForm->connoinherit && !include_noinh)
 			continue;

 		if (cooked)
@@ -830,7 +867,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
 			cooked->attnum = colnum;
 			cooked->expr = NULL;
@@ -850,7 +887,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
index 129c97fdf28..94acf07b63d 100644
 
 		/*
 
@@ -7729,6 +7732,44 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
 	return address;
 }
 
+/*
+ * update pg_attribute.attinvalidnotnull
+*/
+static void
@@ -7845,7 +7886,7 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 
 	/* See if there's already a constraint */
-	tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
+	tuple = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum, true);
 	if (HeapTupleIsValid(tuple))
 	{
 		Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
@@ -7880,6 +7921,16 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 			conForm->conislocal = true;
 			changed = true;
 		}
+		else if (!conForm->convalidated)
+		{
+
+			/*
+			 * flip attnotnull and convalidated, and also validate the
+			 * constraint.
+			 */
+			return ATExecValidateConstraint(wqueue, rel, NameStr(conForm->conname),
+											recurse, recursing, lockmode);
+		}
 
 		if (changed)
 		{
@@ -9387,6 +9438,22 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
 	{
 		AlterTableCmd *newcmd;
 		Constraint *nnconstr;
+		HeapTuple	tuple;
+
+		tuple = findNotNullConstraint(RelationGetRelid(rel), strVal(lfirst(lc)), true);
+		if (tuple != NULL)
+		{
+			Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
+			if (!conForm->convalidated)
+				ereport(ERROR,
+						errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+						errmsg("primary key require an validated NOT NULL constraint"),
+						errdetail("Column \"%s\" of table \"%s\" is marked as NOT VALID NOT NULL constraint",
+							      strVal(lfirst(lc)), RelationGetRelationName(rel)),
+						errhint("You may try ALTER TABLE VALIDATE CONSTRAINT to validate constraint \"%s\"",
+								NameStr(conForm->conname)));
+			heap_freetuple(tuple);
+		}

@@ -9762,11 +9829,19 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			constr->conname = ccon->name;
 
 		/*
-		 * If adding a not-null constraint, set the pg_attribute flag and tell
+		 * If adding a valid not-null constraint, set the pg_attribute flag and tell
 		 * phase 3 to verify existing rows, if needed.
+		 * However if we are adding a invalid not-null constraint, then we only
+		 * need set the pg_attribute.attinvalidnotnull.  phase 3 don't need to
+		 * verify existing rows.
 		 */
 		if (constr->contype == CONSTR_NOTNULL)
-			set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
+		{
+			if (!constr->skip_validation)
+				set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
+			else
+				set_attinvalidnotnull(rel, ccon->attnum, true);
+		}
 
 		ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
 	}
@@ -12176,7 +12251,7 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
 				HeapTuple	childtup;
 				Form_pg_constraint childcon;
 
-				childtup = findNotNullConstraint(childoid, colName);
+				childtup = findNotNullConstraint(childoid, colName, false);
 				if (!childtup)
 					elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
 						 colName, childoid);
@@ -12367,10 +12442,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,

 	if (!con->conenforced)
@@ -12389,6 +12465,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 
 		ObjectAddressSet(address, ConstraintRelationId, con->oid);
 	}
@@ -12605,6 +12686,116 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 	heap_freetuple(copyTuple);
 }
 
+/*
+	 * set attinvalidnotnull to false. set_attnotnull already tell phase 3 to
+	 * vertify not-null status.
@@ -13549,7 +13740,7 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
@@ -16813,8 +17004,9 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 			{
 				HeapTuple	contup;

 
 
-				/*

-					/*

+
+/*
index 896a7f2c59b..59d689640c6 100644
@@ -329,6 +329,18 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 			cd->is_not_null = true;
 			break;
 		}
+		if (!cxt.isforeign && !nn->initially_valid)
+		{
+			nn->initially_valid = true;
+			nn->skip_validation = false;
+			/*
+			 * we do not support NOT NULL NOT VALID for column constraint,
+			 * nn->conname should not be NULL.
+			*/
+			ereport(WARNING,
+					errcode(ERRCODE_WARNING),
+					errmsg("Ignoring NOT VALID flag for NOT NULL constraint \"%s\"", nn->conname));
+		}
 	}
 
 	/*
@@ -1291,7 +1303,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		List	   *lst;

diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9f54a9e72b7..d5cd93e55c3 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -593,6 +593,8 @@ RelationBuildTupleDesc(Relation relation)
 		/* Update constraint/default info */
 		if (attp->attnotnull)
 			constr->has_not_null = true;
+		if (attp->attinvalidnotnull)
+			constr->has_invalid_not_null = true;
 		if (attp->attgenerated == ATTRIBUTE_GENERATED_STORED)
 			constr->has_generated_stored = true;
 		if (attp->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
@@ -678,6 +680,7 @@ RelationBuildTupleDesc(Relation relation)
 	 * Set up constraint/default info
 	 */
 	if (constr->has_not_null ||
+		constr->has_invalid_not_null ||
 		constr->has_generated_stored ||
 		constr->has_generated_virtual ||
 		ndef > 0 ||
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 428ed2d60fc..1d67dcac124 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8884,6 +8884,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	PQExpBuffer q = createPQExpBuffer();
 	PQExpBuffer tbloids = createPQExpBuffer();
 	PQExpBuffer checkoids = createPQExpBuffer();
+	PQExpBuffer invalidnotnulloids = createPQExpBuffer();
 	PGresult   *res;
 	int			ntups;
 	int			curtblindx;
@@ -8903,6 +8904,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_notnull_name;
 	int			i_notnull_noinherit;
 	int			i_notnull_islocal;
+	int			i_notnull_validated;
 	int			i_attoptions;
 	int			i_attcollation;
 	int			i_attcompression;
@@ -8998,12 +9000,14 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		appendPQExpBufferStr(q,
 							 "co.conname AS notnull_name,\n"
 							 "co.connoinherit AS notnull_noinherit,\n"
-							 "co.conislocal AS notnull_islocal,\n");
+							 "co.conislocal AS notnull_islocal,\n"
+							 "co.convalidated as notnull_validated,\n");
 	else
 		appendPQExpBufferStr(q,
 							 "CASE WHEN a.attnotnull THEN '' ELSE NULL END AS notnull_name,\n"
 							 "false AS notnull_noinherit,\n"
-							 "a.attislocal AS notnull_islocal,\n");
+							 "a.attislocal AS notnull_islocal,\n"
+							 "true as notnull_validated\n");
 
 	if (fout->remoteVersion >= 140000)
 		appendPQExpBufferStr(q,
@@ -9078,6 +9082,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_notnull_name = PQfnumber(res, "notnull_name");
 	i_notnull_noinherit = PQfnumber(res, "notnull_noinherit");
 	i_notnull_islocal = PQfnumber(res, "notnull_islocal");
+	i_notnull_validated = PQfnumber(res, "notnull_validated");
 	i_attoptions = PQfnumber(res, "attoptions");
 	i_attcollation = PQfnumber(res, "attcollation");
 	i_attcompression = PQfnumber(res, "attcompression");
@@ -9094,6 +9099,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * r is handled by the inner loop.
 	 */
 	curtblindx = -1;
+	appendPQExpBufferChar(invalidnotnulloids, '{');
 	for (int r = 0; r < ntups;)
 	{
 		Oid			attrelid = atooid(PQgetvalue(res, r, i_attrelid));
@@ -9145,6 +9151,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		tbinfo->notnull_constrs = (char **) pg_malloc(numatts * sizeof(char *));
 		tbinfo->notnull_noinh = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->notnull_islocal = (bool *) pg_malloc(numatts * sizeof(bool));
+		tbinfo->notnull_validated = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->attrdefs = (AttrDefInfo **) pg_malloc(numatts * sizeof(AttrDefInfo *));
 		hasdefaults = false;
 
@@ -9168,12 +9175,33 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen));
 			tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign));
 			tbinfo->attislocal[j] = (PQgetvalue(res, r, i_attislocal)[0] == 't');
+			tbinfo->notnull_validated[j] = (PQgetvalue(res, r, i_notnull_validated)[0] == 't');
 
-			/* Handle not-null constraint name and flags */
-			determineNotNullFlags(fout, res, r,
-								  tbinfo, j,
-								  i_notnull_name, i_notnull_noinherit,
-								  i_notnull_islocal);
+			if (tbinfo->notnull_validated[j])
+			{
+				/* Handle not-null constraint name and flags */
+				determineNotNullFlags(fout, res, r,
+									tbinfo, j,
+									i_notnull_name, i_notnull_noinherit,
+									i_notnull_islocal);
+			}
+			else
+			{
+				tbinfo->notnull_constrs[j] = NULL;
+
+				/*
+				 * if column notnull_validated is false, it can also mean that
+				 * column don't have not-null constraint at all. if column have
+				 * NOT NULL NOT VALID then Add the entry into invalidnotnulloids
+				 * list so it can dumped as seperate constraint.
+				*/
+				if (!PQgetisnull(res, r, i_notnull_name))
+				{
+					if (invalidnotnulloids->len > 1)
+						appendPQExpBufferChar(invalidnotnulloids, ',');
+					appendPQExpBuffer(invalidnotnulloids, "%u", tbinfo->dobj.catId.oid);
+				}
+			}
 
 			tbinfo->attoptions[j] = pg_strdup(PQgetvalue(res, r, i_attoptions));
 			tbinfo->attcollation[j] = atooid(PQgetvalue(res, r, i_attcollation));
@@ -9193,6 +9221,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			appendPQExpBuffer(tbloids, "%u", tbinfo->dobj.catId.oid);
 		}
 	}
+	appendPQExpBufferChar(invalidnotnulloids, '}');
 
 	PQclear(res);
 
@@ -9326,6 +9355,110 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
+	/*
+	 * Get info about table NOT NULL NOT VALID constraints.  This is skipped for
+	 * a data-only dump, as it is only needed for table schemas.
+	 */
+	if (dopt->dumpSchema && invalidnotnulloids->len > 2)
+	{
+		ConstraintInfo *constrs;
+		int         numConstrs;
+		int         i_tableoid;
+		int         i_oid;
+		int         i_conrelid;
+		int         i_conname;
+		int         i_consrc;
+		int         i_conislocal;
+		int         i_convalidated;
+
+		pg_log_info("finding table invalid not null constraints");
+
+		resetPQExpBuffer(q);
+		appendPQExpBuffer(q,
+						  "SELECT c.tableoid, c.oid, conrelid, conname, "
+						  "pg_catalog.pg_get_constraintdef(c.oid) AS consrc, "
+						  "conislocal, convalidated "
+						  "FROM unnest('%s'::pg_catalog.oid[]) AS src(tbloid)\n"
+						  "JOIN pg_catalog.pg_constraint c ON (src.tbloid = c.conrelid)\n"
+						  "WHERE contype = 'n' AND NOT convalidated "
+						  "ORDER BY c.conrelid, c.conname",
+						  invalidnotnulloids->data);
+
+		res = ExecuteSqlQuery(fout, q->data, PGRES_TUPLES_OK);
+
+		numConstrs = PQntuples(res);
+		constrs = (ConstraintInfo *) pg_malloc(numConstrs * sizeof(ConstraintInfo));
+
+		i_tableoid = PQfnumber(res, "tableoid");
+		i_oid = PQfnumber(res, "oid");
+		i_conrelid = PQfnumber(res, "conrelid");
+		i_conname = PQfnumber(res, "conname");
+		i_consrc = PQfnumber(res, "consrc");
+		i_conislocal = PQfnumber(res, "conislocal");
+		i_convalidated = PQfnumber(res, "convalidated");
+
+		/* As above, this loop iterates once per table, not once per row */
+		curtblindx = -1;
+		for (int j = 0; j < numConstrs;)
+		{
+			Oid         conrelid = atooid(PQgetvalue(res, j, i_conrelid));
+			TableInfo  *tbinfo = NULL;
+			int         numcons;
+
+			/* Count rows for this table */
+			for (numcons = 1; numcons < numConstrs - j; numcons++)
+				if (atooid(PQgetvalue(res, j + numcons, i_conrelid)) != conrelid)
+					break;
+
+			/*
+			 * Locate the associated TableInfo; we rely on tblinfo[] being in
+			 * OID order.
+			 */
+			while (++curtblindx < numTables)
+			{
+				tbinfo = &tblinfo[curtblindx];
+				if (tbinfo->dobj.catId.oid == conrelid)
+					break;
+			}
+			if (curtblindx >= numTables)
+				pg_fatal("unrecognized table OID %u", conrelid);
+
+			tbinfo->checkexprs = constrs + j;
+
+			for (int c = 0; c < numcons; c++, j++)
+			{
+				bool        validated = PQgetvalue(res, j, i_convalidated)[0] == 't';
+
+				Assert(!validated);
+				constrs[j].dobj.objType = DO_CONSTRAINT;
+				constrs[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
+				constrs[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
+				AssignDumpId(&constrs[j].dobj);
+				constrs[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_conname));
+				constrs[j].dobj.namespace = tbinfo->dobj.namespace;
+				constrs[j].contable = tbinfo;
+				constrs[j].condomain = NULL;
+				constrs[j].contype = 'n';
+				constrs[j].condef = pg_strdup(PQgetvalue(res, j, i_consrc));
+				constrs[j].confrelid = InvalidOid;
+				constrs[j].conindex = 0;
+				constrs[j].condeferrable = false;
+				constrs[j].condeferred = false;
+				constrs[j].conislocal = (PQgetvalue(res, j, i_conislocal)[0] == 't');
+
+				/*
+		         * NOT NULL NOT VALID must dumped separately as an ALTER TABLE
+				 * ADD CONSTRAINT entry. because CREATE TABLE can not create
+				 * invalid not null constraint.
+				 */
+				constrs[j].separate = !validated;
+
+				constrs[j].dobj.dump = tbinfo->dobj.dump;
+			}
+		}
+		PQclear(res);
+	}
+
 	/*
 	 * Get info about table CHECK constraints.  This is skipped for a
 	 * data-only dump, as it is only needed for table schemas.
@@ -16620,6 +16753,8 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
 					if (print_notnull)
 					{
+						Assert(tbinfo->notnull_validated[j]);
+
 						if (tbinfo->notnull_constrs[j][0] == '\0')
 							appendPQExpBufferStr(q, " NOT NULL");
 						else
@@ -17939,9 +18074,9 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 									  .createStmt = q->data,
 									  .dropStmt = delq->data));
 	}
-	else if (coninfo->contype == 'c' && tbinfo)
+	else if ((coninfo->contype == 'c' || coninfo->contype == 'n') && tbinfo)
 	{
-		/* CHECK constraint on a table */
+		/* CHECK and INVALID NOT NULL constraint on a table */
 
 		/* Ignore if not to be dumped separately, or if it was inherited */
 		if (coninfo->separate && coninfo->conislocal)
@@ -17965,7 +18100,7 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 							 ARCHIVE_OPTS(.tag = tag,
 										  .namespace = tbinfo->dobj.namespace->dobj.name,
 										  .owner = tbinfo->rolname,
-										  .description = "CHECK CONSTRAINT",
+										  .description = coninfo->contype == 'c' ? "CHECK CONSTRAINT" : "CONSTRAINT",
 										  .section = SECTION_POST_DATA,
 										  .createStmt = q->data,
 										  .dropStmt = delq->data));
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bbdb30b5f54..e0c9bbd64a2 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -367,6 +367,7 @@ typedef struct _tableInfo
 									 * (pre-v17) */
 	bool	   *notnull_noinh;	/* NOT NULL is NO INHERIT */
 	bool	   *notnull_islocal;	/* true if NOT NULL has local definition */
+	bool	   *notnull_validated;	/* true if NOT NULL is validated */
 	struct _attrDefInfo **attrdefs; /* DEFAULT expressions */
 	struct _constraintInfo *checkexprs; /* CHECK constraints */
 	bool		needs_override; /* has GENERATED ALWAYS AS IDENTITY */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..09f8f92a59c 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3114,7 +3114,8 @@ describeOneTableDetails(const char *schemaname,
 		{
 			printfPQExpBuffer(&buf,
 							  "SELECT c.conname, a.attname, c.connoinherit,\n"
-							  "  c.conislocal, c.coninhcount <> 0\n"
+							  "  c.conislocal, c.coninhcount <> 0,\n"
+							  "  c.convalidated\n"
 							  "FROM pg_catalog.pg_constraint c JOIN\n"
 							  "  pg_catalog.pg_attribute a ON\n"
 							  "    (a.attrelid = c.conrelid AND a.attnum = c.conkey[1])\n"
@@ -3137,14 +3138,16 @@ describeOneTableDetails(const char *schemaname,
 			{
 				bool		islocal = PQgetvalue(result, i, 3)[0] == 't';
 				bool		inherited = PQgetvalue(result, i, 4)[0] == 't';
+				bool		validated = PQgetvalue(result, i, 5)[0] == 't';

 
 
index 6da164e7e4d..fe27dde16fa 100644
 
-extern bool AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
+extern bool AdjustNotNullInheritance(Relation rel, AttrNumber attnum,
 
 extern void RemoveConstraintById(Oid conId);
 extern void RenameConstraintById(Oid conId, const char *newname);
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..d57d7e09984 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -896,6 +896,175 @@ Not-null constraints:
     "foobar" NOT NULL "a"
 
 DROP TABLE notnull_tbl1;
+-------tests for NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM  pg_constraint
+WHERE conrelid  = ANY($1)
+ORDER BY 1;
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1),(NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error
+ERROR:  column "a" of relation "notnull_tbl1" contains null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; --ok
+\d+ notnull_tbl1
+                               Table "public.notnull_tbl1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+Not-null constraints:
+    "nn" NOT NULL "a" NO VALID
+
+INSERT INTO notnull_tbl1 VALUES (NULL, 4); --error, invalid not-null constraint forbiden new null values
+ERROR:  null value in column "a" of relation "notnull_tbl1" violates not-null constraint
+DETAIL:  Failing row contains (null, 4).
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid no inherit; --error
+ERROR:  cannot change NO INHERIT status of NOT NULL constraint "nn" on relation "notnull_tbl1"
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error can't change not-null status
+ERROR:  cannot change VALID status of NOT NULL constraint "nn" on relation "notnull_tbl1"
+HINT:  You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; --validate it then error out
+ERROR:  column "a" of relation "notnull_tbl1" contains null values
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1 ORDER BY a, b;
+  a  | b 
+-----+---
+ 100 | 1
+ 300 | 3
+(2 rows)
+
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+ERROR:  primary key require an validated NOT NULL constraint
+DETAIL:  Column "a" of table "notnull_tbl1" is marked as NOT VALID NOT NULL constraint
+HINT:  You may try ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+-- however child table NOT NULL constraints should be valid.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+NOTICE:  merging column "a" with inherited definition
+NOTICE:  merging column "b" with inherited definition
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+      conrelid      | conname | convalidated | coninhcount 
+--------------------+---------+--------------+-------------
+ notnull_tbl1       | nn      | f            |           0
+ notnull_tbl1_child | nn      | t            |           1
+(2 rows)
+
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+---now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1_child;
+DROP TABLE notnull_tbl1;
+-- Test the different not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+-- parent have valid not null constraint then child table cannot have invalid one
+CREATE TABLE notnull_chld0 (a int);
+ALTER TABLE notnull_chld0 ADD CONSTRAINT nn_chld0 NOT NULL a;
+ALTER TABLE notnull_tbl1 INHERIT notnull_chld0; --error
+ERROR:  column "a" in child table "notnull_tbl1" must be marked NOT NULL
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+   conrelid   |  conname  | convalidated | coninhcount 
+--------------+-----------+--------------+-------------
+ notnull_tbl1 | nn_parent | t            |           0
+ notnull_chld | nn_child  | t            |           1
+(2 rows)
+
+DROP TABLE notnull_chld0;
+DROP TABLE notnull_chld;
+DROP TABLE notnull_tbl1;
+-- Test invalid not null on inheritance table.
+CREATE TABLE inh_parent ();
+CREATE TABLE inh_child (i int) INHERITS (inh_parent);
+CREATE TABLE inh_grandchild () INHERITS (inh_parent, inh_child);
+ALTER TABLE inh_parent ADD COLUMN i int;
+NOTICE:  merging definition of column "i" for child "inh_child"
+NOTICE:  merging definition of column "i" for child "inh_grandchild"
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i; --error
+ERROR:  cannot change VALID status of NOT NULL constraint "nn" on relation "inh_parent"
+HINT:  You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+ERROR:  cannot change VALID status of NOT NULL constraint "nn" on relation "inh_child"
+HINT:  You may need to use ALTER TABLE VALIDATE CONSTRAINT to validate constraint "nn"
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+    conrelid    | conname | convalidated | coninhcount 
+----------------+---------+--------------+-------------
+ inh_parent     | nn      | f            |           0
+ inh_child      | nn      | f            |           1
+ inh_grandchild | nn      | f            |           2
+(3 rows)
+
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --ok
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be valid now.
+    conrelid    | conname | convalidated | coninhcount 
+----------------+---------+--------------+-------------
+ inh_parent     | nn      | t            |           0
+ inh_child      | nn      | t            |           1
+ inh_grandchild | nn      | t            |           2
+(3 rows)
+
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+DROP TABLE notnull_tbl1;
+ERROR:  table "notnull_tbl1" does not exist
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --ok
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES IN (1,2);
+CREATE TABLE notnull_tbl1_2(a int, CONSTRAINT nn2 NOT NULL a, b int);
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_2 FOR VALUES IN (3,4);
+CREATE TABLE notnull_tbl1_3(a int, b int);
+INSERT INTO notnull_tbl1_3 values(NULL,1);
+ALTER TABLE notnull_tbl1_3 add CONSTRAINT nn3 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_3 FOR VALUES IN (NULL,5);
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+    conrelid    |   conname   | convalidated | coninhcount 
+----------------+-------------+--------------+-------------
+ notnull_tbl1   | notnull_con | f            |           0
+ notnull_tbl1_1 | notnull_con | t            |           1
+ notnull_tbl1_2 | nn2         | t            |           1
+ notnull_tbl1_3 | nn3         | f            |           1
+(4 rows)
+
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --error, table already have null values
+ERROR:  column "a" of relation "notnull_tbl1_3" contains null values
+ALTER TABLE notnull_tbl1_3 VALIDATE CONSTRAINT nn3; --error
+ERROR:  column "a" of relation "notnull_tbl1_3" contains null values
+TRUNCATE notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --OK
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+    conrelid    |   conname   | convalidated | coninhcount 
+----------------+-------------+--------------+-------------
+ notnull_tbl1   | notnull_con | t            |           0
+ notnull_tbl1_1 | notnull_con | t            |           1
+ notnull_tbl1_2 | nn2         | t            |           1
+ notnull_tbl1_3 | nn3         | t            |           1
+(4 rows)
+
+DROP TABLE notnull_tbl1;
+-----partitioned table have not-null, then the partitions can not be NOT NULL NOT VALID.
+CREATE TABLE pp_nn (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE pp_nn ADD CONSTRAINT pp_nn_notnull NOT NULL a;
+CREATE TABLE pp_nn_1(a int, b int);
+ALTER TABLE pp_nn_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --error
+ERROR:  column "a" in child table "pp_nn_1" must be marked NOT NULL
+ALTER TABLE pp_nn_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --ok
+DROP TABLE pp_nn;
+DEALLOCATE get_nnconstraint_info;
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
 -- Verify that constraint names and NO INHERIT are properly considered when
 -- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
 -- and that conflicting cases are rejected.  Mind that table constraints
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 21ce4177de4..976e8e6941c 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -640,6 +640,120 @@ ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a;
 \d+ notnull_tbl1
 DROP TABLE notnull_tbl1;
 
+-------tests for NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM  pg_constraint
+WHERE conrelid  = ANY($1)
+ORDER BY 1;
+
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1),(NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; --ok
+\d+ notnull_tbl1
+
+INSERT INTO notnull_tbl1 VALUES (NULL, 4); --error, invalid not-null constraint forbiden new null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a not valid no inherit; --error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; --error can't change not-null status
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL; --validate it then error out
+
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+DELETE FROM notnull_tbl1 WHERE b = 2;
+SELECT * FROM notnull_tbl1 ORDER BY a, b;
+
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+-- however child table NOT NULL constraints should be valid.
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS(notnull_tbl1);
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+---now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1_child;
+DROP TABLE notnull_tbl1;
+
+
+-- Test the different not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+
+-- parent have valid not null constraint then child table cannot have invalid one
+CREATE TABLE notnull_chld0 (a int);
+ALTER TABLE notnull_chld0 ADD CONSTRAINT nn_chld0 NOT NULL a;
+ALTER TABLE notnull_tbl1 INHERIT notnull_chld0; --error
+
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+DROP TABLE notnull_chld0;
+DROP TABLE notnull_chld;
+DROP TABLE notnull_tbl1;
+
+
+-- Test invalid not null on inheritance table.
+CREATE TABLE inh_parent ();
+CREATE TABLE inh_child (i int) INHERITS (inh_parent);
+CREATE TABLE inh_grandchild () INHERITS (inh_parent, inh_child);
+ALTER TABLE inh_parent ADD COLUMN i int;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i; --error
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --ok
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be valid now.
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+
+
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+DROP TABLE notnull_tbl1;
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --ok
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES IN (1,2);
+CREATE TABLE notnull_tbl1_2(a int, CONSTRAINT nn2 NOT NULL a, b int);
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_2 FOR VALUES IN (3,4);
+
+CREATE TABLE notnull_tbl1_3(a int, b int);
+INSERT INTO notnull_tbl1_3 values(NULL,1);
+ALTER TABLE notnull_tbl1_3 add CONSTRAINT nn3 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_3 FOR VALUES IN (NULL,5);
+
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --error, table already have null values
+ALTER TABLE notnull_tbl1_3 VALIDATE CONSTRAINT nn3; --error
+
+TRUNCATE notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --OK
+
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+DROP TABLE notnull_tbl1;
+
+-----partitioned table have not-null, then the partitions can not be NOT NULL NOT VALID.
+CREATE TABLE pp_nn (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE pp_nn ADD CONSTRAINT pp_nn_notnull NOT NULL a;
+
+CREATE TABLE pp_nn_1(a int, b int);
+ALTER TABLE pp_nn_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --error
+ALTER TABLE pp_nn_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --ok
+DROP TABLE pp_nn;
+
+DEALLOCATE get_nnconstraint_info;
+
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
+
+
 -- Verify that constraint names and NO INHERIT are properly considered when
 -- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
 -- and that conflicting cases are rejected.  Mind that table constraints
-- 
2.34.1

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 21 Mar 2025 15:50:37 +0100

> invalid not-null can not attach to the partition tree.

Correct.

> valid not-null to the partition tree.

Also correct.

>   (inheritance hierarchy behaves the same).

Good -- it should! :-)

> tweak AdjustNotNullInheritance a little bit.

Hmm, well, modifying a function to suite what we need it to do is part
of code patching :-)

> will validate not-null and set attnotnull, attinvalidnotnull accordingly.

Okay.

> or am i missing something?

In some places, yes we will need to change like that.  However, many
places do not need to change like that.  In particular, (most?) client
applications do not necessarily need that change, and to me, that's the
most important part, because we do not control external applications,
and --as I said upthread-- we do not have the luxury of breaking them.

Thank you.

> imho, my approach is less bug-prone, almost no need to refactor current code.

I'll give this a look ... probably won't have time today ...  however,
IMO the consideration of external applications (ORMs, LibreOffice, admin
GUIs, etc) not breaking is the most important thing to keep in mind.
You can go over the code found by codesearch.debian.net when searching
for `attnotnull` to see the sort of code that would be affected.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"¿Qué importan los años?  Lo que realmente importa es comprobar que
a fin de cuentas la mejor edad de la vida es estar vivo"  (Mafalda)

---

From: Robert Haas <[email-masked]>
Date: Fri, 21 Mar 2025 11:26:05 -0400

> me as so problematic as this change would be.

I don't agree with this conclusion. The 8.3 casting changes were
problematic because any piece of SQL you'd ever written could have
problems. This change will only break queries that look at the
attnotnull column. While there may be quite a few of those, it can't
possibly be of the same order. I think it's routine that changing the
name or type of system catalog columns breaks things for a few people
(e.g. procpid->pid, or relistemp->relpersistence) and we sometimes get
complaints about that, but at least you can grep for it and it's
mostly going to affect admin tools rather than all the queries
everywhere.

That's not to say that adding a second bool column instead of changing
the existing column's data type is necessarily the wrong way to go.
But I think you're overestimating the blast radius by quite a lot.

-- 
Robert Haas
EDB: http://www.enterprisedb.com

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 21 Mar 2025 19:04:10 +0100

On 2025-Mar-21, Robert Haas wrote:

> I don't agree with this conclusion.

Uhm.

> The 8.3 casting changes were problematic because any piece of SQL
> you'd ever written could have problems.

Okay, this much I agree with.

> This change will only break queries that look at the attnotnull
> column.  While there may be quite a few of those, it can't possibly be
> of the same order. I think it's routine that changing the name or type
> of system catalog columns breaks things for a few people (e.g.
> procpid->pid, or relistemp->relpersistence) and we sometimes get

In several of the cases that I checked, the application just tests the
returned value for boolean truth.  If we change the column from boolean
to char, they would stop working properly because both the 't' and the
'f' char values would test as true.  But suppose we were to rename the
column; that would cause developers to have to examine the code to
determine how to react.  That might even be good, because we're end up
in a situation were no application uses outdated assumptions about
nullness in a column.  However, consider the rationale given in 
https://postgr.es/m/[email-masked]
that removing attndims would break PHP -- after that discussion, we
decided against removing the column, even though it's completely
useless, because we don't want to break PHP.  You know, removing
attnotnull would break PHP in exactly the same way, or maybe in some
worse way.  I don't see how can we reach a different conclusion for this
change that for that one.

> But I think you're overestimating the blast radius by quite a lot.

I am just going by some truth established by previous discussion.
If we agree to remove attnotnull or to change the way it works, then we
can also remove attndims.

---

From: jian he <[email-masked]>
Date: Mon, 24 Mar 2025 09:27:30 +0800

hi.
you may like the attached. it's based on your idea: attnotnullvalid.

I came across a case, not sure if it's a bug.
CREATE TABLE ttchk (a INTEGER);
ALTER TABLE ttchk ADD CONSTRAINT cc check (a is NOT NULL) NOT VALID;
CREATE TABLE ttchk_child(a INTEGER) INHERITS(ttchk);
ttchk_child's constraint cc will default to valid,
but pg_dump && pg_restore will make ttchk_child's constraint invalid.
since it's an existing behavior, so not-null constraint will align with it.
--------------------------------------------------------------------
-----the following text is copied from the commit message------------

NOT NULL NOT VALID

* TODO: In doc/src/sgml/ref/alter_table.sgml, under the
<title>Compatibility</title> section,
  clarify how the "NOT NULL NOT VALID" syntax conforms with the standard.
* TODO: Should CREATE TABLE LIKE copy an existing invalid not-null
constraint to the new table,
  and if so, the new table's not-null will be marked as valid.

description entry of pg_attribute.attnotnullvalid:
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attnotnullvalid</structfield> <type>bool</type>
+      </para>
+      <para>
+       The not-null constraint validity status of the column.
+       If true, it means this column has a valid not-null constraint,
+       false means this column doesn't have a not-null constraint or
has an unvalidated one.
+       If <structfield>attnotnull</structfield> is false, this must be false.
       </para></entry>

* attnotnull means that a not-null constraint exists; it doesn't imply anything
  regarding the constraint being valid or not.
  attnotnullvalid will indicate whether the constraint is valid; this column can
  only be true if attnotnull is already true.
  attnotnullvalid only added to FormData_pg_attribute, didn't add to
CompactAttribute.
  mainly because invalid not-null is not being commonly used.
  TupleDesc->TupleConstr->has_not_null now also represents invalid not-null
  constraint.

* For table in pg_catalog schema, if that column attnotnull attribute is true,
 then attnotnullvalid attribute is also true.  Similarly, if
attnotnull is false,
 then attnotnullvalid is false.  I added an SQL check at the end of
 src/test/regress/sql/constraints.sql (not sure it's necessary)

* CREATE TABLE specifying not valid not-null constraint will be set to valid,
  a warning is issued within function transformCreateStmt.
  that means InsertPgAttributeTuples can not insert attribute
  that is (attnotnull && !attnotnullvalid).
 I added an Assert in InsertPgAttributeTuples.
(also added to other places, to demo i didn't mess something, maybe
it's necessary).

* table rewrite won't validate invalid not-null constraint, that is aligned
  with check constraint.

* attnotnullvalid mainly changed in these two places:
  1. ATAddCheckNNConstraint, if you specified "NOT NULL NOT VALID", it
will change
    it from false to false, but will set attnotnull to true.
  2. QueueNNConstraintValidation, subroutine of ATExecValidateConstraint.
    when validing an not valid not-null constraint, toggle it from
false to true,
    also set attnotnull to true.

* A partitioned table can have an invalid NOT NULL constraint while its
  partitions have a valid one, but not the other way around.
  but pg_dump/pg_restore may not preserve the constraint name properly, but
  that's fine for not-null constraint, i think.

* regular table invalid not null constraint pg_dump also works fine.
From fc4bf954772d25dfbf60774429d875f78e4fd69e Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Mon, 24 Mar 2025 09:21:10 +0800
Subject: [PATCH v5 1/1] NOT NULL NOT VALID
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* TODO: In doc/src/sgml/ref/alter_table.sgml, under the <title>Compatibility</title> section,
  clarify how the "NOT NULL NOT VALID" syntax conforms with the standard.
* TODO: Should CREATE TABLE LIKE copy an existing invalid not-null constraint to the new table,
  and if so, the new table's not-null will be marked as valid.

* attnotnull means that a not-null constraint exists; it doesn't imply anything
  regarding the constraint being valid or not.
  attnotnullvalid  will indicate whether the constraint is valid; this column can
  only be true if attnotnull is already true.
  attnotnullvalid only added to FormData_pg_attribute, didn't add to CompactAttribute.
  mainly because invalid not-null is not being commonly used.
  TupleDesc->TupleConstr->has_not_null now also represents invalid not-null
  constraint.

* For table in pg_catalog schema, if that column attnotnull attribute is true,
 then attnotnullvalid attribute is also true.  Similarly, if attnotnull is false,
 then attnotnullvalid is false.  I added an SQL check at the end of
 src/test/regress/sql/constraints.sql (not sure it's necessary)

* CREATE TABLE specifying not valid not-null constraint will be set to valid,
  a warning is issued within function transformCreateStmt.
  that means InsertPgAttributeTuples can not insert attribute
  that is (attnotnull && !attnotnullvalid).
  I added an Assert in InsertPgAttributeTuples.

* table rewrite won't validate invalid not-null constraint, that is aligned
  with check constraint.

* attnotnullvalid, two places toggled it from true to false.
  1. ATAddCheckNNConstraint, if you specified "NOT NULL NOT VALID", it will change
    it from false to false, but will set attnotnull to true.
  2. QueueNNConstraintValidation, subroutine of ATExecValidateConstraint.
    when validing an not valid not-null constraint, toggle it from false to true,
    also set attnotnull to true.

* A partitioned table can have an invalid NOT NULL constraint while its
  partitions have a valid one, but not the other way around.
  but pg_dump/pg_restore may not preserve the constraint name properly, but
  that's fine for not-null constraint.

* regular table invalid not null constraint pg_dump also works fine.

discussion: https://postgr.es/m/CAGPqQf0KitkNack4F5CFkFi-9Dqvp29Ro=EpcWt=[email-masked]
---
 contrib/postgres_fdw/postgres_fdw.c       |   1 +
 doc/src/sgml/catalogs.sgml                |  14 +-
 doc/src/sgml/ref/alter_table.sgml         |   8 +-
 src/backend/access/common/tupdesc.c       |   8 +
 src/backend/bootstrap/bootstrap.c         |   9 +
 src/backend/catalog/genbki.pl             |   2 +
 src/backend/catalog/heap.c                |  20 +-
 src/backend/catalog/pg_constraint.c       |  57 +++++-
 src/backend/commands/tablecmds.c          | 222 ++++++++++++++++++++--
 src/backend/executor/execMain.c           |   1 +
 src/backend/optimizer/util/plancat.c      |   6 +-
 src/backend/parser/gram.y                 |   4 +-
 src/backend/parser/parse_utilcmd.c        |  16 +-
 src/backend/utils/cache/catcache.c        |   1 +
 src/backend/utils/cache/relcache.c        |   6 +-
 src/bin/pg_dump/pg_dump.c                 | 157 ++++++++++++++-
 src/bin/pg_dump/pg_dump.h                 |   3 +-
 src/bin/psql/describe.c                   |   9 +-
 src/include/access/tupdesc.h              |   2 +-
 src/include/catalog/pg_attribute.h        |   3 +
 src/include/catalog/pg_constraint.h       |  10 +-
 src/test/regress/expected/constraints.out | 183 ++++++++++++++++++
 src/test/regress/sql/constraints.sql      | 125 ++++++++++++
 23 files changed, 810 insertions(+), 57 deletions(-)

diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index 6beae0fa37f..92dd1afd47f 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -5572,6 +5572,7 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
 							   "  LEFT JOIN pg_attribute a ON "
 							   "    attrelid = c.oid AND attnum > 0 "
 							   "      AND NOT attisdropped "
+							   "      AND attnotnullvalid "
 							   "  LEFT JOIN pg_attrdef ad ON "
 							   "    adrelid = c.oid AND adnum = attnum ");
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..4f42c5bdc32 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1260,7 +1260,19 @@
        <structfield>attnotnull</structfield> <type>bool</type>
       </para>
       <para>
-       This column has a not-null constraint.
+       This column has a possibly unvalidated not-null constraint
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attnotnullvalid</structfield> <type>bool</type>
+      </para>
+      <para>
+       The not-null constraint validity status of the column.
+       If true, it means this column has a valid not-null constraint,
+       false means this column doesn't have a not-null constraint or has an unvalidated one.
+       If <structfield>attnotnull</structfield> is false, this must be false.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 4f15b89a98f..9a2c0f5bfa0 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -243,6 +243,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       entire table; however, if a valid <literal>CHECK</literal> constraint is
       found which proves no <literal>NULL</literal> can exist, then the
       table scan is skipped.
+      If a column has a invalid not-null constraint, <literal>SET NOT NULL</literal>
+      will change it to a validated not-null constraint.
      </para>
 
      <para>
@@ -458,8 +460,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
      <para>
       This form adds a new constraint to a table using the same constraint
       syntax as <link linkend="sql-createtable"><command>CREATE TABLE</command></link>, plus the option <literal>NOT
-      VALID</literal>, which is currently only allowed for foreign key
-      and CHECK constraints.
+      VALID</literal>, which is currently only allowed for foreign key,
+      CHECK constraints and not-null constraints.
      </para>
 
      <para>
@@ -586,7 +588,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     <term><literal>VALIDATE CONSTRAINT</literal></term>
     <listitem>
      <para>
-      This form validates a foreign key or check constraint that was
+      This form validates a foreign key or check constraint or not-null constraint that was
       previously created as <literal>NOT VALID</literal>, by scanning the
       table to ensure there are no rows for which the constraint is not
       satisfied.  Nothing happens if the constraint is already marked valid.
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index ed2195f14b2..86cb23d58ab 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -252,6 +252,7 @@ CreateTupleDescCopy(TupleDesc tupdesc)
 		Form_pg_attribute att = TupleDescAttr(desc, i);
 
 		att->attnotnull = false;
+		att->attnotnullvalid = false;
 		att->atthasdef = false;
 		att->atthasmissing = false;
 		att->attidentity = '\0';
@@ -298,6 +299,7 @@ CreateTupleDescTruncatedCopy(TupleDesc tupdesc, int natts)
 		Form_pg_attribute att = TupleDescAttr(desc, i);
 
 		att->attnotnull = false;
+		att->attnotnullvalid = false;
 		att->atthasdef = false;
 		att->atthasmissing = false;
 		att->attidentity = '\0';
@@ -418,6 +420,7 @@ TupleDescCopy(TupleDesc dst, TupleDesc src)
 		Form_pg_attribute att = TupleDescAttr(dst, i);
 
 		att->attnotnull = false;
+		att->attnotnullvalid = false;
 		att->atthasdef = false;
 		att->atthasmissing = false;
 		att->attidentity = '\0';
@@ -464,6 +467,7 @@ TupleDescCopyEntry(TupleDesc dst, AttrNumber dstAttno,
 
 	/* since we're not copying constraints or defaults, clear these */
 	dstAtt->attnotnull = false;
+	dstAtt->attnotnullvalid = false;
 	dstAtt->atthasdef = false;
 	dstAtt->atthasmissing = false;
 	dstAtt->attidentity = '\0';
@@ -613,6 +617,8 @@ equalTupleDescs(TupleDesc tupdesc1, TupleDesc tupdesc2)
 			return false;
 		if (attr1->attnotnull != attr2->attnotnull)
 			return false;
+		if (attr1->attnotnullvalid != attr2->attnotnullvalid)
+			return false;
 		if (attr1->atthasdef != attr2->atthasdef)
 			return false;
 		if (attr1->attidentity != attr2->attidentity)
@@ -841,6 +847,7 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attndims = attdim;
 
 	att->attnotnull = false;
+	att->attnotnullvalid = false;
 	att->atthasdef = false;
 	att->atthasmissing = false;
 	att->attidentity = '\0';
@@ -904,6 +911,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attndims = attdim;
 
 	att->attnotnull = false;
+	att->attnotnullvalid = false;
 	att->atthasdef = false;
 	att->atthasmissing = false;
 	att->attidentity = '\0';
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 6db864892d0..b18dc1d5ee6 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -615,6 +615,15 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
 				attrtypes[attnum]->attnotnull = true;
 		}
 	}
+
+	/*
+	 * If a system catalog column is attnotnull then attnotnullvalid is true,
+	 * otherwise false.
+	 */
+	if (attrtypes[attnum]->attnotnull == true)
+		attrtypes[attnum]->attnotnullvalid = true;
+	else
+		attrtypes[attnum]->attnotnullvalid = false;
 }

diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index df3231fcd41..b8651a9d865 100644
--- a/src/backend/catalog/genbki.pl
+++ b/src/backend/catalog/genbki.pl
@@ -986,6 +986,8 @@ sub morph_row_for_pgattr
 		$row->{attnotnull} = 'f';
 	}
 
+	$row->{attnotnullvalid} = $row->{attnotnull} eq 't' ? 't' : 'f';
+
 	Catalog::AddDefaultValues($row, $pgattr_schema, 'pg_attribute');
 	return;
 }
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..bed22db755a 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -151,6 +151,7 @@ static const FormData_pg_attribute a1 = {
 	.attalign = TYPALIGN_SHORT,
 	.attstorage = TYPSTORAGE_PLAIN,
 	.attnotnull = true,
+	.attnotnullvalid = true,
 	.attislocal = true,
 };
 
@@ -164,6 +165,7 @@ static const FormData_pg_attribute a2 = {
 	.attalign = TYPALIGN_INT,
 	.attstorage = TYPSTORAGE_PLAIN,
 	.attnotnull = true,
+	.attnotnullvalid = true,
 	.attislocal = true,
 };
 
@@ -177,6 +179,7 @@ static const FormData_pg_attribute a3 = {
 	.attalign = TYPALIGN_INT,
 	.attstorage = TYPSTORAGE_PLAIN,
 	.attnotnull = true,
+	.attnotnullvalid = true,
 	.attislocal = true,
 };
 
@@ -190,6 +193,7 @@ static const FormData_pg_attribute a4 = {
 	.attalign = TYPALIGN_INT,
 	.attstorage = TYPSTORAGE_PLAIN,
 	.attnotnull = true,
+	.attnotnullvalid = true,
 	.attislocal = true,
 };
 
@@ -203,6 +207,7 @@ static const FormData_pg_attribute a5 = {
 	.attalign = TYPALIGN_INT,
 	.attstorage = TYPSTORAGE_PLAIN,
 	.attnotnull = true,
+	.attnotnullvalid = true,
 	.attislocal = true,
 };
 
@@ -222,6 +227,7 @@ static const FormData_pg_attribute a6 = {
 	.attalign = TYPALIGN_INT,
 	.attstorage = TYPSTORAGE_PLAIN,
 	.attnotnull = true,
+	.attnotnullvalid = true,
 	.attislocal = true,
 };
 
@@ -753,6 +759,11 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attstorage - 1] = CharGetDatum(attrs->attstorage);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcompression - 1] = CharGetDatum(attrs->attcompression);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attnotnull - 1] = BoolGetDatum(attrs->attnotnull);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attnotnullvalid - 1] = BoolGetDatum(attrs->attnotnullvalid);
+		if (!attrs->attnotnull)
+			Assert(!attrs->attnotnullvalid);
+		else
+			Assert(attrs->attnotnullvalid);
 		slot[slotCount]->tts_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(attrs->atthasdef);
 		slot[slotCount]->tts_values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(attrs->atthasmissing);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(attrs->attidentity);
@@ -2621,12 +2632,17 @@ AddRelationNewConstraints(Relation rel,
 						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						errmsg("not-null constraints are not supported on virtual generated columns"));

 			/*
index ac80652baf2..5005ea7e6f1 100644

 
 
 /*

+		/*
+					errmsg("cannot change invalid constraint \"%s\" on relation \"%s\" to valid",

 		if (cooked)
index 1202544ebd0..fc77e15793e 100644
@@ -474,6 +477,7 @@ static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool r
+static void set_attnotnullinvalid(Relation rel, AttrNumber attnum, bool is_valid);
@@ -1400,6 +1404,12 @@ BuildDescForRelation(const List *columns)
 
 		/* Fill in additional stuff not handled by TupleDescInitEntry */
 		att->attnotnull = entry->is_not_null;
+
+		if (att->attnotnull)
+			att->attnotnullvalid  = true;
+		else
+			att->attnotnullvalid  = false;
+
 		att->attislocal = entry->is_local;
 		att->attinhcount = entry->inhcount;
 		att->attidentity = entry->identity;
@@ -2713,10 +2723,10 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 
 		/*
 
@@ -6183,12 +6193,14 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 		 * If we are rebuilding the tuples OR if we added any new but not
 		 * verified not-null constraints, check all not-null constraints. This
 		 * is a bit of overkill but it minimizes risk of bugs.
+		 * But we don't need check invalid not-null constraint! this is aligned
+		 * with check constraint behavior.
 		 */
 		for (i = 0; i < newTupDesc->natts; i++)
 		{
 			Form_pg_attribute attr = TupleDescAttr(newTupDesc, i);
 
-			if (attr->attnotnull && !attr->attisdropped)
+			if (attr->attnotnull && attr->attnotnullvalid && !attr->attisdropped)
 				notnull_attrs = lappend_int(notnull_attrs, attr->attnum);
 		}
 		if (notnull_attrs)
@@ -7669,6 +7681,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
 	/* If the column is already nullable there's nothing to do. */
 	if (!attTup->attnotnull)
 	{
+		// Assert(!attTup->attnotnullvalid);
 		table_close(attr_rel, RowExclusiveLock);
 		return InvalidObjectAddress;
 	}
@@ -7709,7 +7722,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
 	 * Find the constraint that makes this column NOT NULL, and drop it.
 	 * dropconstraint_internal() resets attnotnull.
 	 */
-	conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
+	conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum, false);
 	if (conTup == NULL)
 		elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation \"%s\"",
 			 colName, RelationGetRelationName(rel));
@@ -7766,6 +7779,7 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
 		attr = (Form_pg_attribute) GETSTRUCT(tuple);
 		Assert(!attr->attnotnull);
 		attr->attnotnull = true;
+		attr->attnotnullvalid = true;
 		CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
 
 		/*
@@ -7787,6 +7801,42 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
 	}
 }
 
+/*
+ * currently we have two commands can change attnotnullvalid, attnotnull status:
+ * ALTER TABLE VALIDATE CONSTRAINT, ALTER TABLE ADD NOT NULL NOT VALID.  In both
+ * case, we need set attnotnull to true, set attnotnullvalid based on "is_valid".
+*/
+static void
+set_attnotnullinvalid(Relation rel, AttrNumber attnum, bool is_valid)
+{
+	Form_pg_attribute attr;
+	Relation	attr_rel;
+	HeapTuple	tuple;
+
+	CheckAlterTableIsSafe(rel);
+
+	attr = TupleDescAttr(RelationGetDescr(rel), attnum - 1);
+	if (attr->attisdropped)
+		return;
+
+	attr_rel = table_open(AttributeRelationId, RowExclusiveLock);
+
+	tuple = SearchSysCacheCopyAttNum(RelationGetRelid(rel), attnum);
+	if (!HeapTupleIsValid(tuple))
+		elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+					attnum, RelationGetRelid(rel));
+
+	attr = (Form_pg_attribute) GETSTRUCT(tuple);
+	attr->attnotnullvalid = is_valid;
+	attr->attnotnull = true;
+
+	CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
+	CommandCounterIncrement();
+
+	table_close(attr_rel, RowExclusiveLock);
+	heap_freetuple(tuple);
+}
+
 /*
  * ALTER TABLE ALTER COLUMN SET NOT NULL
  *
@@ -7843,7 +7893,7 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 						   colName, RelationGetRelationName(rel))));
 
@@ -7878,6 +7928,16 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 
 		if (changed)
 		{
@@ -9385,6 +9445,24 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
 	{
 		AlterTableCmd *newcmd;
 		Constraint *nnconstr;
+		HeapTuple	tuple;
+
+		tuple = findNotNullConstraint(RelationGetRelid(rel), strVal(lfirst(lc)), true);
+		if (tuple != NULL)
+		{
+			Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
+			if (!conForm->convalidated)
+				ereport(ERROR,
+						errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+						errmsg("primary key require an validated NOT NULL constraint"),
+						errdetail("NOT NULL constraint \"%s\" on Column \"%s\" of table \"%s\" is not validated",
+								  NameStr(conForm->conname),
+								  strVal(lfirst(lc)),
+								  RelationGetRelationName(rel)),
+						errhint("You may try ALTER TABLE VALIDATE CONSTRAINT to validate it \"%s\"",
+								NameStr(conForm->conname)));
+			heap_freetuple(tuple);
+		}

@@ -9760,11 +9838,19 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			constr->conname = ccon->name;
 
 		/*
+		 * need set the pg_attribute.attnotnullvalid to false, phase 3 don't need to
+				set_attnotnullinvalid(rel, ccon->attnum, false);
+		}
 
 		ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
 	}
@@ -12174,7 +12260,7 @@ ATExecAlterConstraintInternal(List **wqueue, ATAlterConstraint *cmdcon,
 				HeapTuple	childtup;
 				Form_pg_constraint childcon;
 
-				childtup = findNotNullConstraint(childoid, colName);
+				childtup = findNotNullConstraint(childoid, colName, false);
 				if (!childtup)
 					elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
 						 colName, childoid);
@@ -12365,10 +12451,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,

 	if (!con->conenforced)
@@ -12387,6 +12474,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 
 		ObjectAddressSet(address, ConstraintRelationId, con->oid);
 	}
@@ -12603,6 +12695,106 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 	heap_freetuple(copyTuple);
 }
 
+/*
+	set_attnotnullinvalid(rel, attnum, true);
@@ -13473,10 +13665,11 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
 									   false),
 
-		/* All good -- reset attnotnull if needed */
+		/* All good -- reset attnotnull and attnotnullvalid if needed */
 		if (attForm->attnotnull)
 		{
 			attForm->attnotnull = false;
+			attForm->attnotnullvalid = false;
 			CatalogTupleUpdate(attrel, &atttup->t_self, atttup);
 		}
 
@@ -13547,7 +13740,7 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha
 		 */
 		if (con->contype == CONSTRAINT_NOTNULL)
 		{
-			tuple = findNotNullConstraint(childrelid, colname);
+			tuple = findNotNullConstraint(childrelid, colname, false);
 			if (!HeapTupleIsValid(tuple))
 				elog(ERROR, "cache lookup failed for not-null constraint on column \"%s\" of relation %u",
 					 colname, RelationGetRelid(childrel));
@@ -16812,7 +17005,7 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 				HeapTuple	contup;
 
 				contup = findNotNullConstraintAttnum(RelationGetRelid(parent_rel),
-													 parent_att->attnum);
+													 parent_att->attnum, true);
 				if (HeapTupleIsValid(contup) &&
 					!((Form_pg_constraint) GETSTRUCT(contup))->connoinherit)
 					ereport(ERROR,
@@ -19328,7 +19521,8 @@ PartConstraintImpliedByRelConstraint(Relation scanrel,
 		{
 			Form_pg_attribute att = TupleDescAttr(scanrel->rd_att, i - 1);
 
-			if (att->attnotnull && !att->attisdropped)
+			/* invalid not-null constraint must be ignored */
+			if (att->attnotnull && att->attnotnullvalid && !att->attisdropped)
 			{
 				NullTest   *ntest = makeNode(NullTest);
 
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index e9bd98c7738..3be2e4f4639 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -2070,6 +2070,7 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
 		{
 			Form_pg_attribute att = TupleDescAttr(tupdesc, attrChk - 1);
 
+			/* not valid not-null constraint also checked */
 			if (att->attnotnull && slot_attisnull(slot, attrChk))
 			{
 				char	   *val_desc;
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 0489ad36644..c8790264c8d 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -175,9 +175,9 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 	{
 		for (int i = 0; i < relation->rd_att->natts; i++)
 		{
-			CompactAttribute *attr = TupleDescCompactAttr(relation->rd_att, i);
+			Form_pg_attribute attr = TupleDescAttr(relation->rd_att, i);
 
-			if (attr->attnotnull)
+			if (attr->attnotnull && attr->attnotnullvalid)
 			{
 				rel->notnullattnums = bms_add_member(rel->notnullattnums,
 													 i + 1);
@@ -1355,7 +1355,7 @@ get_relation_constraints(PlannerInfo *root,
 			{
 				Form_pg_attribute att = TupleDescAttr(relation->rd_att, i - 1);
 
-				if (att->attnotnull && !att->attisdropped)
+				if (att->attnotnull && att->attnotnullvalid && !att->attisdropped)
 				{
 					NullTest   *ntest = makeNode(NullTest);
 
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 271ae26cbaf..715eb51300b 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -4224,9 +4224,9 @@ ConstraintElem:
 					n->keys = list_make1(makeString($3));
 					/* no NOT VALID support yet */
 					processCASbits($4, @4, "NOT NULL",
-								   NULL, NULL, NULL, NULL,
+								   NULL, NULL, NULL, &n->skip_validation,
 								   &n->is_no_inherit, yyscanner);
-					n->initially_valid = true;
+					n->initially_valid = !n->skip_validation;
 					$$ = (Node *) n;
 				}
 			| UNIQUE opt_unique_null_treatment '(' columnList opt_without_overlaps ')' opt_c_include opt_definition OptConsTableSpace
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index 896a7f2c59b..aaaee388f5d 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -329,6 +329,20 @@ transformCreateStmt(CreateStmt *stmt, const char *queryString)
 			cd->is_not_null = true;
 			break;
 		}
+		if (!cxt.isforeign && !nn->initially_valid)
+		{
+			nn->initially_valid = true;
+			nn->skip_validation = false;
+
+			/*
+			 * not-null constraint created via CREATE TABLE will always be
+			 * valid.  since there is no data there while CREATE TABLE, make it
+			 * invalid does not make sense
+			*/
+			ereport(WARNING,
+					errcode(ERRCODE_WARNING),
+					errmsg("Ignoring NOT VALID flag for NOT NULL constraint on column \"%s\"", colname));
+		}
 	}
 
 	/*
@@ -1291,7 +1305,7 @@ transformTableLikeClause(CreateStmtContext *cxt, TableLikeClause *table_like_cla
 		List	   *lst;

diff --git a/src/backend/utils/cache/catcache.c b/src/backend/utils/cache/catcache.c
index 9ad7681f155..70c11529b90 100644
--- a/src/backend/utils/cache/catcache.c
+++ b/src/backend/utils/cache/catcache.c
@@ -1142,6 +1142,7 @@ CatalogCacheInitializeCache(CatCache *cache)
 			keytype = attr->atttypid;
 			/* cache key columns should always be NOT NULL */
 			Assert(attr->attnotnull);
+			Assert(attr->attnotnullvalid);
 		}
 		else
 		{
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 9f54a9e72b7..84fbabd5344 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -590,7 +590,10 @@ RelationBuildTupleDesc(Relation relation)
 
 		populate_compact_attribute(relation->rd_att, attnum - 1);
 
-		/* Update constraint/default info */
+		/*
+		 * Update constraint/default info
+		 * has_not_null also include invalid not-null constraint
+		*/
 		if (attp->attnotnull)
 			constr->has_not_null = true;
 		if (attp->attgenerated == ATTRIBUTE_GENERATED_STORED)
@@ -3573,6 +3576,7 @@ RelationBuildLocalRelation(const char *relname,
 		datt->attidentity = satt->attidentity;
 		datt->attgenerated = satt->attgenerated;
 		datt->attnotnull = satt->attnotnull;
+		datt->attnotnullvalid = satt->attnotnullvalid;
 		has_not_null |= satt->attnotnull;
 		populate_compact_attribute(rel->rd_att, i);
 	}
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 428ed2d60fc..42755bdfd7d 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8884,6 +8884,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	PQExpBuffer q = createPQExpBuffer();
 	PQExpBuffer tbloids = createPQExpBuffer();
 	PQExpBuffer checkoids = createPQExpBuffer();
+	PQExpBuffer invalidnotnulloids = createPQExpBuffer();
 	PGresult   *res;
 	int			ntups;
 	int			curtblindx;
@@ -8903,6 +8904,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_notnull_name;
 	int			i_notnull_noinherit;
 	int			i_notnull_islocal;
+	int			i_notnull_validated;
 	int			i_attoptions;
 	int			i_attcollation;
 	int			i_attcompression;
@@ -8998,12 +9000,14 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		appendPQExpBufferStr(q,
 							 "co.conname AS notnull_name,\n"
 							 "co.connoinherit AS notnull_noinherit,\n"
-							 "co.conislocal AS notnull_islocal,\n");
+							 "co.conislocal AS notnull_islocal,\n"
+							 "co.convalidated as notnull_validated,\n");
 	else
 		appendPQExpBufferStr(q,
 							 "CASE WHEN a.attnotnull THEN '' ELSE NULL END AS notnull_name,\n"
 							 "false AS notnull_noinherit,\n"
-							 "a.attislocal AS notnull_islocal,\n");
+							 "a.attislocal AS notnull_islocal,\n"
+							 "true as notnull_validated\n");

@@ -9168,12 +9175,32 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
+				 * if column notnull_validated is false, it means either column
+				 * don't have not-null constraint at all, or the existing one is
+				 * invalid.  here we onlu dump the invalid not-null.
 
@@ -9193,6 +9220,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 	PQclear(res);
 
@@ -9326,6 +9354,112 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
+	/*
+				 * invalid not-null constraint must dumped separately as an
+				 * ALTER TABLE ADD CONSTRAINT entry. because CREATE TABLE can
+				 * not create invalid not null constraint. it's also required
+				 * for potentially-violating existing data is loaded before the
@@ -16620,6 +16754,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
 					if (print_notnull)
 					{
+						/* column constraint should all be validated not-null */
+						Assert(tbinfo->notnull_validated[j]);
+
 						if (tbinfo->notnull_constrs[j][0] == '\0')
 							appendPQExpBufferStr(q, " NOT NULL");
 						else
@@ -17939,9 +18076,9 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 									  .createStmt = q->data,
 									  .dropStmt = delq->data));
 	}
-	else if (coninfo->contype == 'c' && tbinfo)
+	else if ((coninfo->contype == 'c' || coninfo->contype == 'n') && tbinfo)
 	{
-		/* CHECK constraint on a table */
+		/* CHECK and invalid not-null constraint on a table */
 
 		/* Ignore if not to be dumped separately, or if it was inherited */
 		if (coninfo->separate && coninfo->conislocal)
@@ -17965,7 +18102,7 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 							 ARCHIVE_OPTS(.tag = tag,
 										  .namespace = tbinfo->dobj.namespace->dobj.name,
 										  .owner = tbinfo->rolname,
-										  .description = "CHECK CONSTRAINT",
+										  .description = coninfo->contype == 'c' ? "CHECK CONSTRAINT" : "CONSTRAINT",
 										  .section = SECTION_POST_DATA,
 										  .createStmt = q->data,
 										  .dropStmt = delq->data));
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bbdb30b5f54..c2d8c3e601a 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -367,8 +367,9 @@ typedef struct _tableInfo
 									 * (pre-v17) */
 	bool	   *notnull_noinh;	/* NOT NULL is NO INHERIT */
 	bool	   *notnull_islocal;	/* true if NOT NULL has local definition */
+	bool	   *notnull_validated;	/* true if NOT NULL is validated */
 	struct _attrDefInfo **attrdefs; /* DEFAULT expressions */
-	struct _constraintInfo *checkexprs; /* CHECK constraints */
+	struct _constraintInfo *checkexprs; /* CHECK constraints XXX: also include invalid not-null constraint */
 	bool		needs_override; /* has GENERATED ALWAYS AS IDENTITY */
 	char	   *amname;			/* relation access method */

 
 				printTableAddFooter(&cont, buf.data);
 			}
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 396eeb7a0bb..55418350505 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -42,7 +42,7 @@ typedef struct TupleConstr
 	struct AttrMissing *missing;	/* missing attributes values, NULL if none */
 	uint16		num_defval;
 	uint16		num_check;
-	bool		has_not_null;
+	bool		has_not_null;	/* this includes invalid not-null !!! */
 	bool		has_generated_stored;
 	bool		has_generated_virtual;
 } TupleConstr;
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index deaa515fe53..398b6e324c8 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -120,6 +120,9 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	/* This flag represents the "NOT NULL" constraint */
 	bool		attnotnull;
 
+	/* false means "NOT NULL NOT VALID". true means XXX (need to consider attnotnull) */
+	bool		attnotnullvalid;
+
 	/* Has DEFAULT value or not */
 	bool		atthasdef BKI_DEFAULT(f);

 
index 4f39100fcdf..7cf69fa7dd3 100644
 
 DROP TABLE notnull_tbl1;
+ERROR:  cannot change invalid constraint "nn" on relation "notnull_tbl1" to valid
+DETAIL:  NOT NULL constraint "nn" on Column "a" of table "notnull_tbl1" is not validated
+HINT:  You may try ALTER TABLE VALIDATE CONSTRAINT to validate it "nn"
+ERROR:  constraint "nn_parent" conflicts with NOT VALID constraint on child table "notnull_tbl1"
+ERROR:  cannot change invalid constraint "nn" on relation "inh_parent" to valid
+ERROR:  cannot change invalid constraint "nn" on relation "inh_child" to valid
+ERROR:  constraint "nn1" conflicts with NOT VALID constraint on child table "pp_nn_1"
@@ -1392,3 +1561,17 @@ DROP TABLE constraint_comments_tbl;
+--sanity check attnotnull and attnotnullvalid.
+select pc.relname, pa.attname, pa.attnum, pa.attnotnull, pa.attnotnullvalid
+from    pg_attribute pa join pg_class pc
+on      pa.attrelid = pc.oid
+where ( (pa.attnotnull and not pa.attnotnullvalid) or
+        (not pa.attnotnull and pa.attnotnullvalid) )
+and     pc.relnamespace in
+        ('pg_catalog'::regnamespace,
+         'pg_toast'::regnamespace,
+         'information_schema'::regnamespace);
+ relname | attname | attnum | attnotnull | attnotnullvalid 
+---------+---------+--------+------------+-----------------
+(0 rows)
index 21ce4177de4..34ad33f2d55 100644
 
@@ -836,3 +950,14 @@ DROP DOMAIN constraint_comments_dom;
 
 DROP ROLE regress_constraint_comments;
 DROP ROLE regress_constraint_comments_noaccess;
+
+--sanity check attnotnull and attnotnullvalid.
+select pc.relname, pa.attname, pa.attnum, pa.attnotnull, pa.attnotnullvalid
+from    pg_attribute pa join pg_class pc
+on      pa.attrelid = pc.oid
+where ( (pa.attnotnull and not pa.attnotnullvalid) or
+        (not pa.attnotnull and pa.attnotnullvalid) )
+and     pc.relnamespace in
+        ('pg_catalog'::regnamespace,
+         'pg_toast'::regnamespace,
+         'information_schema'::regnamespace);
\ No newline at end of file
-- 
2.34.1

---

From: Robert Haas <[email-masked]>
Date: Mon, 24 Mar 2025 10:22:17 -0400

On Fri, Mar 21, 2025 at 2:04 PM Alvaro Herrera <[email-masked]> wrote:
> nullness in a column.

Yes.

> However, consider the rationale given in

Well, that discussion seems awfully weird to me. I can recall plenty
of cases where we've changed stuff in the system catalog and rebuffed
complaints from people who were grumpy about having to adjust their
queries. I don't really understand what makes that case different.

I mean, maybe there's an argument that some changes are more
disruptive than others. For instance, if removing attndims would force
drivers to run extra more complicated queries to learn whether a
certain type is an array type, one could argue that taking it away
without providing some alternative is breaking the drivers in some
fundamental way. I'm not sure whether that's a real problem, but there
is no such argument to be made here. There's no proposal on the table
to entirely remove any information from pg_attribute -- only to turn
something that is presently two-valued into something three-valued.
So, it will still be possible for it to learn the same facts that it
can learn today, it will just perhaps need to be adjusted in terms of
exactly how it does that. But if that is prohibited then what made it
OK all the other times we've done it?

Again, I'm not 100% positive that changing the Boolean column to a
three-valued column is the right way forward, but it does have the
advantage of saving a byte, and the width of system catalog tables has
been a periodic concern. Also, relpersistence is an example of a
seemingly very similar kind of change - turning a Boolean column into
a char column so it can support three values. Do we with the benefit
of hindsight consider that to have been a mistake? I don't, because I
think that PostgreSQL development will be paralyzed if we prohibit
such changes, but opinions might vary.

---

From: Alvaro Herrera <[email-masked]>
Date: Mon, 24 Mar 2025 17:29:32 +0100

> OK all the other times we've done it?

I think use of attnotnull is much more prevalent than that of fields
we've removed or changed previously.  In pg_attribute we recently
removed attcacheoff, which pretty much nobody uses; going back to 2016 I
couldn't find any other removal or type change.  In pg_class, I can see
that we got rid of reltoastidxid in 2013, but that one was quite
esoteric and I doubt anybody would be interested in that.  One commit
that I see removed a lot of columns is 739adf32eecf, but that was in
2002.

One we removed not so long ago was protransform, which was turned into
prosupport by commit 1fb57af92069 in 2019.  But I'm sure that
protransform was much less used by external code, because it's very
specialized, so there weren't so many complaints.  We removed
proisagg/proiswindow to turn them into prokind with commit fd1a421fe661
(2018), and you can find plenty of user complaints about that.

Use of attnotnull is very widespread.

> been a periodic concern.

In this case, as I already said, the new boolean column would go in
what's currently padding space, so there's no widening taking place.

> Also, relpersistence is an example of a seemingly very similar kind of
> change - turning a Boolean column into a char column so it can support
> three values. Do we with the benefit of hindsight consider that to
> have been a mistake?

Are you talking about 5f7b58fad8f4 "Generalize concept of temporary
relations to "relation persistence"." from 2010?  That was 15 years ago,
and maybe it was not a mistake because the ecosystem around Postgres was
different then, but also relistemp/relpersistence is not as widely used
as attnotnull.  There were complaints regarding that change though:
https://www.postgresql.org/message-id/flat/[email-masked]

> I don't, because I think that PostgreSQL development will be paralyzed
> if we prohibit such changes, but opinions might vary.

It's very rare that we need to make catalog changes like this, so I
think you're being overly dramatic.  This is not going to paralyze the
development.  But I agree that we should still allow ourselves to change
system schema, even if we'd break a few things here and there, as long
as it's not the world.

Please don't think that I'm in favor of doing it the difficult way for
no reason.  Previous versions of the patch (changing attnotnull to
char), from the Postgres core code perspective, were pretty much ready
-- we're having this discussion only to avoid the breakage.  I could
save us a bunch of time here and just push those patches, but I think
the responsible thing here (the one we're less likely to regret) is not
to.

---

From: Robert Haas <[email-masked]>
Date: Mon, 24 Mar 2025 13:47:50 -0400

> what's currently padding space, so there's no widening taking place.

IMHO, there will almost certainly be more single-byte columns at some
point in the future, so I feel like using up padding space is not much
different than actual widening over the long term.

> the responsible thing here (the one we're less likely to regret) is not

Fair enough. I think I've said what I want to say, and I'm not here to
substitute my judgement for yours.

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 27 Mar 2025 20:25:40 +0100

> you may like the attached. it's based on your idea: attnotnullvalid.

This is quite close to what I was thinking, yeah.  I noticed a couple of
bugs however, and ended up cleaning up the whole thing.  Here's what I
have so far.  I'm not sure the pg_dump bits are okay (apart from what
you report below) -- I think it's losing the constraint names, which is
of course unacceptable.

I think the warnings about creating constraints as valid when
originating as invalid are unnecessary at this point.  We should add
those, or not, for all constraint types, not just not-null.  That's IMO
a separate discussion.

> since it's an existing behavior, so not-null constraint will align with it.

Hmm, yes, such pg_dump behavior would be incorrect.  I'll give the
pg_dump code another look tomorrow.

-- 
Álvaro Herrera               48°01'N 7°57'E  —  https://www.EnterpriseDB.com/
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index ac14c06c715..e9296e2d4bd 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -5549,7 +5549,15 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
 							   "SELECT relname, "
 							   "  attname, "
 							   "  format_type(atttypid, atttypmod), "
-							   "  attnotnull, "
+							   "  attnotnull, ");
+
+		/* NOT VALID NOT NULL columns are supported since Postgres 18 */
+		if (PQserverVersion(conn) >= 180000)
+			appendStringInfoString(&buf, "attnotnullvalid, ");
+		else
+			appendStringInfoString(&buf, "attnotnull AS attnotnullvalid, ");
+
+		appendStringInfoString(&buf,
 							   "  pg_get_expr(adbin, adrelid), ");
 
 		/* Generated columns are supported since Postgres 12 */
@@ -5651,6 +5659,7 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
 				char	   *attname;
 				char	   *typename;
 				char	   *attnotnull;
+				char	   *attnotnullvalid;
 				char	   *attgenerated;
 				char	   *attdefault;
 				char	   *collname;
@@ -5663,14 +5672,15 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
 				attname = PQgetvalue(res, i, 1);
 				typename = PQgetvalue(res, i, 2);
 				attnotnull = PQgetvalue(res, i, 3);
-				attdefault = PQgetisnull(res, i, 4) ? NULL :
-					PQgetvalue(res, i, 4);
-				attgenerated = PQgetisnull(res, i, 5) ? NULL :
+				attnotnullvalid = PQgetvalue(res, i, 4);
+				attdefault = PQgetisnull(res, i, 5) ? NULL :
 					PQgetvalue(res, i, 5);
-				collname = PQgetisnull(res, i, 6) ? NULL :
+				attgenerated = PQgetisnull(res, i, 6) ? NULL :
 					PQgetvalue(res, i, 6);
-				collnamespace = PQgetisnull(res, i, 7) ? NULL :
+				collname = PQgetisnull(res, i, 7) ? NULL :
 					PQgetvalue(res, i, 7);
+				collnamespace = PQgetisnull(res, i, 8) ? NULL :
+					PQgetvalue(res, i, 8);
 
 				if (first_item)
 					first_item = false;
@@ -5714,7 +5724,11 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid)
 
 				/* Add NOT NULL if needed */
 				if (import_not_null && attnotnull[0] == 't')
+				{
 					appendStringInfoString(&buf, " NOT NULL");
+					if (attnotnullvalid[0] == 'f')
+						appendStringInfoString(&buf, " NOT VALID");
+				}
 			}
 			while (++i < numrows &&
 				   strcmp(PQgetvalue(res, i, 0), tablename) == 0);
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index fb050635551..f33f1ea1b57 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1260,7 +1260,16 @@
        <structfield>attnotnull</structfield> <type>bool</type>
       </para>
       <para>
-       This column has a not-null constraint.
+       This column has a (possibly unvalidated) not-null constraint.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>attnotnullvalid</structfield> <type>bool</type>
+      </para>
+      <para>
+       Whether the not-null constraint, if one exists, has been validated.
       </para></entry>
      </row>
 
diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 11d1bc7dbe1..344387cd99d 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -243,6 +243,8 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       entire table; however, if a valid <literal>CHECK</literal> constraint is
       found which proves no <literal>NULL</literal> can exist, then the
       table scan is skipped.
+      If a column has a invalid not-null constraint,
+      <literal>SET NOT NULL</literal> validates it.
      </para>
 
      <para>
+      <literal>CHECK</literal> constraints and not-null constraints.
      </para>
 
      <para>
@@ -586,7 +588,7 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
     <term><literal>VALIDATE CONSTRAINT</literal></term>
     <listitem>
      <para>
-      This form validates a foreign key or check constraint that was
+      This form validates a foreign key, check, or not-null constraint that was
       previously created as <literal>NOT VALID</literal>, by scanning the
       table to ensure there are no rows for which the constraint is not
       satisfied.  Nothing happens if the constraint is already marked valid.
diff --git a/src/backend/access/common/tupdesc.c b/src/backend/access/common/tupdesc.c
index ed2195f14b2..320de2cbda0 100644
--- a/src/backend/access/common/tupdesc.c
+++ b/src/backend/access/common/tupdesc.c
@@ -74,7 +74,8 @@ populate_compact_attribute_internal(Form_pg_attribute src,
 	dst->atthasmissing = src->atthasmissing;
 	dst->attisdropped = src->attisdropped;
 	dst->attgenerated = (src->attgenerated != '\0');
-	dst->attnotnull = src->attnotnull;
+	dst->attnullability = !src->attnotnull ? ATTNULLABLE_NONE :
+		src->attnotnullvalid ? ATTNULLABLE_VALID : ATTNULLABLE_INVALID;
 
 	switch (src->attalign)
 	{
@@ -252,6 +253,7 @@ CreateTupleDescCopy(TupleDesc tupdesc)
 		Form_pg_attribute att = TupleDescAttr(desc, i);
 
 		att->attnotnull = false;
+		att->attnotnullvalid = false;
 		att->atthasdef = false;
 		att->atthasmissing = false;
 		att->attidentity = '\0';
@@ -298,6 +300,7 @@ CreateTupleDescTruncatedCopy(TupleDesc tupdesc, int natts)
 		Form_pg_attribute att = TupleDescAttr(desc, i);
 
 		att->attnotnull = false;
 
 		att->attnotnull = false;
 
@@ -841,6 +848,7 @@ TupleDescInitEntry(TupleDesc desc,
 	att->attndims = attdim;
 
 	att->attnotnull = false;
+	att->attnotnullvalid = false;
 	att->atthasdef = false;
 	att->atthasmissing = false;
 	att->attidentity = '\0';
@@ -904,6 +912,7 @@ TupleDescInitBuiltinEntry(TupleDesc desc,
 	att->attndims = attdim;
 
 	att->attnotnull = false;
+	att->attnotnullvalid = false;
 	att->atthasdef = false;
 	att->atthasmissing = false;
 	att->attidentity = '\0';
diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 6db864892d0..44ec9e58520 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -615,6 +615,9 @@ DefineAttr(char *name, char *type, int attnum, int nullness)
 				attrtypes[attnum]->attnotnull = true;
 		}
 	}
+
+	/* Not-null constraints on system catalogs are always valid. */
+	attrtypes[attnum]->attnotnullvalid = attrtypes[attnum]->attnotnull;
 }

diff --git a/src/backend/catalog/genbki.pl b/src/backend/catalog/genbki.pl
index df3231fcd41..bdf55d7dc8d 100644
--- a/src/backend/catalog/genbki.pl
+++ b/src/backend/catalog/genbki.pl
@@ -986,6 +986,9 @@ sub morph_row_for_pgattr
 		$row->{attnotnull} = 'f';
 	}
 
+	# Not-null constraints on system catalogs are always valid.
+	$row->{attnotnullvalid} = $row->{attnotnull};
+
 	Catalog::AddDefaultValues($row, $pgattr_schema, 'pg_attribute');
 	return;
 }
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index bd3554c0bfd..d3a814986e9 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -151,6 +151,7 @@ static const FormData_pg_attribute a1 = {
 	.attalign = TYPALIGN_SHORT,
 	.attstorage = TYPSTORAGE_PLAIN,
 	.attnotnull = true,
+	.attnotnullvalid = true,
 	.attislocal = true,
 };

 

 
@@ -753,6 +759,7 @@ InsertPgAttributeTuples(Relation pg_attribute_rel,
 		slot[slotCount]->tts_values[Anum_pg_attribute_attstorage - 1] = CharGetDatum(attrs->attstorage);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attcompression - 1] = CharGetDatum(attrs->attcompression);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attnotnull - 1] = BoolGetDatum(attrs->attnotnull);
+		slot[slotCount]->tts_values[Anum_pg_attribute_attnotnullvalid - 1] = BoolGetDatum(attrs->attnotnullvalid);
 		slot[slotCount]->tts_values[Anum_pg_attribute_atthasdef - 1] = BoolGetDatum(attrs->atthasdef);
 		slot[slotCount]->tts_values[Anum_pg_attribute_atthasmissing - 1] = BoolGetDatum(attrs->atthasmissing);
 		slot[slotCount]->tts_values[Anum_pg_attribute_attidentity - 1] = CharGetDatum(attrs->attidentity);
@@ -1714,6 +1721,7 @@ RemoveAttributeById(Oid relid, AttrNumber attnum)
 
 	/* Remove any not-null constraint the column may have */
 	attStruct->attnotnull = false;
+	attStruct->attnotnullvalid = false;
 
 	/* Unset this so no one tries to look up the generation expression */
 	attStruct->attgenerated = '\0';
@@ -2621,12 +2629,17 @@ AddRelationNewConstraints(Relation rel,
 						errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
 						errmsg("not-null constraints are not supported on virtual generated columns"));
 
+			Assert(cdef->initially_valid != cdef->skip_validation);
+
 			/*
 			 * If the column already has a not-null constraint, we don't want
-			 * to add another one; just adjust inheritance status as needed.
+			 * to add another one; adjust inheritance status as needed.  This
+			 * also checks whether the existing constraint matches the
+			 * requested validity.
 			 */
-			if (AdjustNotNullInheritance(RelationGetRelid(rel), colnum,
-										 is_local, cdef->is_no_inherit))
+			if (AdjustNotNullInheritance(rel, colnum, is_local,
+										 cdef->is_no_inherit,
+										 cdef->skip_validation))
 				continue;
 
 			/*
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index ac80652baf2..81e975e1238 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -574,8 +574,8 @@ ChooseConstraintName(const char *name1, const char *name2,
 
 /*
  * Find and return a copy of the pg_constraint tuple that implements a
- * validated not-null constraint for the given column of the given relation.
- * If no such constraint exists, return NULL.
+ * (possibly not valid) not-null constraint for the given column of the
+ * given relation.  If no such constraint exists, return NULL.
  *
  * XXX This would be easier if we had pg_attribute.notnullconstr with the OID
  * of the constraint that implements the not-null constraint for that column.
@@ -604,13 +604,11 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
 		AttrNumber	conkey;
 
 		/*
 
 		conkey = extractNotNullColumn(conTup);
 		if (conkey != attnum)
@@ -628,9 +626,10 @@ findNotNullConstraintAttnum(Oid relid, AttrNumber attnum)
 }
 
 /*
- * Find and return the pg_constraint tuple that implements a validated
- * not-null constraint for the given column of the given relation.  If
- * no such column or no such constraint exists, return NULL.
+ * Find and return a copy of the pg_constraint tuple that implements a
+ * (possibly not valid) not-null constraint for the given column of the
+ * given relation.
+ * If no such column or no such constraint exists, return NULL.
  */
 HeapTuple
 findNotNullConstraint(Oid relid, const char *colname)
@@ -721,19 +720,23 @@ extractNotNullColumn(HeapTuple constrTup)
  *
  * If no not-null constraint is found for the column, return false.
  * Caller can create one.
+ *
  * If a constraint exists but the connoinherit flag is not what the caller
- * wants, throw an error about the incompatibility.  Otherwise, we adjust
- * conislocal/coninhcount and return true.
- * In the latter case, if is_local is true we flip conislocal true, or do
- * nothing if it's already true; otherwise we increment coninhcount by 1.
+ * wants, throw an error about the incompatibility.  If the desired
+ * constraint is valid but the existing constraint is not valid, also
+ * throw an error about that (the opposite case is acceptable).
+ *
+ * If everything checks out, we adjust conislocal/coninhcount and return
+ * true.  If is_local is true we flip conislocal true, or do nothing if
+ * it's already true; otherwise we increment coninhcount by 1.
  */
 bool
-AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
-						 bool is_local, bool is_no_inherit)
+AdjustNotNullInheritance(Relation rel, AttrNumber attnum,
+						 bool is_local, bool is_no_inherit, bool is_notvalid)
 {
 	HeapTuple	tup;
 
-	tup = findNotNullConstraintAttnum(relid, attnum);
+	tup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
 	if (HeapTupleIsValid(tup))
 	{
 		Relation	pg_constraint;
@@ -751,7 +754,18 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
 			ereport(ERROR,
 					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
 					errmsg("cannot change NO INHERIT status of NOT NULL constraint \"%s\" on relation \"%s\"",
-						   NameStr(conform->conname), get_rel_name(relid)));
+						   NameStr(conform->conname), RelationGetRelationName(rel)));
+
+		/*
+		 * Throw an error if the existing constraint is NOT VALID and caller
+		 * wants a valid one.
+		 */
+		if (!is_notvalid && !conform->convalidated)
+			ereport(ERROR,
+					errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+					errmsg("incompatible NOT VALID constraint \"%s\" on relation \"%s\"",
+						   NameStr(conform->conname), RelationGetRelationName(rel)),
+					errhint("You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to validate it."));
 
 		if (!is_local)
 		{
@@ -830,7 +844,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
 			cooked->attnum = colnum;
 			cooked->expr = NULL;
 			cooked->is_enforced = true;
-			cooked->skip_validation = false;
+			cooked->skip_validation = !conForm->convalidated;
 			cooked->is_local = true;
 			cooked->inhcount = 0;
 			cooked->is_no_inherit = conForm->connoinherit;
@@ -850,7 +864,7 @@ RelationGetNotNullConstraints(Oid relid, bool cooked, bool include_noinh)
 			constr->keys = list_make1(makeString(get_attname(relid, colnum,
 															 false)));
 			constr->is_enforced = true;
-			constr->skip_validation = false;
+			constr->skip_validation = !conForm->convalidated;
 			constr->initially_valid = true;
 			constr->is_no_inherit = conForm->connoinherit;
 			notnulls = lappend(notnulls, constr);
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index afb25007613..bb5bd0f6d61 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -419,6 +419,9 @@ static void QueueFKConstraintValidation(List **wqueue, Relation conrel, Relation
 static void QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 										   char *constrName, HeapTuple contuple,
 										   bool recurse, bool recursing, LOCKMODE lockmode);
+static void QueueNNConstraintValidation(List **wqueue, Relation conrel, Relation rel,
+										HeapTuple contuple, bool recurse, bool recursing,
+										LOCKMODE lockmode);
 static int	transformColumnNameList(Oid relId, List *colList,
 									int16 *attnums, Oid *atttypids, Oid *attcollids);
 static int	transformFkeyGetPrimaryKey(Relation pkrel, Oid *indexOid,
@@ -482,7 +485,7 @@ static void add_column_collation_dependency(Oid relid, int32 attnum, Oid collid)
 static ObjectAddress ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
 									   LOCKMODE lockmode);
 static void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
-						   LOCKMODE lockmode);
+						   bool is_valid, bool queue_validation);
 static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel,
 									  char *constrname, char *colName,
 									  bool recurse, bool recursing,
@@ -1324,7 +1327,7 @@ DefineRelation(CreateStmt *stmt, char relkind, Oid ownerId,
 	nncols = AddRelationNotNullConstraints(rel, stmt->nnconstraints,
 										   old_notnulls);
 	foreach_int(attrnum, nncols)
-		set_attnotnull(NULL, rel, attrnum, NoLock);
+		set_attnotnull(NULL, rel, attrnum, true, true);

@@ -1408,7 +1411,7 @@ BuildDescForRelation(const List *columns)
 		TupleDescInitEntryCollation(desc, attnum, attcollation);
 
 		/* Fill in additional stuff not handled by TupleDescInitEntry */
-		att->attnotnull = entry->is_not_null;
+		att->attnotnull = att->attnotnullvalid = entry->is_not_null;
 		att->attislocal = entry->is_local;
 		att->attinhcount = entry->inhcount;
 		att->attidentity = entry->identity;
@@ -2722,7 +2725,7 @@ MergeAttributes(List *columns, const List *supers, char relpersistence,
 
 		/*
 		 * Request attnotnull on columns that have a not-null constraint
-		 * that's not marked NO INHERIT.
+		 * that's not marked NO INHERIT (even if not valid).
 		 */
 		nnconstrs = RelationGetNotNullConstraints(RelationGetRelid(relation),
 												  true, false);
@@ -6190,14 +6193,16 @@ ATRewriteTable(AlteredTableInfo *tab, Oid OIDNewHeap)
 	{
 		/*
 		 * If we are rebuilding the tuples OR if we added any new but not
-		 * verified not-null constraints, check all not-null constraints. This
-		 * is a bit of overkill but it minimizes risk of bugs.
+		 * verified not-null constraints, check all valid not-null
+		 * constraints. This is a bit of overkill but it minimizes risk of
+		 * bugs.
 		 */
 		for (i = 0; i < newTupDesc->natts; i++)
 		{
 			Form_pg_attribute attr = TupleDescAttr(newTupDesc, i);
 
-			if (attr->attnotnull && !attr->attisdropped)
+			if (attr->attnotnull && attr->attnotnullvalid &&
+				!attr->attisdropped)
 				notnull_attrs = lappend_int(notnull_attrs, attr->attnum);
 		}
 		if (notnull_attrs)
@@ -7716,7 +7721,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
 
 	/*
 	 * Find the constraint that makes this column NOT NULL, and drop it.
-	 * dropconstraint_internal() resets attnotnull.
+	 * dropconstraint_internal() resets attnotnull/attnotnullvalid.
 	 */
 	conTup = findNotNullConstraintAttnum(RelationGetRelid(rel), attnum);
 	if (conTup == NULL)
@@ -7746,7 +7751,7 @@ ATExecDropNotNull(Relation rel, const char *colName, bool recurse,
  */
 static void
 set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
-			   LOCKMODE lockmode)
+			   bool is_valid, bool queue_validation)
 {
 	Form_pg_attribute attr;
 
@@ -7760,7 +7765,7 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
 	if (attr->attisdropped)
 		return;
 
-	if (!attr->attnotnull)
+	if (!attr->attnotnull || (is_valid && !attr->attnotnullvalid))
 	{
 		Relation	attr_rel;
 		HeapTuple	tuple;
@@ -7773,15 +7778,17 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
 				 attnum, RelationGetRelid(rel));
 
 		attr = (Form_pg_attribute) GETSTRUCT(tuple);
-		Assert(!attr->attnotnull);
+
 		attr->attnotnull = true;
+		attr->attnotnullvalid = is_valid;
 		CatalogTupleUpdate(attr_rel, &tuple->t_self, tuple);
 
 		/*
 		 * If the nullness isn't already proven by validated constraints, have
 		 * ALTER TABLE phase 3 test for it.
 		 */
-		if (wqueue && !NotNullImpliedByRelConstraints(rel, attr))
+		if (queue_validation && wqueue &&
+			!NotNullImpliedByRelConstraints(rel, attr))
 		{
 			AlteredTableInfo *tab;
 
@@ -7887,6 +7894,15 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 			conForm->conislocal = true;
 			changed = true;
 		}
+		else if (!conForm->convalidated)
+		{
+			/*
+			 * Flip attnotnull and convalidated, and also validate the
+			 * constraint.
+			 */
+			return ATExecValidateConstraint(wqueue, rel, NameStr(conForm->conname),
+											recurse, recursing, lockmode);
+		}
 
 		if (changed)
 		{
@@ -7949,8 +7965,8 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 	InvokeObjectPostAlterHook(RelationRelationId,
 							  RelationGetRelid(rel), attnum);
 
-	/* Mark pg_attribute.attnotnull for the column */
-	set_attnotnull(wqueue, rel, attnum, lockmode);
+	/* Mark pg_attribute.attnotnull for the column and queue validation */
+	set_attnotnull(wqueue, rel, attnum, true, true);
 
 	/*
 	 * Recurse to propagate the constraint to children that don't have one.
@@ -9389,12 +9405,45 @@ ATPrepAddPrimaryKey(List **wqueue, Relation rel, AlterTableCmd *cmd,
 		}
 	}
 
-	/* Insert not-null constraints in the queue for the PK columns */
+	/* Verify that columns are not-null, or request that they be made so */
 	foreach(lc, pkconstr->keys)
 	{
 		AlterTableCmd *newcmd;
 		Constraint *nnconstr;
+		HeapTuple	tuple;
 
+		/*
+		 * First check if a suitable constraint exists.  If it does, we don't
+		 * need to request another one.  We do need to bail out if it's not
+		 * valid, though.
+		 */
+		tuple = findNotNullConstraint(RelationGetRelid(rel), strVal(lfirst(lc)));
+		if (tuple != NULL)
+		{
+			Form_pg_constraint conForm = (Form_pg_constraint) GETSTRUCT(tuple);
+
+			/* a NO INHERIT constraint is no good */
+			if (conForm->connoinherit)
+				ereport(ERROR,
+						errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+						errmsg("NO INHERIT not-null constraint is incompatible with primary key"),
+						errhint("You will need to use ALTER TABLE ... ALTER CONSTRAINT ... INHERIT."));
+
+			/* an unvalidated constraint is no good */
+			if (!conForm->convalidated)
+				ereport(ERROR,
+						errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
+						errmsg("not-null constraint \"%s\" of table \"%s\" has not been validated",
+							   NameStr(conForm->conname),
+							   RelationGetRelationName(rel)),
+						errhint("You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to validate it."));
+
+			/* All good with this one; don't request another */
+			heap_freetuple(tuple);
+			continue;
+		}
+
+		/* This column is not already not-null, so add it to the queue */
 		nnconstr = makeNotNullConstraint(lfirst(lc));
 
 		newcmd = makeNode(AlterTableCmd);
@@ -9769,11 +9818,15 @@ ATAddCheckNNConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			constr->conname = ccon->name;
 
 		/*
-		 * If adding a not-null constraint, set the pg_attribute flag and tell
-		 * phase 3 to verify existing rows, if needed.
+		 * If adding a valid not-null constraint, set the pg_attribute flag
+		 * and tell phase 3 to verify existing rows, if needed.  For an
+		 * invalid constraint, just set attnotnull and attnotnullvalid,
+		 * without queueing verification.
 		 */
 		if (constr->contype == CONSTR_NOTNULL)
-			set_attnotnull(wqueue, rel, ccon->attnum, lockmode);
+			set_attnotnull(wqueue, rel, ccon->attnum,
+						   !constr->skip_validation,
+						   !constr->skip_validation);
 
 		ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
 	}
@@ -12437,10 +12490,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 
+				 errmsg("constraint \"%s\" of relation \"%s\" is not a foreign key, check, or not-null constraint",
 
 	if (!con->conenforced)
@@ -12459,6 +12513,11 @@ ATExecValidateConstraint(List **wqueue, Relation rel, char *constrName,
 
 		ObjectAddressSet(address, ConstraintRelationId, con->oid);
 	}
@@ -12675,6 +12734,109 @@ QueueCheckConstraintValidation(List **wqueue, Relation conrel, Relation rel,
 	heap_freetuple(copyTuple);
 }
 
+/*
+	 * If we're recursing, we've already done this for parent, so skip it.
+	 * Also, if the constraint is a NO INHERIT constraint, we shouldn't try to
+	 * look for it in the children.
+	/* Set the flags appropriately without queueing another validation */
+	set_attnotnull(NULL, rel, attnum, true, false);
@@ -13543,10 +13705,11 @@ dropconstraint_internal(Relation rel, HeapTuple constraintTup, DropBehavior beha

@@ -16897,19 +17060,25 @@ MergeAttributesIntoExisting(Relation child_rel, Relation parent_rel, bool ispart
 			if (parent_att->attgenerated && !child_att->attgenerated)
 				ereport(ERROR,
 						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("column \"%s\" in child table must be a generated column", parent_attname)));
+						 errmsg("column \"%s\" in child table must be a generated column",
+								parent_attname)));
 			if (child_att->attgenerated && !parent_att->attgenerated)
 				ereport(ERROR,
 						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("column \"%s\" in child table must not be a generated column", parent_attname)));
+						 errmsg("column \"%s\" in child table must not be a generated column",
+								parent_attname)));
 
-			if (parent_att->attgenerated && child_att->attgenerated && child_att->attgenerated != parent_att->attgenerated)
+			if (parent_att->attgenerated && child_att->attgenerated &&
+				child_att->attgenerated != parent_att->attgenerated)
 				ereport(ERROR,
 						(errcode(ERRCODE_DATATYPE_MISMATCH),
-						 errmsg("column \"%s\" inherits from generated column of different kind", parent_attname),
+						 errmsg("column \"%s\" inherits from generated column of different kind",
+								parent_attname),
 						 errdetail("Parent column is %s, child column is %s.",
-								   parent_att->attgenerated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL",
-								   child_att->attgenerated == ATTRIBUTE_GENERATED_STORED ? "STORED" : "VIRTUAL")));
+								   parent_att->attgenerated == ATTRIBUTE_GENERATED_STORED ?
+								   "STORED" : "VIRTUAL",
+								   child_att->attgenerated == ATTRIBUTE_GENERATED_STORED ?
+								   "STORED" : "VIRTUAL")));
 
 			/*
 			 * Regular inheritance children are independent enough not to
@@ -19398,7 +19567,8 @@ PartConstraintImpliedByRelConstraint(Relation scanrel,
 		{
 			Form_pg_attribute att = TupleDescAttr(scanrel->rd_att, i - 1);

 
+			/* not valid not-null constraint also checked */
 			if (att->attnotnull && slot_attisnull(slot, attrChk))
 			{
 				char	   *val_desc;
diff --git a/src/backend/jit/llvm/llvmjit_deform.c b/src/backend/jit/llvm/llvmjit_deform.c
index 5d169c7a40b..c562edd094b 100644
--- a/src/backend/jit/llvm/llvmjit_deform.c
+++ b/src/backend/jit/llvm/llvmjit_deform.c
@@ -123,7 +123,7 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
 		 * combination of attisdropped && attnotnull combination shouldn't
 		 * exist.
 		 */
-		if (att->attnotnull &&
+		if (att->attnullability == ATTNULLABLE_VALID &&
 			!att->atthasmissing &&
 			!att->attisdropped)
 			guaranteed_column_number = attnum;
@@ -438,7 +438,7 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
 		 * into account, because if they're present the heaptuple's natts
 		 * would have indicated that a slot_getmissingattrs() is needed.
 		 */
-		if (!att->attnotnull)
+		if (att->attnullability != ATTNULLABLE_VALID)
 		{
 			LLVMBasicBlockRef b_ifnotnull;
 			LLVMBasicBlockRef b_ifnull;
@@ -604,7 +604,8 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
 			known_alignment = -1;
 			attguaranteedalign = false;
 		}
-		else if (att->attnotnull && attguaranteedalign && known_alignment >= 0)
+		else if (att->attnullability == ATTNULLABLE_VALID &&
+				 attguaranteedalign && known_alignment >= 0)
 		{
 			/*
 			 * If the offset to the column was previously known, a NOT NULL &
@@ -614,7 +615,8 @@ slot_compile_deform(LLVMJitContext *context, TupleDesc desc,
 			Assert(att->attlen > 0);
 			known_alignment += att->attlen;
 		}
-		else if (att->attnotnull && (att->attlen % alignto) == 0)
+		else if (att->attnullability == ATTNULLABLE_VALID &&
+				 (att->attlen % alignto) == 0)
 		{
 			/*
 			 * After a NOT NULL fixed-width column with a length that is a
diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c
index 0489ad36644..44b65b99413 100644
--- a/src/backend/optimizer/util/plancat.c
+++ b/src/backend/optimizer/util/plancat.c
@@ -177,7 +177,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent,
 		{
 			CompactAttribute *attr = TupleDescCompactAttr(relation->rd_att, i);
 
-			if (attr->attnotnull)
+			if (attr->attnullability == ATTNULLABLE_VALID)
 			{
 				rel->notnullattnums = bms_add_member(rel->notnullattnums,
 													 i + 1);
@@ -1355,7 +1355,7 @@ get_relation_constraints(PlannerInfo *root,
 			{
 				Form_pg_attribute att = TupleDescAttr(relation->rd_att, i - 1);
 
-				if (att->attnotnull && !att->attisdropped)
+				if (att->attnotnull /* && att->attnotnullvalid */ && !att->attisdropped)
 				{
 					NullTest   *ntest = makeNode(NullTest);
 
index 0fc502a3a40..e6d3c56df86 100644
@@ -4214,9 +4214,9 @@ ConstraintElem:
index 896a7f2c59b..92f422540c2 100644
+			 * valid.  since there is no data there while CREATE TABLE, make
+			 * it invalid does not make sense
 
 	/*
index 9f54a9e72b7..ea035f6feb8 100644

-		/* Update constraint/default info */
+		/*
+		 * Update constraint/default info has_not_null also include invalid
+		 * not-null constraint
+		 */
 		if (attp->attnotnull)
 			constr->has_not_null = true;
 		if (attp->attgenerated == ATTRIBUTE_GENERATED_STORED)
@@ -3573,6 +3576,7 @@ RelationBuildLocalRelation(const char *relname,
 		datt->attidentity = satt->attidentity;
 		datt->attgenerated = satt->attgenerated;
 		datt->attnotnull = satt->attnotnull;
+		datt->attnotnullvalid = satt->attnotnullvalid;
 		has_not_null |= satt->attnotnull;
 		populate_compact_attribute(rel->rd_att, i);
 	}
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e41e645f649..4c370889c52 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -8919,6 +8919,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	PQExpBuffer q = createPQExpBuffer();
 	PQExpBuffer tbloids = createPQExpBuffer();
 	PQExpBuffer checkoids = createPQExpBuffer();
+	PQExpBuffer invalidnotnulloids = createPQExpBuffer();
 	PGresult   *res;
 	int			ntups;
 	int			curtblindx;
@@ -8938,6 +8939,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_notnull_name;
 	int			i_notnull_noinherit;
 	int			i_notnull_islocal;
+	int			i_notnull_validated;
 	int			i_attoptions;
 	int			i_attcollation;
 	int			i_attcompression;
@@ -9018,11 +9020,12 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 	/*
 	 * Find out any NOT NULL markings for each column.  In 18 and up we read
-	 * pg_constraint to obtain the constraint name.  notnull_noinherit is set
-	 * according to the NO INHERIT property.  For versions prior to 18, we
-	 * store an empty string as the name when a constraint is marked as
-	 * attnotnull (this cues dumpTableSchema to print the NOT NULL clause
-	 * without a name); also, such cases are never NO INHERIT.
+	 * pg_constraint to obtain the constraint name.  notnull_validated and
+	 * notnull_noinherit are set according to the NOT VALID and NO INHERIT
+	 * properties, respectively.  For versions prior to 18, we store an empty
+	 * string as the name when a constraint is marked as attnotnull (this cues
+	 * dumpTableSchema to print the NOT NULL clause without a name); also,
+	 * such cases are never NO INHERIT nor NOT VALID.
 	 *
 	 * We track in notnull_islocal whether the constraint was defined directly
 	 * in this table or via an ancestor, for binary upgrade.  flagInhAttrs
@@ -9032,11 +9035,13 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	if (fout->remoteVersion >= 180000)
 		appendPQExpBufferStr(q,
 							 "co.conname AS notnull_name,\n"
+							 "co.convalidated AS notnull_validated,\n"
 							 "co.connoinherit AS notnull_noinherit,\n"
 							 "co.conislocal AS notnull_islocal,\n");
 	else
 		appendPQExpBufferStr(q,
 							 "CASE WHEN a.attnotnull THEN '' ELSE NULL END AS notnull_name,\n"
+							 "a.attnotnull AS notnull_validated\n"
 							 "false AS notnull_noinherit,\n"
 							 "a.attislocal AS notnull_islocal,\n");
 
@@ -9111,6 +9116,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	i_attalign = PQfnumber(res, "attalign");
 	i_attislocal = PQfnumber(res, "attislocal");
 	i_notnull_name = PQfnumber(res, "notnull_name");
+	i_notnull_validated = PQfnumber(res, "notnull_validated");
 	i_notnull_noinherit = PQfnumber(res, "notnull_noinherit");
 	i_notnull_islocal = PQfnumber(res, "notnull_islocal");
 	i_attoptions = PQfnumber(res, "attoptions");
@@ -9129,6 +9135,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * r is handled by the inner loop.
 	 */
 	curtblindx = -1;
+	appendPQExpBufferChar(invalidnotnulloids, '{');
 	for (int r = 0; r < ntups;)
 	{
 		Oid			attrelid = atooid(PQgetvalue(res, r, i_attrelid));
@@ -9178,6 +9185,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		tbinfo->attfdwoptions = (char **) pg_malloc(numatts * sizeof(char *));
 		tbinfo->attmissingval = (char **) pg_malloc(numatts * sizeof(char *));
 		tbinfo->notnull_constrs = (char **) pg_malloc(numatts * sizeof(char *));
+		tbinfo->notnull_validated = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->notnull_noinh = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->notnull_islocal = (bool *) pg_malloc(numatts * sizeof(bool));
 		tbinfo->attrdefs = (AttrDefInfo **) pg_malloc(numatts * sizeof(AttrDefInfo *));
@@ -9203,12 +9211,33 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			tbinfo->attlen[j] = atoi(PQgetvalue(res, r, i_attlen));
 			tbinfo->attalign[j] = *(PQgetvalue(res, r, i_attalign));
 			tbinfo->attislocal[j] = (PQgetvalue(res, r, i_attislocal)[0] == 't');
+			tbinfo->notnull_validated[j] = (PQgetvalue(res, r, i_notnull_validated)[0] == 't');
 
+				 * if column notnull_validated is false, it means either
+				 * column don't have not-null constraint at all, or the
+				 * existing one is invalid.  here we onlu dump the invalid
+				 * not-null.
 
@@ -9228,6 +9257,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 
 	PQclear(res);
 
@@ -9361,6 +9391,112 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 		PQclear(res);
 	}
 
+	/*
+	 * Get info about table NOT NULL NOT VALID constraints.  This is skipped
+	 * for a data-only dump, as it is only needed for table schemas.
+	 */
+				 * for potentially-violating existing data is loaded before
+				 * the constraint.
@@ -16658,6 +16794,9 @@ dumpTableSchema(Archive *fout, const TableInfo *tbinfo)
 
 					if (print_notnull)
 					{
@@ -17977,13 +18116,20 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
+		/* CHECK or invalid not-null constraint on a table */
 
 		/* Ignore if not to be dumped separately, or if it was inherited */
 		if (coninfo->separate && coninfo->conislocal)
 		{
+			const char *keyword;
+
+			if (coninfo->contype == 'c')
+				keyword = "CHECK CONSTRAINT";
+			else
+				keyword = "INVALID NOT NULL CONSTRAINT";
+
 			/* not ONLY since we want it to propagate to children */
 			appendPQExpBuffer(q, "ALTER %sTABLE %s\n", foreign,
 							  fmtQualifiedDumpable(tbinfo));
@@ -18003,7 +18149,7 @@ dumpConstraint(Archive *fout, const ConstraintInfo *coninfo)
 							 ARCHIVE_OPTS(.tag = tag,
 										  .namespace = tbinfo->dobj.namespace->dobj.name,
 										  .owner = tbinfo->rolname,
-										  .description = "CHECK CONSTRAINT",
+										  .description = keyword,
 										  .section = SECTION_POST_DATA,
 										  .createStmt = q->data,
 										  .dropStmt = delq->data));
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index bbdb30b5f54..aec0f38a353 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -365,10 +365,11 @@ typedef struct _tableInfo
 									 * there isn't one on this column. If
 									 * empty string, unnamed constraint
 									 * (pre-v17) */
+	bool	   *notnull_validated;	/* true if NOT NULL is valid */
 	bool	   *notnull_noinh;	/* NOT NULL is NO INHERIT */
 	bool	   *notnull_islocal;	/* true if NOT NULL has local definition */
 	struct _attrDefInfo **attrdefs; /* DEFAULT expressions */
-	struct _constraintInfo *checkexprs; /* CHECK constraints */
+	struct _constraintInfo *checkexprs; /* CHECK, invalid not-null constraints */
 	bool		needs_override; /* has GENERATED ALWAYS AS IDENTITY */
 	char	   *amname;			/* relation access method */
 
@@ -496,6 +497,8 @@ typedef struct _evttriggerInfo
  * use a different objType for foreign key constraints, to make it easier
  * to sort them the way we want.
  *
+ * Not-null constraints don't need this, unless they are NOT VALID.
+ *
  * Note: condeferrable and condeferred are currently only valid for
  * unique/primary-key constraints.  Otherwise that info is in condef.
  */
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index bf565afcc4e..8ba47572ed3 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -3114,7 +3114,8 @@ describeOneTableDetails(const char *schemaname,
 		{
 			printfPQExpBuffer(&buf,
 							  "SELECT c.conname, a.attname, c.connoinherit,\n"
-							  "  c.conislocal, c.coninhcount <> 0\n"
+							  "  c.conislocal, c.coninhcount <> 0,\n"
+							  "  c.convalidated\n"
 							  "FROM pg_catalog.pg_constraint c JOIN\n"
 							  "  pg_catalog.pg_attribute a ON\n"
 							  "    (a.attrelid = c.conrelid AND a.attnum = c.conkey[1])\n"
@@ -3137,14 +3138,16 @@ describeOneTableDetails(const char *schemaname,
 			{
 				bool		islocal = PQgetvalue(result, i, 3)[0] == 't';
 				bool		inherited = PQgetvalue(result, i, 4)[0] == 't';
+				bool		validated = PQgetvalue(result, i, 5)[0] == 't';
 
+								  !validated ? " NOT VALID" : "");
 
 				printTableAddFooter(&cont, buf.data);
 			}
diff --git a/src/include/access/tupdesc.h b/src/include/access/tupdesc.h
index 396eeb7a0bb..83b33ffc8a4 100644
--- a/src/include/access/tupdesc.h
+++ b/src/include/access/tupdesc.h
@@ -42,7 +42,7 @@ typedef struct TupleConstr
 	struct AttrMissing *missing;	/* missing attributes values, NULL if none */
 	uint16		num_defval;
 	uint16		num_check;
-	bool		has_not_null;
+	bool		has_not_null;	/* this includes invalid not-null !!! */
 	bool		has_generated_stored;
 	bool		has_generated_virtual;
 } TupleConstr;
@@ -76,10 +76,15 @@ typedef struct CompactAttribute
 	bool		atthasmissing;	/* as FormData_pg_attribute.atthasmissing */
 	bool		attisdropped;	/* as FormData_pg_attribute.attisdropped */
 	bool		attgenerated;	/* FormData_pg_attribute.attgenerated != '\0' */
-	bool		attnotnull;		/* as FormData_pg_attribute.attnotnull */
+	char		attnullability; /* attnotnull + attnotnullvalid */
 	uint8		attalignby;		/* alignment requirement in bytes */
 } CompactAttribute;
 
+/* Valid values for CompactAttribute->attnullability */
+#define		ATTNULLABLE_VALID		'v' /* valid constraint exists */
+#define		ATTNULLABLE_INVALID		'i' /* constraint exists, marked invalid */
+#define		ATTNULLABLE_NONE		'f' /* no constraint exists */
+
 /*
  * This struct is passed around within the backend to describe the structure
  * of tuples.  For tuples coming from on-disk relations, the information is
diff --git a/src/include/catalog/pg_attribute.h b/src/include/catalog/pg_attribute.h
index deaa515fe53..9b1236e7a90 100644
--- a/src/include/catalog/pg_attribute.h
+++ b/src/include/catalog/pg_attribute.h
@@ -117,9 +117,12 @@ CATALOG(pg_attribute,1249,AttributeRelationId) BKI_BOOTSTRAP BKI_ROWTYPE_OID(75,
 	 */
 	char		attcompression BKI_DEFAULT('\0');
 
-	/* This flag represents the "NOT NULL" constraint */
+	/* Whether a not-null constraint exists for the column */
 	bool		attnotnull;
 
+	/* Whether the not-null constraint, if it exists, is valid */
+	bool		attnotnullvalid;
+
 	/* Has DEFAULT value or not */
 	bool		atthasdef BKI_DEFAULT(f);
 
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 6da164e7e4d..cf27d53e848 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -263,8 +263,8 @@ extern HeapTuple findNotNullConstraintAttnum(Oid relid, AttrNumber attnum);
 extern HeapTuple findNotNullConstraint(Oid relid, const char *colname);
 extern HeapTuple findDomainNotNullConstraint(Oid typid);
 extern AttrNumber extractNotNullColumn(HeapTuple constrTup);
-extern bool AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
-									 bool is_local, bool is_no_inherit);
+extern bool AdjustNotNullInheritance(Relation rel, AttrNumber attnum,
+									 bool is_local, bool is_no_inherit, bool is_notvalid);
 extern List *RelationGetNotNullConstraints(Oid relid, bool cooked,
 										   bool include_noinh);
 
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 4f39100fcdf..b8398efcdeb 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -896,6 +896,189 @@ Not-null constraints:
     "foobar" NOT NULL "a"
 
 DROP TABLE notnull_tbl1;
+-- test NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM  pg_constraint
+WHERE conrelid = ANY($1)
+ORDER BY 1;
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; -- error
+ERROR:  column "a" of relation "notnull_tbl1" contains null values
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; -- ok
+\d+ notnull_tbl1
+                               Table "public.notnull_tbl1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+ b      | integer |           |          |         | plain   |              | 
+Not-null constraints:
+    "nn" NOT NULL "a" NOT VALID
+
+-- even an invalid not-null forbids new nulls:
+INSERT INTO notnull_tbl1 VALUES (NULL, 4);
+ERROR:  null value in column "a" of relation "notnull_tbl1" violates not-null constraint
+DETAIL:  Failing row contains (null, 4).
+-- a constraint already exists:
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a NOT VALID NO INHERIT;
+ERROR:  cannot change NO INHERIT status of NOT NULL constraint "nn" on relation "notnull_tbl1"
+-- Can't change constraint validity this way:
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+ERROR:  incompatible NOT VALID constraint "nn" on relation "notnull_tbl1"
+HINT:  You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to validate it.
+-- This can change the validity, but must fail if there are nulls
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+ERROR:  column "a" of relation "notnull_tbl1" contains null values
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+ERROR:  not-null constraint "nn" of table "notnull_tbl1" has not been validated
+HINT:  You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to validate it.
+-- Creating a child table should mark the constraint there as valid
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS (notnull_tbl1);
+NOTICE:  merging column "a" with inherited definition
+NOTICE:  merging column "b" with inherited definition
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+      conrelid      | conname | convalidated | coninhcount 
+--------------------+---------+--------------+-------------
+ notnull_tbl1       | nn      | f            |           0
+ notnull_tbl1_child | nn      | t            |           1
+(2 rows)
+
+-- Can't make it valid if there are nulls
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+ERROR:  column "a" of relation "notnull_tbl1" contains null values
+-- Make the constraint valid
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+--- now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1, notnull_tbl1_child;
+-- dropping an invalid constraint is possible
+CREATE TABLE notnull_tbl1 (a int, b int);
+ALTER TABLE notnull_tbl1 ADD NOT NULL a NOT VALID,
+	ADD NOT NULL b NOT VALID;
+ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL;
+ALTER TABLE notnull_tbl1 DROP CONSTRAINT notnull_tbl1_b_not_null;
+DROP TABLE notnull_tbl1;
+-- ALTER .. NO INHERIT works for invalid constraints
+CREATE TABLE notnull_tbl1 (a int);
+CREATE TABLE notnull_tbl1_chld () INHERITS (notnull_tbl1);
+ALTER TABLE notnull_tbl1 ADD NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ALTER CONSTRAINT notnull_tbl1_a_not_null NO INHERIT;
+-- DROP CONSTRAINT recurses correctly on invalid constraints
+ALTER TABLE notnull_tbl1 ALTER CONSTRAINT notnull_tbl1_a_not_null INHERIT;
+ALTER TABLE notnull_tbl1 DROP CONSTRAINT notnull_tbl1_a_not_null;
+DROP TABLE notnull_tbl1, notnull_tbl1_chld;
+-- Test the different not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+-- parent have valid not null constraint then child table cannot have invalid one
+CREATE TABLE notnull_chld0 (a int);
+ALTER TABLE notnull_chld0 ADD CONSTRAINT nn_chld0 NOT NULL a;
+ALTER TABLE notnull_tbl1 INHERIT notnull_chld0; --error
+ERROR:  constraint "nn_parent" conflicts with NOT VALID constraint on child table "notnull_tbl1"
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+   conrelid   |  conname  | convalidated | coninhcount 
+--------------+-----------+--------------+-------------
+ notnull_tbl1 | nn_parent | t            |           0
+ notnull_chld | nn_child  | t            |           1
+(2 rows)
+
+DROP TABLE notnull_chld0;
+DROP TABLE notnull_chld;
+DROP TABLE notnull_tbl1;
+-- Test invalid not null on inheritance table.
+CREATE TABLE inh_parent ();
+CREATE TABLE inh_child (i int) INHERITS (inh_parent);
+CREATE TABLE inh_grandchild () INHERITS (inh_parent, inh_child);
+ALTER TABLE inh_parent ADD COLUMN i int;
+NOTICE:  merging definition of column "i" for child "inh_child"
+NOTICE:  merging definition of column "i" for child "inh_grandchild"
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i; --error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "inh_parent"
+HINT:  You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to validate it.
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "inh_child"
+HINT:  You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to validate it.
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+    conrelid    | conname | convalidated | coninhcount 
+----------------+---------+--------------+-------------
+ inh_parent     | nn      | f            |           0
+ inh_child      | nn      | f            |           1
+ inh_grandchild | nn      | f            |           2
+(3 rows)
+
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --ok
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be valid now.
+    conrelid    | conname | convalidated | coninhcount 
+----------------+---------+--------------+-------------
+ inh_parent     | nn      | t            |           0
+ inh_child      | nn      | t            |           1
+ inh_grandchild | nn      | t            |           2
+(3 rows)
+
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+DROP TABLE notnull_tbl1;
+ERROR:  table "notnull_tbl1" does not exist
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --ok
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES IN (1,2);
+CREATE TABLE notnull_tbl1_2(a int, CONSTRAINT nn2 NOT NULL a, b int);
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_2 FOR VALUES IN (3,4);
+CREATE TABLE notnull_tbl1_3(a int, b int);
+INSERT INTO notnull_tbl1_3 values(NULL,1);
+ALTER TABLE notnull_tbl1_3 add CONSTRAINT nn3 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_3 FOR VALUES IN (NULL,5);
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+    conrelid    |   conname   | convalidated | coninhcount 
+----------------+-------------+--------------+-------------
+ notnull_tbl1   | notnull_con | f            |           0
+ notnull_tbl1_1 | notnull_con | t            |           1
+ notnull_tbl1_2 | nn2         | t            |           1
+ notnull_tbl1_3 | nn3         | f            |           1
+(4 rows)
+
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --error, table already have null values
+ERROR:  column "a" of relation "notnull_tbl1_3" contains null values
+ALTER TABLE notnull_tbl1_3 VALIDATE CONSTRAINT nn3; --error
+ERROR:  column "a" of relation "notnull_tbl1_3" contains null values
+TRUNCATE notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --OK
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+    conrelid    |   conname   | convalidated | coninhcount 
+----------------+-------------+--------------+-------------
+ notnull_tbl1   | notnull_con | t            |           0
+ notnull_tbl1_1 | notnull_con | t            |           1
+ notnull_tbl1_2 | nn2         | t            |           1
+ notnull_tbl1_3 | nn3         | t            |           1
+(4 rows)
+
+DROP TABLE notnull_tbl1;
+-----partitioned table have not-null, then the partitions can not be NOT NULL NOT VALID.
+CREATE TABLE pp_nn (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE pp_nn ADD CONSTRAINT pp_nn_notnull NOT NULL a;
+CREATE TABLE pp_nn_1(a int, b int);
+ALTER TABLE pp_nn_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --error
+ERROR:  constraint "nn1" conflicts with NOT VALID constraint on child table "pp_nn_1"
+ALTER TABLE pp_nn_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --ok
+DROP TABLE pp_nn;
+DEALLOCATE get_nnconstraint_info;
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
 -- Verify that constraint names and NO INHERIT are properly considered when
 -- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
 -- and that conflicting cases are rejected.  Mind that table constraints
@@ -1392,3 +1575,17 @@ DROP TABLE constraint_comments_tbl;
 DROP DOMAIN constraint_comments_dom;
 DROP ROLE regress_constraint_comments;
 DROP ROLE regress_constraint_comments_noaccess;
+--sanity check attnotnull and attnotnullvalid.
+select pc.relname, pa.attname, pa.attnum, pa.attnotnull, pa.attnotnullvalid
+from    pg_attribute pa join pg_class pc
+on      pa.attrelid = pc.oid
+where ( (pa.attnotnull and not pa.attnotnullvalid) or
+        (not pa.attnotnull and pa.attnotnullvalid) )
+and     pc.relnamespace in
+        ('pg_catalog'::regnamespace,
+         'pg_toast'::regnamespace,
+         'information_schema'::regnamespace);
+ relname | attname | attnum | attnotnull | attnotnullvalid 
+---------+---------+--------+------------+-----------------
+(0 rows)
+
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 21ce4177de4..fec1213aa49 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -640,6 +640,146 @@ ALTER TABLE notnull_tbl1 ADD CONSTRAINT foobar NOT NULL a;
 \d+ notnull_tbl1
 DROP TABLE notnull_tbl1;
 
+-- test NOT NULL NOT VALID
+PREPARE get_nnconstraint_info(regclass[]) AS
+SELECT conrelid::regclass, conname, convalidated, coninhcount
+FROM  pg_constraint
+WHERE conrelid = ANY($1)
+ORDER BY 1;
+
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1 VALUES (NULL, 1), (300, 3);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a; -- error
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a NOT VALID; -- ok
+\d+ notnull_tbl1
+
+-- even an invalid not-null forbids new nulls:
+INSERT INTO notnull_tbl1 VALUES (NULL, 4);
+
+-- a constraint already exists:
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn1 NOT NULL a NOT VALID NO INHERIT;
+
+-- Can't change constraint validity this way:
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn NOT NULL a;
+
+-- This can change the validity, but must fail if there are nulls
+ALTER TABLE notnull_tbl1 ALTER a SET NOT NULL;
+
+-- cannot add primary key on column marked as NOT VALID NOT NULL
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+
+-- Creating a child table should mark the constraint there as valid
+CREATE TABLE notnull_tbl1_child(a INTEGER, b INTEGER) INHERITS (notnull_tbl1);
+EXECUTE get_nnconstraint_info('{notnull_tbl1_child, notnull_tbl1}');
+
+-- Can't make it valid if there are nulls
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+-- Make the constraint valid
+UPDATE notnull_tbl1 SET a = 100 WHERE b = 1;
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn;
+
+--- now we can add primary key
+ALTER TABLE notnull_tbl1 ADD PRIMARY KEY (a);
+DROP TABLE notnull_tbl1, notnull_tbl1_child;
+
+-- dropping an invalid constraint is possible
+CREATE TABLE notnull_tbl1 (a int, b int);
+ALTER TABLE notnull_tbl1 ADD NOT NULL a NOT VALID,
+	ADD NOT NULL b NOT VALID;
+ALTER TABLE notnull_tbl1 ALTER a DROP NOT NULL;
+ALTER TABLE notnull_tbl1 DROP CONSTRAINT notnull_tbl1_b_not_null;
+DROP TABLE notnull_tbl1;
+
+-- ALTER .. NO INHERIT works for invalid constraints
+CREATE TABLE notnull_tbl1 (a int);
+CREATE TABLE notnull_tbl1_chld () INHERITS (notnull_tbl1);
+ALTER TABLE notnull_tbl1 ADD NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ALTER CONSTRAINT notnull_tbl1_a_not_null NO INHERIT;
+
+-- DROP CONSTRAINT recurses correctly on invalid constraints
+ALTER TABLE notnull_tbl1 ALTER CONSTRAINT notnull_tbl1_a_not_null INHERIT;
+ALTER TABLE notnull_tbl1 DROP CONSTRAINT notnull_tbl1_a_not_null;
+DROP TABLE notnull_tbl1, notnull_tbl1_chld;
+
+-- Test the different not null constraint name for parent and child table
+CREATE TABLE notnull_tbl1 (a int);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT nn_parent NOT NULL a not valid;
+
+-- parent have valid not null constraint then child table cannot have invalid one
+CREATE TABLE notnull_chld0 (a int);
+ALTER TABLE notnull_chld0 ADD CONSTRAINT nn_chld0 NOT NULL a;
+ALTER TABLE notnull_tbl1 INHERIT notnull_chld0; --error
+
+CREATE TABLE notnull_chld (a int);
+ALTER TABLE notnull_chld ADD CONSTRAINT nn_child NOT NULL a not valid;
+ALTER TABLE notnull_chld INHERIT notnull_tbl1;
+
+ALTER TABLE notnull_chld VALIDATE CONSTRAINT nn_child;
+-- parents and child not-null will all be validated.
+ALTER TABLE notnull_tbl1 VALIDATE CONSTRAINT nn_parent;
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_chld}');
+DROP TABLE notnull_chld0;
+DROP TABLE notnull_chld;
+DROP TABLE notnull_tbl1;
+
+
+-- Test invalid not null on inheritance table.
+CREATE TABLE inh_parent ();
+CREATE TABLE inh_child (i int) INHERITS (inh_parent);
+CREATE TABLE inh_grandchild () INHERITS (inh_parent, inh_child);
+ALTER TABLE inh_parent ADD COLUMN i int;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i NOT VALID;
+ALTER TABLE inh_parent ADD CONSTRAINT nn NOT NULL i; --error
+ALTER TABLE inh_child ADD CONSTRAINT nn1 NOT NULL i; --error
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be invalid.
+ALTER TABLE inh_parent ALTER i SET NOT NULL; --ok
+EXECUTE get_nnconstraint_info('{inh_parent, inh_child, inh_grandchild}'); --all should be valid now.
+DROP TABLE inh_parent, inh_child, inh_grandchild;
+
+
+-- Verify NOT NULL VALID/NOT VALID with partition table.
+DROP TABLE notnull_tbl1;
+CREATE TABLE notnull_tbl1 (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE notnull_tbl1 ADD CONSTRAINT notnull_con NOT NULL a NOT VALID; --ok
+CREATE TABLE notnull_tbl1_1 PARTITION OF notnull_tbl1 FOR VALUES IN (1,2);
+CREATE TABLE notnull_tbl1_2(a int, CONSTRAINT nn2 NOT NULL a, b int);
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_2 FOR VALUES IN (3,4);
+
+CREATE TABLE notnull_tbl1_3(a int, b int);
+INSERT INTO notnull_tbl1_3 values(NULL,1);
+ALTER TABLE notnull_tbl1_3 add CONSTRAINT nn3 NOT NULL a NOT VALID;
+ALTER TABLE notnull_tbl1 ATTACH PARTITION notnull_tbl1_3 FOR VALUES IN (NULL,5);
+
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --error, table already have null values
+ALTER TABLE notnull_tbl1_3 VALIDATE CONSTRAINT nn3; --error
+
+TRUNCATE notnull_tbl1;
+ALTER TABLE notnull_tbl1 ALTER COLUMN a SET NOT NULL; --OK
+
+EXECUTE get_nnconstraint_info('{notnull_tbl1, notnull_tbl1_1, notnull_tbl1_2, notnull_tbl1_3}');
+DROP TABLE notnull_tbl1;
+
+-----partitioned table have not-null, then the partitions can not be NOT NULL NOT VALID.
+CREATE TABLE pp_nn (a INTEGER, b INTEGER) PARTITION BY LIST (a);
+ALTER TABLE pp_nn ADD CONSTRAINT pp_nn_notnull NOT NULL a;
+
+CREATE TABLE pp_nn_1(a int, b int);
+ALTER TABLE pp_nn_1 add CONSTRAINT nn1 NOT NULL a NOT VALID;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --error
+ALTER TABLE pp_nn_1 VALIDATE CONSTRAINT nn1;
+ALTER TABLE pp_nn ATTACH PARTITION pp_nn_1 FOR VALUES IN (NULL,5); --ok
+DROP TABLE pp_nn;
+
+DEALLOCATE get_nnconstraint_info;
+
+-- Create table with NOT NULL INVALID constraint, for pg_upgrade.
+CREATE TABLE notnull_tbl1_upg (a INTEGER, b INTEGER);
+INSERT INTO notnull_tbl1_upg VALUES (NULL, 1), (NULL, 2), (300, 3);
+ALTER TABLE notnull_tbl1_upg ADD CONSTRAINT nn NOT NULL a NOT VALID;
+-- end of NOT NULL VALID/NOT VALID --------------------------------
+
+
 -- Verify that constraint names and NO INHERIT are properly considered when
 -- multiple constraint are specified, either explicitly or via SERIAL/PK/etc,
 -- and that conflicting cases are rejected.  Mind that table constraints
@@ -836,3 +976,14 @@ DROP DOMAIN constraint_comments_dom;

---

From: jian he <[email-masked]>
Date: Fri, 28 Mar 2025 16:50:43 +0800

hi.

        /*
         * Update constraint/default info has_not_null also include invalid
         * not-null constraint
         */
this comment needs a period. it should be:
        /*
         * Update constraint/default info. has_not_null also include invalid
         * not-null constraint
         */

gram.y:
                    /* no NOT VALID support yet */
                    processCASbits($4, @4, "NOT NULL",
                                   NULL, NULL, NULL, &n->skip_validation,
                                   &n->is_no_inherit, yyscanner);
                    n->initially_valid = !n->skip_validation;
                    $$ = (Node *) n;
comment  "/* no NOT VALID support yet */" should be removed?

get_relation_constraints:
- if (att->attnotnull && !att->attisdropped)
+ if (att->attnotnull /* && att->attnotnullvalid */ && !att->attisdropped)
looking at how we deal with check constraints,
i think we need
+ if (att->attnotnull && att->attnotnullvalid && !att->attisdropped)

set_attnotnull comments should say something about attnotnullvalid?
since it will change pg_attribute.attnotnullvalid

ATPrepAddPrimaryKey
+ errhint("You will need to use ALTER TABLE ... VALIDATE CONSTRAINT to
validate it."));

I think the error message is less helpful.
Overall, I think we should say that:
to add the primary key on column x requires a validated not-null
constraint on column x.

------------------------------------------------------------------------
i think your patch messed up with pg_constraint.conislocal.
for example:

CREATE TABLE parted (id bigint default 1,id_abc bigint) PARTITION BY LIST (id);
alter TABLE parted add CONSTRAINT dummy_constr not null id not valid;
CREATE TABLE parted_1 (id bigint default 1,id_abc bigint);
alter TABLE parted_1 add CONSTRAINT dummy_constr not null id;
ALTER TABLE parted ATTACH PARTITION parted_1 FOR VALUES IN ('1');

select conrelid::regclass, conname, conislocal
from pg_constraint where conname = 'dummy_constr';

 conrelid |   conname    | conislocal
----------+--------------+------------
 parted   | dummy_constr | t
 parted_1 | dummy_constr | f
(2 rows)

if you  do pg_dump, and execute the pg_dump output
pg_dump  --no-statistics --clean --table-and-children=*parted*
--no-owner --verbose --column-inserts --file=dump.sql --no-acl

select conrelid::regclass, conname, conislocal
from pg_constraint where conname = 'dummy_constr';
output is

 conrelid |   conname    | conislocal
----------+--------------+------------
 parted   | dummy_constr | t
 parted_1 | dummy_constr | t
(2 rows)

because pg_dump will produce
CREATE TABLE public.parted (id bigint DEFAULT 1, id_abc bigint )
PARTITION BY LIST (id);
CREATE TABLE public.parted_1 ( id bigint DEFAULT 1 CONSTRAINT
dummy_constr NOT NULL, id_abc bigint);
ALTER TABLE public.parted ADD CONSTRAINT dummy_constr NOT NULL id NOT VALID;
the third sql command didn't override the parted_1 constraint
dummy_constr  conislocal value.
you may check my code change in AdjustNotNullInheritance

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 28 Mar 2025 19:42:22 +0100

>  conrelid |   conname    | conislocal

Interesting.  Yeah, I removed the code you had there because it was
super weird, had no comments, and removing it had zero effect (no tests
failed), so I thought it was useless.  But apparently something is going
on here that's not what we want.

To fix this, we could say that pg_dump should realize the difference and
dump in a different way ... however I think that'd require looking at
conislocal differently, which I definitely don't want to mess with.

Maybe the real problem here is that making the (valid) child constraint
no longer local when the parent constraint is not valid is not sensible,
precisely because pg_dump won't be able to produce good output.  That
sounds more workable to me ... except that we'd have to ensure that
validating the parent constraint would turn the child constraints as not
local anymore, which might be a bit weird.  But maybe not weirder than
the other approach.

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"I must say, I am absolutely impressed with what pgsql's implementation of
VALUES allows me to do. It's kind of ridiculous how much "work" goes away in
my code.  Too bad I can't do this at work (Oracle 8/9)."       (Tom Allison)
           http://archives.postgresql.org/pgsql-general/2007-06/msg00016.php

---

From: jian he <[email-masked]>
Date: Mon, 31 Mar 2025 16:45:47 +0800

> on here that's not what we want.

my change in AdjustNotNullInheritance is copied from
MergeConstraintsIntoExisting
``
            /*
             * OK, bump the child constraint's inheritance count.  (If we fail
             * later on, this change will just roll back.)
             */
            child_copy = heap_copytuple(child_tuple);
            child_con = (Form_pg_constraint) GETSTRUCT(child_copy);
            if (pg_add_s16_overflow(child_con->coninhcount, 1,
                                    &child_con->coninhcount))
                ereport(ERROR,
                        errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
                        errmsg("too many inheritance parents"));
            /*
             * In case of partitions, an inherited constraint must be
             * inherited only once since it cannot have multiple parents and
             * it is never considered local.
             */
            if (parent_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
            {
                Assert(child_con->coninhcount == 1);
                child_con->conislocal = false;
            }
``

if you look at MergeConstraintsIntoExisting, then it won't be weird.
AdjustNotNullInheritance is kind of doing the same thing as
MergeConstraintsIntoExisting, I think.

---

From: jian he <[email-masked]>
Date: Mon, 31 Mar 2025 20:25:35 +0800

hi.
in notnull-notvalid.patch

we have a new TocEntry->desc kind.
so the following related code within src/bin/pg_dump also needs change
----------------

                                if (strcmp(te->desc, "CONSTRAINT") == 0 ||
                                    strcmp(te->desc, "CHECK CONSTRAINT") == 0 ||
                                    strcmp(te->desc, "FK CONSTRAINT") == 0)
                                    strcpy(buffer, "DROP CONSTRAINT");
                                else
                                    snprintf(buffer, sizeof(buffer), "DROP %s",
                                             te->desc);
----------------
            else if (strcmp(te->desc, "CONSTRAINT") == 0 ||
                     strcmp(te->desc, "CHECK CONSTRAINT") == 0 ||
                     strcmp(te->desc, "FK CONSTRAINT") == 0 ||
                     strcmp(te->desc, "INDEX") == 0 ||
                     strcmp(te->desc, "RULE") == 0 ||
                     strcmp(te->desc, "TRIGGER") == 0)
                te->section = SECTION_POST_DATA;
----------------
    /* these object types don't have separate owners */
    else if (strcmp(type, "CAST") == 0 ||
             strcmp(type, "CHECK CONSTRAINT") == 0 ||
             strcmp(type, "CONSTRAINT") == 0 ||
             strcmp(type, "DATABASE PROPERTIES") == 0 ||
             strcmp(type, "DEFAULT") == 0 ||
             strcmp(type, "FK CONSTRAINT") == 0 ||
             strcmp(type, "INDEX") == 0 ||
             strcmp(type, "RULE") == 0 ||
             strcmp(type, "TRIGGER") == 0 ||
             strcmp(type, "ROW SECURITY") == 0 ||
             strcmp(type, "POLICY") == 0 ||
             strcmp(type, "USER MAPPING") == 0)
    {
        /* do nothing */
    }

---

From: Alvaro Herrera <[email-masked]>
Date: Mon, 31 Mar 2025 15:10:54 +0200

> we have a new TocEntry->desc kind.

Yeah, I wasn't sure that this change made much actual sense.  I think it
may be better to stick with just CONSTRAINT.  There probably isn't
sufficient reason to have a different ->desc value.

---

From: Robert Haas <[email-masked]>
Date: Mon, 31 Mar 2025 13:21:20 -0400

> local anymore, which might be a bit weird.  But maybe not weirder than

It seems like a bad idea to make conislocal and coninhcount have
anything to do with whether the constraint is valid. We need those to
mean what they have traditionally meant just to make the correct
things happen when the constraint is dropped, either directly from the
child or at some ancestor of that child. If something is dropped at an
ancestor table, we cascade down the tree and decrement coninhcount. If
something is dropped at a child table, we can set conislocal=false.
The constraint goes away, IIUC, when coninhcount=0 and
conislocal=false, which directly corresponds to "this constraint no
longer has a remaining definition either locally or by inheritance". I
don't see how you can change much of anything here without breaking
the existing structure. Validity could have separate tracking of some
sort, possibly as elaborate as convalidlocal and convalidinhcount, but
I don't think it can get away with redefining the tracking that we
already have for existence.