From 16cc77625887908168966a54b27e3fe9e44f24eb Mon Sep 17 00:00:00 2001
From: Haibo Yan <tristan.yim@gmail.com>
Date: Mon, 26 Oct 2026 13:46:41 -0700
Subject: [PATCH v2] Fix NOT NULL validation in ALTER TABLE

ALTER TABLE ... SET NOT NULL, and ALTER CONSTRAINT ... INHERIT, create
a not-null constraint on children that lack one.  When a child already
had a NOT VALID constraint, the recursive call merely incremented its
coninhcount, leaving a NOT VALID constraint below a validated one even
though the child might contain nulls.  For a partitioned table this
let the planner trust the parent's constraint, so that for example
"WHERE a IS NULL" and "NOT IN (SELECT a ...)" gave wrong results.
ADD CONSTRAINT and ATTACH PARTITION already reject such states.

Reject the merge, with the same error as AdjustNotNullInheritance(),
when the constraint being propagated is valid and the child's is not.
When the parent's constraint is NOT VALID, ALTER CONSTRAINT ... INHERIT
now creates NOT VALID constraints on children that have none, instead
of valid ones, so that existing NOT VALID descendants remain legal; the
validity of the constraint actually created is what gets propagated
further.  Existing rows are never scanned for a NOT VALID constraint,
but new writes are still checked.  ATExecSetNotNull() gets a new
argument for this.

Also fix SET NOT NULL on a table whose NOT VALID constraint is only
inherited: it made the constraint local but did not validate it until
a second command.  Do both, advancing the command counter in between.

This does not repair catalogs already left inconsistent by earlier
releases.

Discussion: https://postgr.es/m/CACJufxGm_97F-iELOrOzXbv-gzV4fr0VjhfdikSys1pA%3D7JXJw%40mail.gmail.com
---
 doc/src/sgml/ref/alter_table.sgml         |  17 ++-
 src/backend/commands/tablecmds.c          |  76 ++++++++--
 src/test/regress/expected/constraints.out | 166 ++++++++++++++++++++++
 src/test/regress/sql/constraints.sql      | 147 +++++++++++++++++++
 4 files changed, 389 insertions(+), 17 deletions(-)

diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 0f9d698d170..e7973e6775c 100644
--- a/doc/src/sgml/ref/alter_table.sgml
+++ b/doc/src/sgml/ref/alter_table.sgml
@@ -248,7 +248,15 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       (and is not dropped in the same command) which proves no
       <literal>NULL</literal> can exist, then the table scan is skipped.
       If a column has an invalid not-null constraint,
-      <literal>SET NOT NULL</literal> validates it.
+      <literal>SET NOT NULL</literal> validates it.  When the command instead
+      creates a new, valid not-null constraint and propagates it to
+      descendant tables, an existing invalid not-null constraint on that
+      column in a descendant must have been validated beforehand (using
+      <literal>ALTER TABLE ... VALIDATE CONSTRAINT</literal>); otherwise the
+      command fails.  If <literal>ONLY</literal> is specified for a table that
+      has inheritance children, the new constraint is marked
+      <literal>NO INHERIT</literal> and is not propagated; for a partitioned
+      table that has partitions, <literal>ONLY</literal> is rejected.
      </para>
 
      <para>
@@ -607,7 +615,12 @@ WITH ( MODULUS <replaceable class="parameter">numeric_literal</replaceable>, REM
       In addition to changing the inheritability status of the constraint,
       in the case where a non-inheritable constraint is being marked
       inheritable, if the table has children, an equivalent constraint
-      will be added to them. If marking an inheritable constraint as
+      will be added to them.  For a not-null constraint, the added
+      constraints are <literal>NOT VALID</literal> if the constraint being
+      marked inheritable is; if it is valid, the command fails when a child
+      already has an invalid not-null constraint on the column, which must be
+      validated first.
+      If marking an inheritable constraint as
       non-inheritable on a table with children, then the corresponding
       constraint on children will be marked as no longer inherited,
       but not removed.
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 97e16f3eb47..e73ebfd733f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -523,6 +523,7 @@ static void set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
 static ObjectAddress ATExecSetNotNull(List **wqueue, Relation rel,
 									  char *conName, char *colName,
 									  bool recurse, bool recursing,
+									  bool parent_validated,
 									  LOCKMODE lockmode);
 static bool NotNullImpliedByRelConstraints(Relation rel, Form_pg_attribute attr);
 static bool ConstraintImpliedByRelConstraint(Relation scanrel,
@@ -5484,7 +5485,7 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 			break;
 		case AT_SetNotNull:		/* ALTER COLUMN SET NOT NULL */
 			address = ATExecSetNotNull(wqueue, rel, NULL, cmd->name,
-									   cmd->recurse, false, lockmode);
+									   cmd->recurse, false, true, lockmode);
 			break;
 		case AT_SetExpression:
 			address = ATExecSetExpression(tab, rel, cmd->name, cmd->def, lockmode);
@@ -7994,10 +7995,19 @@ set_attnotnull(List **wqueue, Relation rel, AttrNumber attnum,
  *
  * We must recurse to child tables during execution, rather than using
  * ALTER TABLE's normal prep-time recursion.
+ *
+ * When recursing, parent_validated tells whether the constraint being
+ * propagated from the parent is validated.  A validated constraint on the
+ * parent guarantees that no row of any descendant is null, so it cannot be
+ * merged with an existing NOT VALID constraint on a child.  If the child has
+ * no constraint, the one created for it is NOT VALID when the parent's is,
+ * like ADD CONSTRAINT ... NOT VALID does, and its own validity is what gets
+ * passed down in turn.  parent_validated is not used at top level.
  */
 static ObjectAddress
 ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
-				 bool recurse, bool recursing, LOCKMODE lockmode)
+				 bool recurse, bool recursing, bool parent_validated,
+				 LOCKMODE lockmode)
 {
 	HeapTuple	tuple;
 	AttrNumber	attnum;
@@ -8056,6 +8066,20 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 		 */
 		if (recursing)
 		{
+			/*
+			 * Throw an error if the existing constraint is NOT VALID but the
+			 * parent's constraint is validated; this is the same rule that
+			 * AdjustNotNullInheritance applies for ADD CONSTRAINT.
+			 */
+			if (parent_validated && !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 might need to validate it using %s.",
+								"ALTER TABLE ... VALIDATE CONSTRAINT"));
+
 			if (pg_add_s16_overflow(conForm->coninhcount, 1,
 									&conForm->coninhcount))
 				ereport(ERROR,
@@ -8068,15 +8092,6 @@ 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)
 		{
@@ -8089,6 +8104,22 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 			table_close(constr_rel, RowExclusiveLock);
 		}
 
+		/*
+		 * If the constraint is NOT VALID, SET NOT NULL on the target table
+		 * validates it, whether or not it was just made local above.
+		 * Validation reads the constraint again and rewrites it, so make our
+		 * own update visible first.
+		 */
+		if (!recursing && !conForm->convalidated)
+		{
+			if (changed)
+				CommandCounterIncrement();
+
+			/* Flip attnotnull and convalidated, and validate the constraint */
+			return ATExecValidateConstraint(wqueue, rel, NameStr(conForm->conname),
+											recurse, recursing, lockmode);
+		}
+
 		if (changed)
 			return address;
 		else
@@ -8130,14 +8161,27 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 	constraint->is_no_inherit = is_no_inherit;
 	constraint->conname = conName;
 
+	/*
+	 * A constraint inherited from a NOT VALID one cannot be validated either,
+	 * since we don't scan for it here; create it NOT VALID too.
+	 */
+	if (recursing && !parent_validated)
+	{
+		constraint->skip_validation = true;
+		constraint->initially_valid = false;
+	}
+
 	/* and do it */
 	cooked = AddRelationNewConstraints(rel, NIL, list_make1(constraint),
 									   false, !recursing, false, NULL);
 	ccon = linitial(cooked);
 	ObjectAddressSet(address, ConstraintRelationId, ccon->conoid);
 
-	/* Mark pg_attribute.attnotnull for the column and queue validation */
-	set_attnotnull(wqueue, rel, attnum, true);
+	/*
+	 * Mark pg_attribute.attnotnull for the column, and queue validation if
+	 * the constraint is valid.
+	 */
+	set_attnotnull(wqueue, rel, attnum, !constraint->skip_validation);
 
 	InvokeObjectPostAlterHook(RelationRelationId,
 							  RelationGetRelid(rel), attnum);
@@ -8159,7 +8203,8 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 			CommandCounterIncrement();
 
 			ATExecSetNotNull(wqueue, childrel, conName, colName,
-							 recurse, true, lockmode);
+							 recurse, true, !constraint->skip_validation,
+							 lockmode);
 			table_close(childrel, NoLock);
 		}
 	}
@@ -13160,7 +13205,8 @@ ATExecAlterConstrInheritability(List **wqueue, ATAlterConstraint *cmdcon,
 			Relation	childrel = table_open(childoid, NoLock);
 
 			addr = ATExecSetNotNull(wqueue, childrel, NameStr(currcon->conname),
-									colName, true, true, lockmode);
+									colName, true, true, currcon->convalidated,
+									lockmode);
 			if (OidIsValid(addr.objectId))
 				CommandCounterIncrement();
 			table_close(childrel, NoLock);
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 83f97f684d5..b10a400d6da 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -1832,3 +1832,169 @@ COMMENT ON CONSTRAINT inv_ck ON DOMAIN constraint_comments_dom IS 'comment on in
 CREATE TABLE regress_notnull1 (a integer);
 CREATE TABLE regress_notnull2 () INHERITS (regress_notnull1);
 ALTER TABLE ONLY regress_notnull2 ALTER COLUMN a SET NOT NULL;
+-- Not-null constraints in inheritance trees: validity must be consistent.
+-- A validated constraint must not sit above a NOT VALID one, because the
+-- descendant may contain nulls.  nnv_state() summarizes column "a" of the
+-- nnv_* tables: V or N (valid or NOT VALID constraint), coninhcount, L if
+-- conislocal, I if connoinherit, and "+" if attnotnull; "-" is no constraint.
+\pset format unaligned
+\pset tuples_only on
+\set VERBOSITY terse
+SET client_min_messages = warning;
+CREATE FUNCTION nnv_state() RETURNS text LANGUAGE sql AS
+$$ SELECT string_agg(substr(c.relname, 5) || '=' ||
+         coalesce(CASE WHEN co.convalidated THEN 'V' ELSE 'N' END ||
+                  co.coninhcount || CASE WHEN co.conislocal THEN 'L' ELSE '' END ||
+                  CASE WHEN co.connoinherit THEN 'I' ELSE '' END, '-') ||
+         CASE WHEN a.attnotnull THEN '+' ELSE '' END, ' ' ORDER BY c.relname)
+   FROM pg_class c
+        JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = 'a'
+        LEFT JOIN pg_constraint co ON co.conrelid = c.oid AND co.contype = 'n'
+                                  AND co.conkey[1] = a.attnum
+   WHERE c.relname ~ '^nnv_' AND c.relkind IN ('r', 'p') $$;
+-- SET NOT NULL must not propagate a valid constraint to a NOT VALID one,
+-- and the failure must undo the other subcommands
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_c (a int) INHERITS (nnv_p);
+INSERT INTO nnv_c VALUES (NULL);
+ALTER TABLE nnv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE nnv_p ADD COLUMN b int, ALTER COLUMN a SET NOT NULL;	-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnv_c"
+SELECT nnv_state(), count(*) FROM pg_attribute WHERE attname = 'b'
+  AND attrelid IN ('nnv_p'::regclass, 'nnv_c'::regclass);
+c=N0L+ p=-|0
+DELETE FROM nnv_c;
+ALTER TABLE nnv_c VALIDATE CONSTRAINT nn;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;			-- repeat
+SELECT nnv_state();
+c=V1L+ p=V0L+
+DROP TABLE nnv_p, nnv_c;
+-- same for a partition holding an old null; wrong results must not occur
+CREATE TABLE nnv_p (a int) PARTITION BY LIST (a);
+CREATE TABLE nnv_p1 PARTITION OF nnv_p FOR VALUES IN (1);
+CREATE TABLE nnv_pd PARTITION OF nnv_p DEFAULT;
+INSERT INTO nnv_p VALUES (1);
+INSERT INTO nnv_pd VALUES (NULL);
+ALTER TABLE nnv_pd ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CREATE TABLE nnv_o (x int NOT NULL);
+INSERT INTO nnv_o VALUES (2);
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;			-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnv_pd"
+ALTER TABLE ONLY nnv_p ALTER COLUMN a SET NOT NULL;		-- error
+ERROR:  constraint must be added to child tables too
+SELECT nnv_state();
+p=- p1=- pd=N0L+
+SELECT count(*) FROM nnv_p WHERE a IS NULL;
+1
+SELECT count(*) FROM nnv_o WHERE x NOT IN (SELECT a FROM nnv_p);
+0
+DELETE FROM nnv_pd;
+ALTER TABLE nnv_pd VALIDATE CONSTRAINT nn;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;
+SELECT nnv_state();
+p=V0L+ p1=V1+ pd=V1L+
+SELECT count(*) FROM nnv_o WHERE x NOT IN (SELECT a FROM nnv_p);
+1
+DROP TABLE nnv_p, nnv_o;
+-- a valid constraint cannot go through a missing intermediate constraint to
+-- a NOT VALID leaf, by SET NOT NULL or by INHERIT
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_m (a int) INHERITS (nnv_p);
+CREATE TABLE nnv_c (a int) INHERITS (nnv_m);
+ALTER TABLE nnv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;			-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnv_c"
+ALTER TABLE nnv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT;
+ALTER TABLE nnv_p ALTER CONSTRAINT nn INHERIT;			-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnv_c"
+SELECT nnv_state();			-- nnv_m still has no constraint
+c=N0L+ m=- p=V0LI+
+DROP TABLE nnv_p, nnv_m, nnv_c;
+-- A NOT VALID parent constraint, made inheritable, gets a NOT VALID one
+-- created for each descendant that lacks it (nnv_m), and accepts existing
+-- NOT VALID (nnv_c) or valid (nnv_c0) ones.  The old nulls in nnv_m and
+-- nnv_c are no problem: nothing is scanned, but the new constraint on nnv_m
+-- is enforced.
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_m (a int) INHERITS (nnv_p);
+CREATE TABLE nnv_c (a int) INHERITS (nnv_m);
+CREATE TABLE nnv_c0 (a int NOT NULL) INHERITS (nnv_m);
+INSERT INTO nnv_m VALUES (NULL);
+INSERT INTO nnv_c VALUES (NULL);
+ALTER TABLE nnv_c ADD CONSTRAINT leaf NOT NULL a NOT VALID;
+ALTER TABLE nnv_p ADD CONSTRAINT root NOT NULL a NO INHERIT NOT VALID;
+ALTER TABLE nnv_p ALTER CONSTRAINT root INHERIT;
+ALTER TABLE nnv_p ALTER CONSTRAINT root INHERIT;		-- repeat
+SELECT nnv_state();
+c=N1L+ c0=V1L+ m=N1+ p=N0L+
+SELECT conenforced FROM pg_constraint
+  WHERE conrelid = 'nnv_m'::regclass AND contype = 'n';
+t
+SELECT count(*) FROM nnv_p WHERE a IS NULL;		-- old nulls remain
+2
+INSERT INTO nnv_m VALUES (NULL);				-- error
+ERROR:  null value in column "a" of relation "nnv_m" violates not-null constraint
+DROP TABLE nnv_p, nnv_m, nnv_c, nnv_c0;
+-- diamond with a further descendant: parent counts, no double counting
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_m1 () INHERITS (nnv_p);
+CREATE TABLE nnv_m2 () INHERITS (nnv_p);
+CREATE TABLE nnv_c () INHERITS (nnv_m1, nnv_m2);
+CREATE TABLE nnv_d () INHERITS (nnv_c);
+ALTER TABLE nnv_p ADD CONSTRAINT root NOT NULL a NO INHERIT NOT VALID;
+ALTER TABLE nnv_p ALTER CONSTRAINT root INHERIT;
+ALTER TABLE nnv_p ALTER CONSTRAINT root INHERIT;		-- repeat
+SELECT nnv_state();
+c=N2+ d=N1+ m1=N1+ m2=N1+ p=N0L+
+DROP TABLE nnv_p, nnv_m1, nnv_m2, nnv_c, nnv_d;
+-- valid parent, NOT VALID bottom of a diamond
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_m1 () INHERITS (nnv_p);
+CREATE TABLE nnv_m2 () INHERITS (nnv_p);
+CREATE TABLE nnv_c () INHERITS (nnv_m1, nnv_m2);
+CREATE TABLE nnv_d () INHERITS (nnv_c);
+ALTER TABLE nnv_d ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;			-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnv_d"
+SELECT nnv_state();
+c=- d=N0L+ m1=- m2=- p=-
+ALTER TABLE nnv_d VALIDATE CONSTRAINT nn;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;
+SELECT nnv_state();
+c=V2+ d=V1L+ m1=V1+ m2=V1+ p=V0L+
+DROP TABLE nnv_p, nnv_m1, nnv_m2, nnv_c, nnv_d;
+-- ONLY, and a NO INHERIT constraint below
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_c (a int) INHERITS (nnv_p);
+ALTER TABLE nnv_c ADD CONSTRAINT nn NOT NULL a NO INHERIT NOT VALID;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;			-- error
+ERROR:  cannot change NO INHERIT status of NOT NULL constraint "nn" on relation "nnv_c"
+ALTER TABLE ONLY nnv_p ALTER COLUMN a SET NOT NULL;		-- ok, NO INHERIT
+SELECT nnv_state();
+c=N0LI+ p=V0LI+
+DROP TABLE nnv_p, nnv_c;
+-- SET NOT NULL on an inherited-only NOT VALID constraint validates it and
+-- makes it local at once; a failure undoes both
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_m (a int) INHERITS (nnv_p);
+CREATE TABLE nnv_c (a int) INHERITS (nnv_m);
+INSERT INTO nnv_c VALUES (NULL);
+ALTER TABLE nnv_p ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE nnv_m ALTER COLUMN a SET NOT NULL;			-- error
+ERROR:  column "a" of relation "nnv_c" contains null values
+ALTER TABLE ONLY nnv_m ALTER COLUMN a SET NOT NULL;		-- error
+ERROR:  constraint must be validated on child tables too
+SELECT nnv_state();
+c=N1+ m=N1+ p=N0L+
+DELETE FROM nnv_c;
+ALTER TABLE nnv_m ALTER COLUMN a SET NOT NULL;
+ALTER TABLE nnv_m ALTER COLUMN a SET NOT NULL;			-- repeat
+SELECT nnv_state();			-- nnv_m and nnv_c valid, only nnv_m local
+c=V1+ m=V1L+ p=N0L+
+DROP TABLE nnv_p, nnv_m, nnv_c;
+DROP FUNCTION nnv_state();
+RESET client_min_messages;
+\set VERBOSITY default
+\pset tuples_only off
+\pset format aligned
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 9705962eb9f..caf094924fc 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -1118,3 +1118,150 @@ COMMENT ON CONSTRAINT inv_ck ON DOMAIN constraint_comments_dom IS 'comment on in
 CREATE TABLE regress_notnull1 (a integer);
 CREATE TABLE regress_notnull2 () INHERITS (regress_notnull1);
 ALTER TABLE ONLY regress_notnull2 ALTER COLUMN a SET NOT NULL;
+-- Not-null constraints in inheritance trees: validity must be consistent.
+-- A validated constraint must not sit above a NOT VALID one, because the
+-- descendant may contain nulls.  nnv_state() summarizes column "a" of the
+-- nnv_* tables: V or N (valid or NOT VALID constraint), coninhcount, L if
+-- conislocal, I if connoinherit, and "+" if attnotnull; "-" is no constraint.
+\pset format unaligned
+\pset tuples_only on
+\set VERBOSITY terse
+SET client_min_messages = warning;
+CREATE FUNCTION nnv_state() RETURNS text LANGUAGE sql AS
+$$ SELECT string_agg(substr(c.relname, 5) || '=' ||
+         coalesce(CASE WHEN co.convalidated THEN 'V' ELSE 'N' END ||
+                  co.coninhcount || CASE WHEN co.conislocal THEN 'L' ELSE '' END ||
+                  CASE WHEN co.connoinherit THEN 'I' ELSE '' END, '-') ||
+         CASE WHEN a.attnotnull THEN '+' ELSE '' END, ' ' ORDER BY c.relname)
+   FROM pg_class c
+        JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = 'a'
+        LEFT JOIN pg_constraint co ON co.conrelid = c.oid AND co.contype = 'n'
+                                  AND co.conkey[1] = a.attnum
+   WHERE c.relname ~ '^nnv_' AND c.relkind IN ('r', 'p') $$;
+
+-- SET NOT NULL must not propagate a valid constraint to a NOT VALID one,
+-- and the failure must undo the other subcommands
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_c (a int) INHERITS (nnv_p);
+INSERT INTO nnv_c VALUES (NULL);
+ALTER TABLE nnv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE nnv_p ADD COLUMN b int, ALTER COLUMN a SET NOT NULL;	-- error
+SELECT nnv_state(), count(*) FROM pg_attribute WHERE attname = 'b'
+  AND attrelid IN ('nnv_p'::regclass, 'nnv_c'::regclass);
+DELETE FROM nnv_c;
+ALTER TABLE nnv_c VALIDATE CONSTRAINT nn;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;			-- repeat
+SELECT nnv_state();
+DROP TABLE nnv_p, nnv_c;
+
+-- same for a partition holding an old null; wrong results must not occur
+CREATE TABLE nnv_p (a int) PARTITION BY LIST (a);
+CREATE TABLE nnv_p1 PARTITION OF nnv_p FOR VALUES IN (1);
+CREATE TABLE nnv_pd PARTITION OF nnv_p DEFAULT;
+INSERT INTO nnv_p VALUES (1);
+INSERT INTO nnv_pd VALUES (NULL);
+ALTER TABLE nnv_pd ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CREATE TABLE nnv_o (x int NOT NULL);
+INSERT INTO nnv_o VALUES (2);
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;			-- error
+ALTER TABLE ONLY nnv_p ALTER COLUMN a SET NOT NULL;		-- error
+SELECT nnv_state();
+SELECT count(*) FROM nnv_p WHERE a IS NULL;
+SELECT count(*) FROM nnv_o WHERE x NOT IN (SELECT a FROM nnv_p);
+DELETE FROM nnv_pd;
+ALTER TABLE nnv_pd VALIDATE CONSTRAINT nn;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;
+SELECT nnv_state();
+SELECT count(*) FROM nnv_o WHERE x NOT IN (SELECT a FROM nnv_p);
+DROP TABLE nnv_p, nnv_o;
+
+-- a valid constraint cannot go through a missing intermediate constraint to
+-- a NOT VALID leaf, by SET NOT NULL or by INHERIT
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_m (a int) INHERITS (nnv_p);
+CREATE TABLE nnv_c (a int) INHERITS (nnv_m);
+ALTER TABLE nnv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;			-- error
+ALTER TABLE nnv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT;
+ALTER TABLE nnv_p ALTER CONSTRAINT nn INHERIT;			-- error
+SELECT nnv_state();			-- nnv_m still has no constraint
+DROP TABLE nnv_p, nnv_m, nnv_c;
+
+-- A NOT VALID parent constraint, made inheritable, gets a NOT VALID one
+-- created for each descendant that lacks it (nnv_m), and accepts existing
+-- NOT VALID (nnv_c) or valid (nnv_c0) ones.  The old nulls in nnv_m and
+-- nnv_c are no problem: nothing is scanned, but the new constraint on nnv_m
+-- is enforced.
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_m (a int) INHERITS (nnv_p);
+CREATE TABLE nnv_c (a int) INHERITS (nnv_m);
+CREATE TABLE nnv_c0 (a int NOT NULL) INHERITS (nnv_m);
+INSERT INTO nnv_m VALUES (NULL);
+INSERT INTO nnv_c VALUES (NULL);
+ALTER TABLE nnv_c ADD CONSTRAINT leaf NOT NULL a NOT VALID;
+ALTER TABLE nnv_p ADD CONSTRAINT root NOT NULL a NO INHERIT NOT VALID;
+ALTER TABLE nnv_p ALTER CONSTRAINT root INHERIT;
+ALTER TABLE nnv_p ALTER CONSTRAINT root INHERIT;		-- repeat
+SELECT nnv_state();
+SELECT conenforced FROM pg_constraint
+  WHERE conrelid = 'nnv_m'::regclass AND contype = 'n';
+SELECT count(*) FROM nnv_p WHERE a IS NULL;		-- old nulls remain
+INSERT INTO nnv_m VALUES (NULL);				-- error
+DROP TABLE nnv_p, nnv_m, nnv_c, nnv_c0;
+
+-- diamond with a further descendant: parent counts, no double counting
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_m1 () INHERITS (nnv_p);
+CREATE TABLE nnv_m2 () INHERITS (nnv_p);
+CREATE TABLE nnv_c () INHERITS (nnv_m1, nnv_m2);
+CREATE TABLE nnv_d () INHERITS (nnv_c);
+ALTER TABLE nnv_p ADD CONSTRAINT root NOT NULL a NO INHERIT NOT VALID;
+ALTER TABLE nnv_p ALTER CONSTRAINT root INHERIT;
+ALTER TABLE nnv_p ALTER CONSTRAINT root INHERIT;		-- repeat
+SELECT nnv_state();
+DROP TABLE nnv_p, nnv_m1, nnv_m2, nnv_c, nnv_d;
+-- valid parent, NOT VALID bottom of a diamond
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_m1 () INHERITS (nnv_p);
+CREATE TABLE nnv_m2 () INHERITS (nnv_p);
+CREATE TABLE nnv_c () INHERITS (nnv_m1, nnv_m2);
+CREATE TABLE nnv_d () INHERITS (nnv_c);
+ALTER TABLE nnv_d ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;			-- error
+SELECT nnv_state();
+ALTER TABLE nnv_d VALIDATE CONSTRAINT nn;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;
+SELECT nnv_state();
+DROP TABLE nnv_p, nnv_m1, nnv_m2, nnv_c, nnv_d;
+
+-- ONLY, and a NO INHERIT constraint below
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_c (a int) INHERITS (nnv_p);
+ALTER TABLE nnv_c ADD CONSTRAINT nn NOT NULL a NO INHERIT NOT VALID;
+ALTER TABLE nnv_p ALTER COLUMN a SET NOT NULL;			-- error
+ALTER TABLE ONLY nnv_p ALTER COLUMN a SET NOT NULL;		-- ok, NO INHERIT
+SELECT nnv_state();
+DROP TABLE nnv_p, nnv_c;
+
+-- SET NOT NULL on an inherited-only NOT VALID constraint validates it and
+-- makes it local at once; a failure undoes both
+CREATE TABLE nnv_p (a int);
+CREATE TABLE nnv_m (a int) INHERITS (nnv_p);
+CREATE TABLE nnv_c (a int) INHERITS (nnv_m);
+INSERT INTO nnv_c VALUES (NULL);
+ALTER TABLE nnv_p ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE nnv_m ALTER COLUMN a SET NOT NULL;			-- error
+ALTER TABLE ONLY nnv_m ALTER COLUMN a SET NOT NULL;		-- error
+SELECT nnv_state();
+DELETE FROM nnv_c;
+ALTER TABLE nnv_m ALTER COLUMN a SET NOT NULL;
+ALTER TABLE nnv_m ALTER COLUMN a SET NOT NULL;			-- repeat
+SELECT nnv_state();			-- nnv_m and nnv_c valid, only nnv_m local
+DROP TABLE nnv_p, nnv_m, nnv_c;
+
+DROP FUNCTION nnv_state();
+RESET client_min_messages;
+\set VERBOSITY default
+\pset tuples_only off
+\pset format aligned

base-commit: f400289946f7d0e66a4169cf4aa56079f1422a2b
-- 
2.56.0

