From 116cf5017ca7453d75e83c2d424a8c0b60083886 Mon Sep 17 00:00:00 2001
From: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Date: Wed, 27 Nov 2024 16:22:44 +0530
Subject: [PATCH] Dump and restore of inherited and local child NOT NULL
 constraints

Fixes two issues described below: 1. A child table will inherit NOT NULL
constraints from the parent. But it's allowed to add local NOT NULL constraints
on the same column.  If a user specifies a name while adding such a constraint
it will be ignored. That may not be intended. But ignoring the given name has an
ill-effect on dump and restore of such a constraint.  A named NOT NULL
constraint which is inherited and is also local will be dumped as ALTER TABLE
... ADD CONSTRAINT ... command specifying its name. If we ignore this name, it
will not be restored and thus lost. Hence while ADDing a local NOT NULL
constraint on the child table, change the name of the inherited constraint, if
one exists.

2. Consider a parent with NOT NULL col1. If a child is created with CREATE TABLE
child (NOT NULL col1) INHERITS (parent), col1 would be an inherited column with
a local as well as inherited NOT NULL constraint with the default name
containing the name of the child table in it.  We will dump "CREATE TABLE
child() INHERITS parent" to create the child table. This will add the inherited
constraint on child with name of the parent constraint. To turn it into a local
constraint we will dump "ALTER TABLE child ALTER COLUMN col1 SET NOT NULL". This
will not restore the original name of the constraint. Hence instead of dumping
ALTER TABLE ... ALTER COLUMN, we dump ALTER TABLE ... ADD CONSTRAINT with the
child's default name. This would preserve the child's default name across dump
and restore.

Author: Ashutosh Bapat
Discussion: https://www.postgresql.org/message-id/flat/CAExHW5tbdgAKDfqjDJ-7Fk6PJtHg8D4zUF6FQ4H2Pq8zK38Nyw%40mail.gmail.com
---
 src/backend/catalog/heap.c                | 12 ++--
 src/backend/catalog/index.c               |  4 +-
 src/backend/catalog/pg_constraint.c       | 76 +++++++++++++++++++----
 src/backend/commands/tablecmds.c          | 12 ++--
 src/backend/commands/typecmds.c           | 12 ++--
 src/bin/pg_dump/pg_dump.c                 | 61 ++++++++++++------
 src/include/catalog/pg_constraint.h       |  5 +-
 src/test/regress/expected/constraints.out | 41 ++++++++++++
 src/test/regress/sql/constraints.sql      | 17 +++++
 9 files changed, 188 insertions(+), 52 deletions(-)

diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 003af4bf21c..12d440200b8 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2563,10 +2563,12 @@ AddRelationNewConstraints(Relation rel,
 
 			/*
 			 * 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; just adjust inheritance status and the
+			 * constraint name as needed.
 			 */
 			if (AdjustNotNullInheritance(RelationGetRelid(rel), colnum,
-										 is_local, cdef->is_no_inherit))
+										 is_local, cdef->is_no_inherit,
+										 cdef->conname, &nnnames))
 				continue;
 
 			/*
@@ -2575,9 +2577,9 @@ AddRelationNewConstraints(Relation rel,
 			 */
 			if (cdef->conname)
 			{
-				if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
-										 RelationGetRelid(rel),
-										 cdef->conname))
+				if (OidIsValid(ConstraintNameIsUsed(CONSTRAINT_RELATION,
+													RelationGetRelid(rel),
+													cdef->conname)))
 					ereport(ERROR,
 							errcode(ERRCODE_DUPLICATE_OBJECT),
 							errmsg("constraint \"%s\" for relation \"%s\" already exists",
diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index f9bb721c5fe..9a2a8eaef27 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -901,8 +901,8 @@ index_create(Relation heapRelation,
 	}
 
 	if ((flags & INDEX_CREATE_ADD_CONSTRAINT) != 0 &&
-		ConstraintNameIsUsed(CONSTRAINT_RELATION, heapRelationId,
-							 indexRelationName))
+		OidIsValid(ConstraintNameIsUsed(CONSTRAINT_RELATION, heapRelationId,
+										indexRelationName)))
 	{
 		/*
 		 * INDEX_CREATE_IF_NOT_EXISTS does not apply here, since the
diff --git a/src/backend/catalog/pg_constraint.c b/src/backend/catalog/pg_constraint.c
index 9c05a98d28c..058c4d5ab28 100644
--- a/src/backend/catalog/pg_constraint.c
+++ b/src/backend/catalog/pg_constraint.c
@@ -396,17 +396,20 @@ CreateConstraintEntry(const char *constraintName,
  * whether an auto-generated name is OK: here, we will allow it unless there
  * is an identical constraint name in use *on the same object*.
  *
+ * Returns the Oid of constraint with matching name. InvalidOid if none exists.
+ *
  * NB: Caller should hold exclusive lock on the given object, else
  * this test can be fooled by concurrent additions.
  */
-bool
+Oid
 ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId,
 					 const char *conname)
 {
-	bool		found;
+	Oid			result = InvalidOid;
 	Relation	conDesc;
 	SysScanDesc conscan;
 	ScanKeyData skey[3];
+	HeapTuple	tup;
 
 	conDesc = table_open(ConstraintRelationId, AccessShareLock);
 
@@ -429,12 +432,14 @@ ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId,
 								 true, NULL, 3, skey);
 
 	/* There can be at most one matching row */
-	found = (HeapTupleIsValid(systable_getnext(conscan)));
+	tup = systable_getnext(conscan);
+	if (HeapTupleIsValid(tup))
+		result = ((Form_pg_constraint) GETSTRUCT(tup))->oid;
 
 	systable_endscan(conscan);
 	table_close(conDesc, AccessShareLock);
 
-	return found;
+	return result;
 }
 
 /*
@@ -719,10 +724,16 @@ extractNotNullColumn(HeapTuple constrTup)
  * 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.
+ *
+ * A user would expect the now-flipped-to-local constraint to be renamed if they
+ * have provided a name while ADDing the constraint. This is similar to how
+ * CREATE TABLE ... INHERIT would behave, if user provided a name for a local
+ * constraint, being created, which will also be inherited.
  */
 bool
 AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
-						 bool is_local, bool is_no_inherit)
+						 bool is_local, bool is_no_inherit,
+						 char *conname, List **nnnames)
 {
 	HeapTuple	tup;
 
@@ -732,6 +743,8 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
 		Relation	pg_constraint;
 		Form_pg_constraint conform;
 		bool		changed = false;
+		NameData	oldname;
+		bool		renamed = false;
 
 		pg_constraint = table_open(ConstraintRelationId, RowExclusiveLock);
 		conform = (Form_pg_constraint) GETSTRUCT(tup);
@@ -758,12 +771,53 @@ AdjustNotNullInheritance(Oid relid, AttrNumber attnum,
 		else if (!conform->conislocal)
 		{
 			conform->conislocal = true;
+
+			if (conname)
+			{
+				Oid			used_con = ConstraintNameIsUsed(CONSTRAINT_RELATION,
+															relid, conname);
+
+				if (!OidIsValid(used_con))
+				{
+					/*
+					 * No constraint with the same name exists, not even this
+					 * one. Safe to rename it to the given name.
+					 */
+					Assert(namestrcmp(&conform->conname, conname) != 0);
+					namestrcpy(&oldname, NameStr(conform->conname));
+					namestrcpy(&conform->conname, conname);
+					renamed = true;
+					*nnnames = lappend(*nnnames, conname);
+				}
+				else if (used_con != conform->oid)
+					ereport(ERROR,
+							errcode(ERRCODE_DUPLICATE_OBJECT),
+							errmsg("constraint \"%s\" for relation \"%s\" already exists",
+								   conname, get_rel_name(relid)));
+				else
+				{
+					/*
+					 * The given name is same as the existing name of the
+					 * constraint. Nothing to do.
+					 */
+					Assert(namestrcmp(&conform->conname, conname) == 0);
+				}
+			}
+
 			changed = true;
 		}
 
 		if (changed)
+		{
 			CatalogTupleUpdate(pg_constraint, &tup->t_self, tup);
 
+			if (renamed)
+				ereport(WARNING,
+						errcode(ERRCODE_DUPLICATE_OBJECT),
+						errmsg("renamed existing non-null constraint \"%s\" of relation \"%s\" to \"%s\"",
+							   NameStr(oldname), get_rel_name(relid), NameStr(conform->conname)));
+		}
+
 		table_close(pg_constraint, RowExclusiveLock);
 
 		return true;
@@ -967,17 +1021,17 @@ RenameConstraintById(Oid conId, const char *newname)
 	 * For user-friendliness, check whether the name is already in use.
 	 */
 	if (OidIsValid(con->conrelid) &&
-		ConstraintNameIsUsed(CONSTRAINT_RELATION,
-							 con->conrelid,
-							 newname))
+		OidIsValid(ConstraintNameIsUsed(CONSTRAINT_RELATION,
+										con->conrelid,
+										newname)))
 		ereport(ERROR,
 				(errcode(ERRCODE_DUPLICATE_OBJECT),
 				 errmsg("constraint \"%s\" for relation \"%s\" already exists",
 						newname, get_rel_name(con->conrelid))));
 	if (OidIsValid(con->contypid) &&
-		ConstraintNameIsUsed(CONSTRAINT_DOMAIN,
-							 con->contypid,
-							 newname))
+		OidIsValid(ConstraintNameIsUsed(CONSTRAINT_DOMAIN,
+										con->contypid,
+										newname)))
 		ereport(ERROR,
 				(errcode(ERRCODE_DUPLICATE_OBJECT),
 				 errmsg("constraint \"%s\" for domain %s already exists",
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 1af2e2bffb2..1b43ba500ec 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -9459,9 +9459,9 @@ ATExecAddConstraint(List **wqueue, AlteredTableInfo *tab, Relation rel,
 			 */
 			if (newConstraint->conname)
 			{
-				if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
-										 RelationGetRelid(rel),
-										 newConstraint->conname))
+				if (OidIsValid(ConstraintNameIsUsed(CONSTRAINT_RELATION,
+													RelationGetRelid(rel),
+													newConstraint->conname)))
 					ereport(ERROR,
 							(errcode(ERRCODE_DUPLICATE_OBJECT),
 							 errmsg("constraint \"%s\" for relation \"%s\" already exists",
@@ -10380,9 +10380,9 @@ addFkConstraint(addFkConstraintSides fkside,
 	 * Caller supplies us with a constraint name; however, it may be used in
 	 * this partition, so come up with a different one in that case.
 	 */
-	if (ConstraintNameIsUsed(CONSTRAINT_RELATION,
-							 RelationGetRelid(rel),
-							 constraintname))
+	if (OidIsValid(ConstraintNameIsUsed(CONSTRAINT_RELATION,
+										RelationGetRelid(rel),
+										constraintname)))
 		conname = ChooseConstraintName(RelationGetRelationName(rel),
 									   ChooseForeignKeyConstraintNameAddition(fkconstraint->fk_attrs),
 									   "fkey",
diff --git a/src/backend/commands/typecmds.c b/src/backend/commands/typecmds.c
index 859e2191f08..f45ec90f25e 100644
--- a/src/backend/commands/typecmds.c
+++ b/src/backend/commands/typecmds.c
@@ -3528,9 +3528,9 @@ domainAddCheckConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
 	 */
 	if (constr->conname)
 	{
-		if (ConstraintNameIsUsed(CONSTRAINT_DOMAIN,
-								 domainOid,
-								 constr->conname))
+		if (OidIsValid(ConstraintNameIsUsed(CONSTRAINT_DOMAIN,
+											domainOid,
+											constr->conname)))
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_OBJECT),
 					 errmsg("constraint \"%s\" for domain \"%s\" already exists",
@@ -3683,9 +3683,9 @@ domainAddNotNullConstraint(Oid domainOid, Oid domainNamespace, Oid baseTypeOid,
 	 */
 	if (constr->conname)
 	{
-		if (ConstraintNameIsUsed(CONSTRAINT_DOMAIN,
-								 domainOid,
-								 constr->conname))
+		if (OidIsValid(ConstraintNameIsUsed(CONSTRAINT_DOMAIN,
+											domainOid,
+											constr->conname)))
 			ereport(ERROR,
 					(errcode(ERRCODE_DUPLICATE_OBJECT),
 					 errmsg("constraint \"%s\" for domain \"%s\" already exists",
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index add7f16c902..8df06c7cbce 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -346,7 +346,7 @@ static void getTableDataFKConstraints(void);
 static void determineNotNullFlags(Archive *fout, PGresult *res, int r,
 								  TableInfo *tbinfo, int j,
 								  int i_notnull_name, int i_notnull_noinherit,
-								  int i_notnull_islocal);
+								  int i_notnull_islocal, int i_notnull_inhcount);
 static char *format_function_arguments(const FuncInfo *finfo, const char *funcargs,
 									   bool is_agg);
 static char *format_function_signature(Archive *fout,
@@ -8764,6 +8764,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	int			i_notnull_name;
 	int			i_notnull_noinherit;
 	int			i_notnull_islocal;
+	int			i_notnull_inhcount;
 	int			i_attoptions;
 	int			i_attcollation;
 	int			i_attcompression;
@@ -8851,20 +8852,23 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 	 * without a name); also, such cases are never NO INHERIT.
 	 *
 	 * We track in notnull_islocal whether the constraint was defined directly
-	 * in this table or via an ancestor, for binary upgrade.  flagInhAttrs
-	 * might modify this later for servers older than 18; it's also in charge
-	 * of determining the correct inhcount.
+	 * in this table or via an ancestor, for binary upgrade. We track
+	 * not_nullinhcount to decide whether to retain the constraint name for
+	 * non-binary dump. For servers older than 18 flagInhAttrs might modify
+	 * notnull_islocal later and also determine the correct not_inhcount.
 	 */
 	if (fout->remoteVersion >= 180000)
 		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.coninhcount AS notnull_inhcount,\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"
+							 "0 AS notnull_inhcount,\n");
 
 	if (fout->remoteVersion >= 140000)
 		appendPQExpBufferStr(q,
@@ -8939,6 +8943,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_inhcount = PQfnumber(res, "notnull_inhcount");
 	i_attoptions = PQfnumber(res, "attoptions");
 	i_attcollation = PQfnumber(res, "attcollation");
 	i_attcompression = PQfnumber(res, "attcompression");
@@ -9034,7 +9039,7 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
 			determineNotNullFlags(fout, res, r,
 								  tbinfo, j,
 								  i_notnull_name, i_notnull_noinherit,
-								  i_notnull_islocal);
+								  i_notnull_islocal, i_notnull_inhcount);
 
 			tbinfo->attoptions[j] = pg_strdup(PQgetvalue(res, r, i_attoptions));
 			tbinfo->attcollation[j] = atooid(PQgetvalue(res, r, i_attcollation));
@@ -9337,19 +9342,31 @@ getTableAttrs(Archive *fout, TableInfo *tblinfo, int numTables)
  * 2) The column has a constraint with no name (this is the case when
  *    constraints come from pre-18 servers).  In this case, ->notnull_constrs
  *    is set to the empty string; dumpTableSchema will print just "NOT NULL".
- * 3) The column has a constraint with a known name; in that case
+ * 3) The column has a constraint with default (table_column_not_null) name;
+ *    there's no need to print that name in the dump, so notnull_constrs is
+ *    set to the empty string and it behaves as the case 2 above.
+ * 4) The column has a constraint with a non-default name; in that case
  *    notnull_constrs carries that name and dumpTableSchema will print
- *    "CONSTRAINT the_name NOT NULL".  However, if the name is the default
- *    (table_column_not_null), there's no need to print that name in the dump,
- *    so notnull_constrs is set to the empty string and it behaves as the case
- *    above.
+ *    "CONSTRAINT the_name NOT NULL".
  *
- * In a child table that inherits from a parent already containing NOT NULL
- * constraints and the columns in the child don't have their own NOT NULL
- * declarations, we suppress printing constraints in the child: the
- * constraints are acquired at the point where the child is attached to the
- * parent.  This is tracked in ->notnull_inh (which is set in flagInhAttrs for
- * servers pre-18).
+ * NOT NULL constraints on the child table require additional rules as below:
+ * A) In a child table that inherits from a parent already containing NOT NULL
+ *    constraints and the columns in the child don't have their own NOT NULL
+ *    declarations, we suppress printing constraints in the child: the
+ *    constraints are acquired at the point where the child is attached to the
+ *    parent. This is tracked in ->notnull_local (which is set in flagInhAttrs
+ *    for servers pre-18).
+ * B) A column has a local NOT NULL constraint, with non-default name, which is
+ *    also inherited from the parent. This case is treated same as the case 4
+ *    above. But if the column will not be printed in CREATE TABLE command, we
+ *    will print a separate ALTER TABLE ... ADD CONSTRAINT ... command to
+ *    rename the constraint and convert it to local constraint.
+ * C) A column has a local NOT NULL constraint, with the default name, which is
+ *    also inherited from a parent. This case is treated as case B instead of
+ *    case 3 if the column will not be printed in CREATE TABLE command. The
+ *    child would inherit the constraint with parent constraint name when
+ *    attached. It needs to be renamed and converted to a local constraint
+ *    later using same DDL as case B.
  *
  * Any of these constraints might have the NO INHERIT bit.  If so we set
  * ->notnull_noinh and NO INHERIT will be printed by dumpTableSchema.
@@ -9363,9 +9380,10 @@ static void
 determineNotNullFlags(Archive *fout, PGresult *res, int r,
 					  TableInfo *tbinfo, int j,
 					  int i_notnull_name, int i_notnull_noinherit,
-					  int i_notnull_islocal)
+					  int i_notnull_islocal, int i_notnull_inhcount)
 {
 	DumpOptions *dopt = fout->dopt;
+	int			inhcount;
 
 	/*
 	 * notnull_noinh is straight from the query result. notnull_islocal also,
@@ -9373,6 +9391,7 @@ determineNotNullFlags(Archive *fout, PGresult *res, int r,
 	 */
 	tbinfo->notnull_noinh[j] = PQgetvalue(res, r, i_notnull_noinherit)[0] == 't';
 	tbinfo->notnull_islocal[j] = PQgetvalue(res, r, i_notnull_islocal)[0] == 't';
+	inhcount = atoi(PQgetvalue(res, r, i_notnull_inhcount));
 
 	/*
 	 * Determine a constraint name to use.  If the column is not marked not-
@@ -9415,7 +9434,9 @@ determineNotNullFlags(Archive *fout, PGresult *res, int r,
 				/* XXX should match ChooseConstraintName better */
 				default_name = psprintf("%s_%s_not_null", tbinfo->dobj.name,
 										tbinfo->attnames[j]);
-				if (strcmp(default_name,
+
+				if ((inhcount <= 0 || shouldPrintColumn(dopt, tbinfo, j)) &&
+					strcmp(default_name,
 						   PQgetvalue(res, r, i_notnull_name)) == 0)
 					tbinfo->notnull_constrs[j] = "";
 				else
diff --git a/src/include/catalog/pg_constraint.h b/src/include/catalog/pg_constraint.h
index 4b4476738a2..3a842402452 100644
--- a/src/include/catalog/pg_constraint.h
+++ b/src/include/catalog/pg_constraint.h
@@ -250,7 +250,7 @@ extern Oid	CreateConstraintEntry(const char *constraintName,
 								  bool conPeriod,
 								  bool is_internal);
 
-extern bool ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId,
+extern Oid	ConstraintNameIsUsed(ConstraintCategory conCat, Oid objId,
 								 const char *conname);
 extern bool ConstraintNameExists(const char *conname, Oid namespaceid);
 extern char *ChooseConstraintName(const char *name1, const char *name2,
@@ -262,7 +262,8 @@ 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);
+									 bool is_local, bool is_no_inherit,
+									 char *conname, List **nnnames);
 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 71200c90ed3..d57b1e79b6f 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -1285,6 +1285,47 @@ Not-null constraints:
     "a_nn" NOT NULL "a" (local, inherited)
 Inherits: notnull_tbl4
 
+-- named local NOT NULL constraints on child table
+CREATE TABLE notnull_tbl4_cld4 () INHERITS (notnull_tbl4);
+ALTER TABLE notnull_tbl4_cld4 ADD CONSTRAINT dup_name CHECK (a > 2);
+ALTER TABLE notnull_tbl4_cld4 ADD CONSTRAINT dup_name NOT NULL a;
+ERROR:  constraint "dup_name" for relation "notnull_tbl4_cld4" already exists
+ALTER TABLE notnull_tbl4_cld4 DROP CONSTRAINT dup_name;
+ALTER TABLE notnull_tbl4_cld4 ADD CONSTRAINT tbl4_cld4_a_nn NOT NULL a;
+WARNING:  renamed existing non-null constraint "notnull_tbl4_a_not_null" of relation "notnull_tbl4_cld4" to "tbl4_cld4_a_nn"
+ALTER TABLE notnull_tbl4_cld4 ADD CONSTRAINT tbl4_cld4_a_nn2 NOT NULL a;
+\d+ notnull_tbl4_cld4
+                             Table "public.notnull_tbl4_cld4"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "tbl4_cld4_a_nn" NOT NULL "a" (local, inherited)
+Inherits: notnull_tbl4
+
+-- adding NOT NULL constraint with the same name as the existing one is noop
+ALTER TABLE notnull_tbl4_cld4 ADD CONSTRAINT tbl4_cld4_a_nn NOT NULL a;
+\d+ notnull_tbl4_cld4
+                             Table "public.notnull_tbl4_cld4"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "tbl4_cld4_a_nn" NOT NULL "a" (local, inherited)
+Inherits: notnull_tbl4
+
+-- unnamed local NOT NULL constraint on child table
+CREATE TABLE notnull_tbl4_cld5 () INHERITS (notnull_tbl4);
+ALTER TABLE notnull_tbl4_cld5 ALTER COLUMN a SET NOT NULL;
+\d+ notnull_tbl4_cld5
+                             Table "public.notnull_tbl4_cld5"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           | not null |         | plain   |              | 
+Not-null constraints:
+    "notnull_tbl4_a_not_null" NOT NULL "a" (local, inherited)
+Inherits: notnull_tbl4
+
 -- leave these tables around for pg_upgrade testing
 -- It's possible to remove a constraint from parents without affecting children
 CREATE TABLE notnull_tbl5 (a int CONSTRAINT ann NOT NULL,
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index e607eb1fddb..17f0c11c854 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -766,6 +766,23 @@ CREATE TABLE notnull_tbl4_cld3 (PRIMARY KEY (a) DEFERRABLE, CONSTRAINT a_nn NOT
 \d+ notnull_tbl4_cld
 \d+ notnull_tbl4_cld2
 \d+ notnull_tbl4_cld3
+
+-- named local NOT NULL constraints on child table
+CREATE TABLE notnull_tbl4_cld4 () INHERITS (notnull_tbl4);
+ALTER TABLE notnull_tbl4_cld4 ADD CONSTRAINT dup_name CHECK (a > 2);
+ALTER TABLE notnull_tbl4_cld4 ADD CONSTRAINT dup_name NOT NULL a;
+ALTER TABLE notnull_tbl4_cld4 DROP CONSTRAINT dup_name;
+ALTER TABLE notnull_tbl4_cld4 ADD CONSTRAINT tbl4_cld4_a_nn NOT NULL a;
+ALTER TABLE notnull_tbl4_cld4 ADD CONSTRAINT tbl4_cld4_a_nn2 NOT NULL a;
+\d+ notnull_tbl4_cld4
+-- adding NOT NULL constraint with the same name as the existing one is noop
+ALTER TABLE notnull_tbl4_cld4 ADD CONSTRAINT tbl4_cld4_a_nn NOT NULL a;
+\d+ notnull_tbl4_cld4
+-- unnamed local NOT NULL constraint on child table
+CREATE TABLE notnull_tbl4_cld5 () INHERITS (notnull_tbl4);
+ALTER TABLE notnull_tbl4_cld5 ALTER COLUMN a SET NOT NULL;
+\d+ notnull_tbl4_cld5
+
 -- leave these tables around for pg_upgrade testing
 
 -- It's possible to remove a constraint from parents without affecting children
-- 
2.34.1

