From a8425e0bd04e4dc15c3a1f9de6d6f796671c93b2 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 v1] Fix NOT NULL validation in ALTER TABLE

Two problems with ALTER TABLE ... ALTER COLUMN ... SET NOT NULL and
NOT VALID not-null constraints in inheritance hierarchies.

First, when SET NOT NULL creates a valid constraint on a table, it
propagates it to the children by calling ATExecSetNotNull() recursively.
If a child already had a not-null constraint, that call only incremented
its coninhcount and returned: it did not look at convalidated, and no
validation is scheduled for the existing child constraint.  A constraint
that is still NOT VALID was thus left below a validated one, although the
child may contain null values.  ALTER TABLE ... ALTER CONSTRAINT ...
INHERIT reaches the same code and has the same problem.  ADD CONSTRAINT
and ATTACH PARTITION, on the other hand, already reject such incompatible
states.

Observed consequences, on a server without this patch: for a partitioned
table whose default partition holds a null row under a NOT VALID
constraint, SET NOT NULL on the parent succeeds, and then "SELECT a FROM
parent" returns the null row while "SELECT a FROM parent WHERE a IS NULL"
returns no rows, because the planner relies on the parent's validated
constraint (the plan is a Result with a constant-false filter).  The
corresponding query on an ordinary inheritance hierarchy did return the
row, but the catalogs are equally inconsistent there.  VALIDATE CONSTRAINT
on the parent does nothing in this state, since the parent's constraint is
already valid.  A pg_dump of such a database fails to restore: the null
row is loaded into a child that has inherited a valid NOT NULL, and
re-adding the child's NOT VALID constraint can additionally fail on a
constraint name mismatch; for partitions the restore stops at ATTACH
PARTITION.

Make the recursive call fail, with the same error text, errcode and hint
as AdjustNotNullInheritance(), if the constraint being propagated is
valid and the existing child constraint is not.  The error is raised
while executing the command, so all of its changes are rolled back.  Users
validate the descendant's constraint first and then retry.  Existing
constraints that are valid, or NOT VALID ones under a NOT VALID parent,
continue to be accepted, which is why a new argument tells the function
whether the propagated constraint is validated.  ALTER CONSTRAINT ...
INHERIT passes the validity of the constraint it is changing; the
recursion after creating a missing constraint passes true, because the
constraint just created is valid, whatever the validity of the original
parent.  In particular, INHERIT on a NOT VALID NO INHERIT constraint, with
a missing constraint on an intermediate table and an existing NOT VALID
one on a leaf, is rejected now, where it used to leave a valid constraint
on the intermediate table above the NOT VALID leaf.

Second, SET NOT NULL validates a NOT VALID not-null constraint that the
table already has, but ATExecSetNotNull() chose between its actions with
an if/else chain in which "make an inherited constraint local" excluded
"validate it".  For a constraint that was NOT VALID and only inherited,
the first SET NOT NULL on the child therefore just set conislocal and
returned successfully, with the constraint still NOT VALID; a second
identical command was needed to validate it.  Do the localization and the
validation independently.  Validation reads the pg_constraint row again
and writes back a modified copy of it, so advance the command counter
after the conislocal update; otherwise the write would be based on the old
row and undo the update.  coninhcount is not touched, so the counts stay
correct, including with diamond inheritance.  If validation fails, because
of null values or because ONLY was used on a table that has descendants,
the whole command is rolled back, including the change to conislocal.

This does not detect or repair constraints that an unpatched server has
already left in an inconsistent state; ALTER TABLE ... VALIDATE
CONSTRAINT on the descendant is required for that.

Regression tests cover inheritance and partitioning, three-level and
diamond hierarchies, multiple parents, repeated commands, ONLY, NO
INHERIT, ALTER CONSTRAINT ... INHERIT with valid and NOT VALID parents,
and the direct SET NOT NULL cases; after each rejected command they check
that all relevant pg_constraint and pg_attribute state is unchanged.  The
documentation is updated.
---
 doc/src/sgml/ref/alter_table.sgml         |  15 +-
 src/backend/commands/tablecmds.c          |  56 +-
 src/test/regress/expected/constraints.out | 756 ++++++++++++++++++++++
 src/test/regress/sql/constraints.sql      | 360 +++++++++++
 4 files changed, 1172 insertions(+), 15 deletions(-)

diff --git a/doc/src/sgml/ref/alter_table.sgml b/doc/src/sgml/ref/alter_table.sgml
index 0f9d698d170..783fd031852 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,10 @@ 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.  If the constraint being marked inheritable is
+      valid, the command fails when a child already has an invalid not-null
+      constraint on the column; that constraint 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..62f27606607 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,17 @@ 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.  It 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 +8064,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 +8090,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 +8102,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
@@ -8159,7 +8188,7 @@ ATExecSetNotNull(List **wqueue, Relation rel, char *conName, char *colName,
 			CommandCounterIncrement();
 
 			ATExecSetNotNull(wqueue, childrel, conName, colName,
-							 recurse, true, lockmode);
+							 recurse, true, true, lockmode);
 			table_close(childrel, NoLock);
 		}
 	}
@@ -13160,7 +13189,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..f37f950a11a 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -1791,6 +1791,762 @@ COMMENT ON CONSTRAINT constr_parent2_a_not_null ON constr_parent2 IS 'this const
 COMMENT ON CONSTRAINT constr_parent2_a_not_null ON constr_child2 IS 'this constraint is valid';
 DEALLOCATE get_nnconstraint_info;
 -- end NOT NULL NOT VALID
+-- ALTER TABLE ... SET NOT NULL (and ALTER CONSTRAINT ... INHERIT) must not
+-- propagate a validated not-null constraint to a descendant that has an
+-- existing NOT VALID one, because the descendant might contain nulls.
+-- nnsv_info shows the state of the constraints involved; nnsv_snapshot() and
+-- nnsv_unchanged() check that a failed command left all of it untouched.
+CREATE VIEW nnsv_info AS
+  SELECT c.relname, a.attnotnull, co.conname, co.convalidated AS valid,
+         co.conenforced AS enforced, co.coninhcount AS inhcnt,
+         co.conislocal AS islocal, co.connoinherit AS noinherit
+  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 ~ '^nnsv_' AND c.relkind IN ('r', 'p')
+        AND pg_table_is_visible(c.oid)
+  ORDER BY c.relname, co.conname;
+CREATE TEMP TABLE nnsv_snap AS SELECT * FROM nnsv_info WITH NO DATA;
+CREATE PROCEDURE nnsv_snapshot() LANGUAGE plpgsql AS
+$$ BEGIN TRUNCATE nnsv_snap; INSERT INTO nnsv_snap SELECT * FROM nnsv_info; END $$;
+CREATE FUNCTION nnsv_unchanged() RETURNS bool LANGUAGE plpgsql AS
+$$ BEGIN
+  RETURN NOT EXISTS (SELECT * FROM nnsv_snap EXCEPT SELECT * FROM nnsv_info)
+     AND NOT EXISTS (SELECT * FROM nnsv_info EXCEPT SELECT * FROM nnsv_snap);
+END $$;
+-- inheritance, descendant has a null and a NOT VALID constraint
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+INSERT INTO nnsv_c VALUES (NULL);
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a;		-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnsv_c"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;			-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnsv_c"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn      | f     | t        |      0 | t       | f
+ nnsv_p  | f          |         |       |          |        |         | 
+(2 rows)
+
+SELECT count(*) FROM nnsv_p WHERE a IS NULL;			-- still finds the null
+ count 
+-------
+     1
+(1 row)
+
+-- after repairing the descendant, SET NOT NULL works and is idempotent
+DELETE FROM nnsv_c;
+ALTER TABLE nnsv_c VALIDATE CONSTRAINT nn;
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;
+ relname | attnotnull |      conname      | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+-------------------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn                | t     | t        |      1 | t       | f
+ nnsv_p  | t          | nnsv_p_a_not_null | t     | t        |      0 | t       | f
+(2 rows)
+
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+DROP TABLE nnsv_p, nnsv_c;
+-- the other subcommands of a failing command are rolled back as well
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ADD COLUMN b int, ALTER COLUMN a SET NOT NULL;	-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnsv_c"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+SELECT count(*) FROM pg_attribute
+  WHERE attrelid IN ('nnsv_p'::regclass, 'nnsv_c'::regclass) AND attname = 'b';
+ count 
+-------
+     0
+(1 row)
+
+DROP TABLE nnsv_p, nnsv_c;
+-- partitioning: the parent must not claim "no nulls" while a partition has some
+CREATE TABLE nnsv_pp (a int) PARTITION BY LIST (a);
+CREATE TABLE nnsv_pc PARTITION OF nnsv_pp DEFAULT;
+INSERT INTO nnsv_pc VALUES (NULL);
+ALTER TABLE nnsv_pc ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_pp ALTER COLUMN a SET NOT NULL;		-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnsv_pc"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_pc | t          | nn      | f     | t        |      0 | t       | f
+ nnsv_pp | f          |         |       |          |        |         | 
+(2 rows)
+
+SELECT tableoid::regclass, a FROM nnsv_pp;
+ tableoid | a 
+----------+---
+ nnsv_pc  |  
+(1 row)
+
+SELECT tableoid::regclass, a FROM nnsv_pp WHERE a IS NULL;
+ tableoid | a 
+----------+---
+ nnsv_pc  |  
+(1 row)
+
+DELETE FROM nnsv_pc;
+ALTER TABLE nnsv_pc VALIDATE CONSTRAINT nn;
+ALTER TABLE nnsv_pp ALTER COLUMN a SET NOT NULL;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_pp ALTER COLUMN a SET NOT NULL;
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+SELECT * FROM nnsv_info;
+ relname | attnotnull |      conname       | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+--------------------+-------+----------+--------+---------+-----------
+ nnsv_pc | t          | nn                 | t     | t        |      1 | t       | f
+ nnsv_pp | t          | nnsv_pp_a_not_null | t     | t        |      0 | t       | f
+(2 rows)
+
+INSERT INTO nnsv_pp VALUES (NULL);				-- error
+ERROR:  null value in column "a" of relation "nnsv_pc" violates not-null constraint
+DETAIL:  Failing row contains (null).
+-- ONLY is not possible for a partitioned table with partitions
+ALTER TABLE nnsv_pp ALTER COLUMN a DROP NOT NULL;
+CALL nnsv_snapshot();
+ALTER TABLE ONLY nnsv_pp ALTER COLUMN a SET NOT NULL;		-- error
+ERROR:  constraint must be added to child tables too
+HINT:  Do not specify the ONLY keyword.
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+DROP TABLE nnsv_pp;
+-- three levels: invalid constraint at the bottom, in the middle, or both
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;			-- error, nnsv_m is new
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnsv_c"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+ALTER TABLE nnsv_c VALIDATE CONSTRAINT nn;
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;
+ relname | attnotnull |      conname      | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+-------------------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn                | t     | t        |      1 | t       | f
+ nnsv_m  | t          | nnsv_p_a_not_null | t     | t        |      1 | f       | f
+ nnsv_p  | t          | nnsv_p_a_not_null | t     | t        |      0 | t       | f
+(3 rows)
+
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_m ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;			-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnsv_m"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn      | f     | t        |      1 | f       | f
+ nnsv_m  | t          | nn      | f     | t        |      0 | t       | f
+ nnsv_p  | f          |         |       |          |        |         | 
+(3 rows)
+
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+-- diamond: a shared descendant and a further one below it
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m1 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_m2 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_c () INHERITS (nnsv_m1, nnsv_m2);
+NOTICE:  merging multiple inherited definitions of column "a"
+CREATE TABLE nnsv_d () INHERITS (nnsv_c);
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;		-- nnsv_c has two parents, nnsv_d one
+ relname | attnotnull |      conname      | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+-------------------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nnsv_p_a_not_null | t     | t        |      2 | f       | f
+ nnsv_d  | t          | nnsv_p_a_not_null | t     | t        |      1 | f       | f
+ nnsv_m1 | t          | nnsv_p_a_not_null | t     | t        |      1 | f       | f
+ nnsv_m2 | t          | nnsv_p_a_not_null | t     | t        |      1 | f       | f
+ nnsv_p  | t          | nnsv_p_a_not_null | t     | t        |      0 | t       | f
+(5 rows)
+
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+DROP TABLE nnsv_p, nnsv_m1, nnsv_m2, nnsv_c, nnsv_d;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m1 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_m2 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_c () INHERITS (nnsv_m1, nnsv_m2);
+NOTICE:  merging multiple inherited definitions of column "a"
+CREATE TABLE nnsv_d () INHERITS (nnsv_c);
+ALTER TABLE nnsv_d ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;			-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnsv_d"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;	-- now invalid in the middle, too
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;			-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnsv_c"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn      | f     | t        |      0 | t       | f
+ nnsv_d  | t          | nn      | f     | t        |      1 | t       | f
+ nnsv_m1 | f          |         |       |          |        |         | 
+ nnsv_m2 | f          |         |       |          |        |         | 
+ nnsv_p  | f          |         |       |          |        |         | 
+(5 rows)
+
+DROP TABLE nnsv_p, nnsv_m1, nnsv_m2, nnsv_c, nnsv_d;
+-- diamond whose shared descendant already has a valid constraint
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m1 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_m2 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_c (a int NOT NULL) INHERITS (nnsv_m1, nnsv_m2);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging column "a" with inherited definition
+CREATE TABLE nnsv_d () INHERITS (nnsv_c);
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;
+ relname | attnotnull |      conname      | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+-------------------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nnsv_c_a_not_null | t     | t        |      2 | t       | f
+ nnsv_d  | t          | nnsv_c_a_not_null | t     | t        |      1 | f       | f
+ nnsv_m1 | t          | nnsv_p_a_not_null | t     | t        |      1 | f       | f
+ nnsv_m2 | t          | nnsv_p_a_not_null | t     | t        |      1 | f       | f
+ nnsv_p  | t          | nnsv_p_a_not_null | t     | t        |      0 | t       | f
+(5 rows)
+
+DROP TABLE nnsv_p, nnsv_m1, nnsv_m2, nnsv_c, nnsv_d;
+-- multiple direct parents: a failure must not change any inheritance count
+CREATE TABLE nnsv_p1 (a int);
+CREATE TABLE nnsv_p2 (a int);
+CREATE TABLE nnsv_c () INHERITS (nnsv_p1, nnsv_p2);
+NOTICE:  merging multiple inherited definitions of column "a"
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p1 ALTER COLUMN a SET NOT NULL;		-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnsv_c"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+ALTER TABLE nnsv_p2 ALTER COLUMN a SET NOT NULL;		-- error
+ERROR:  incompatible NOT VALID constraint "nn" on relation "nnsv_c"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+ALTER TABLE nnsv_c VALIDATE CONSTRAINT nn;
+ALTER TABLE nnsv_p1 ALTER COLUMN a SET NOT NULL;
+ALTER TABLE nnsv_p2 ALTER COLUMN a SET NOT NULL;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p1 ALTER COLUMN a SET NOT NULL;
+ALTER TABLE nnsv_p2 ALTER COLUMN a SET NOT NULL;
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+SELECT * FROM nnsv_info;
+ relname | attnotnull |      conname       | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+--------------------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn                 | t     | t        |      2 | t       | f
+ nnsv_p1 | t          | nnsv_p1_a_not_null | t     | t        |      0 | t       | f
+ nnsv_p2 | t          | nnsv_p2_a_not_null | t     | t        |      0 | t       | f
+(3 rows)
+
+DROP TABLE nnsv_p1, nnsv_p2, nnsv_c;
+-- ONLY, and NO INHERIT constraints on the descendant
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE ONLY nnsv_p ALTER COLUMN a SET NOT NULL;		-- ok, NO INHERIT
+SELECT * FROM nnsv_info;
+ relname | attnotnull |      conname      | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+-------------------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn                | f     | t        |      0 | t       | f
+ nnsv_p  | t          | nnsv_p_a_not_null | t     | t        |      0 | t       | t
+(2 rows)
+
+DROP TABLE nnsv_p, nnsv_c;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NO INHERIT NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;			-- error
+ERROR:  cannot change NO INHERIT status of NOT NULL constraint "nn" on relation "nnsv_c"
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+DROP TABLE nnsv_p, nnsv_c;
+-- ALTER CONSTRAINT ... INHERIT propagates the constraint the same way
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT;
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;			-- child has none
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn      | t     | t        |      1 | f       | f
+ nnsv_p  | t          | nn      | t     | t        |      0 | t       | f
+(2 rows)
+
+DROP TABLE nnsv_p, nnsv_c;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int NOT NULL) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT;
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;			-- child is valid
+SELECT * FROM nnsv_info;
+ relname | attnotnull |      conname      | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+-------------------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nnsv_c_a_not_null | t     | t        |      1 | t       | f
+ nnsv_p  | t          | nn                | t     | t        |      0 | t       | f
+(2 rows)
+
+DROP TABLE nnsv_p, nnsv_c;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+INSERT INTO nnsv_c VALUES (NULL);
+ALTER TABLE nnsv_c ADD CONSTRAINT nn2 NOT NULL a NOT VALID;
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;			-- error
+ERROR:  incompatible NOT VALID constraint "nn2" on relation "nnsv_c"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn2     | f     | t        |      0 | t       | f
+ nnsv_p  | t          | nn      | t     | t        |      0 | t       | t
+(2 rows)
+
+DROP TABLE nnsv_p, nnsv_c;
+-- a NOT VALID parent constraint may be inherited by NOT VALID or valid children
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+INSERT INTO nnsv_c VALUES (NULL);
+ALTER TABLE nnsv_c ADD CONSTRAINT nn2 NOT NULL a NOT VALID;
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT NOT VALID;
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn2     | f     | t        |      1 | t       | f
+ nnsv_p  | t          | nn      | f     | t        |      0 | t       | f
+(2 rows)
+
+DROP TABLE nnsv_p, nnsv_c;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int NOT NULL) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT NOT VALID;
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;
+SELECT * FROM nnsv_info;
+ relname | attnotnull |      conname      | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+-------------------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nnsv_c_a_not_null | t     | t        |      1 | t       | f
+ nnsv_p  | t          | nn                | f     | t        |      0 | t       | f
+(2 rows)
+
+DROP TABLE nnsv_p, nnsv_c;
+-- three levels, valid parent, no constraint in the middle
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_c ADD CONSTRAINT nn2 NOT NULL a NOT VALID;
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;			-- error
+ERROR:  incompatible NOT VALID constraint "nn2" on relation "nnsv_c"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+CREATE TABLE nnsv_c (a int NOT NULL) INHERITS (nnsv_m);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT;
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;
+SELECT * FROM nnsv_info;
+ relname | attnotnull |      conname      | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+-------------------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nnsv_c_a_not_null | t     | t        |      1 | t       | f
+ nnsv_m  | t          | nn                | t     | t        |      1 | f       | f
+ nnsv_p  | t          | nn                | t     | t        |      0 | t       | f
+(3 rows)
+
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+-- A NOT VALID parent constraint, no constraint in the middle, NOT VALID leaf:
+-- the constraint created for the middle table is valid, so it cannot be
+-- inherited by the leaf, even though the parent's own constraint is not valid
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_c ADD CONSTRAINT leafnn NOT NULL a NOT VALID;
+ALTER TABLE nnsv_p ADD CONSTRAINT rootnn NOT NULL a NO INHERIT NOT VALID;
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | leafnn  | f     | t        |      0 | t       | f
+ nnsv_m  | f          |         |       |          |        |         | 
+ nnsv_p  | t          | rootnn  | f     | t        |      0 | t       | t
+(3 rows)
+
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER CONSTRAINT rootnn INHERIT;		-- error
+ERROR:  incompatible NOT VALID constraint "leafnn" on relation "nnsv_c"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+SELECT * FROM nnsv_info;	-- nnsv_m still has no constraint, a is nullable
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | leafnn  | f     | t        |      0 | t       | f
+ nnsv_m  | f          |         |       |          |        |         | 
+ nnsv_p  | t          | rootnn  | f     | t        |      0 | t       | t
+(3 rows)
+
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+-- same, with a null in the leaf
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+NOTICE:  merging column "a" with inherited definition
+INSERT INTO nnsv_c VALUES (NULL);
+ALTER TABLE nnsv_c ADD CONSTRAINT leafnn NOT NULL a NOT VALID;
+ALTER TABLE nnsv_p ADD CONSTRAINT rootnn NOT NULL a NO INHERIT NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER CONSTRAINT rootnn INHERIT;		-- error
+ERROR:  incompatible NOT VALID constraint "leafnn" on relation "nnsv_c"
+HINT:  You might need to validate it using ALTER TABLE ... VALIDATE CONSTRAINT.
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | leafnn  | f     | t        |      0 | t       | f
+ nnsv_m  | f          |         |       |          |        |         | 
+ nnsv_p  | t          | rootnn  | f     | t        |      0 | t       | t
+(3 rows)
+
+SELECT count(*) FROM nnsv_p WHERE a IS NULL;
+ count 
+-------
+     1
+(1 row)
+
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+-- with a valid leaf the same command works
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+CREATE TABLE nnsv_c (a int NOT NULL) INHERITS (nnsv_m);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_p ADD CONSTRAINT rootnn NOT NULL a NO INHERIT NOT VALID;
+ALTER TABLE nnsv_p ALTER CONSTRAINT rootnn INHERIT;
+SELECT * FROM nnsv_info;
+ relname | attnotnull |      conname      | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+-------------------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nnsv_c_a_not_null | t     | t        |      1 | t       | f
+ nnsv_m  | t          | rootnn            | t     | t        |      1 | f       | f
+ nnsv_p  | t          | rootnn            | f     | t        |      0 | t       | f
+(3 rows)
+
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+-- SET NOT NULL on a table whose not-null constraint is inherited only and
+-- NOT VALID must validate it right away, while making it local as well
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NOT VALID;
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn      | f     | t        |      1 | f       | f
+ nnsv_p  | t          | nn      | f     | t        |      0 | t       | f
+(2 rows)
+
+ALTER TABLE nnsv_c ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;	-- nnsv_c: valid, now local; nnsv_p is unchanged
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn      | t     | t        |      1 | t       | f
+ nnsv_p  | t          | nn      | f     | t        |      0 | t       | f
+(2 rows)
+
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_c ALTER COLUMN a SET NOT NULL;
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+DROP TABLE nnsv_p, nnsv_c;
+-- a failing validation must also undo the change to conislocal
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+INSERT INTO nnsv_c VALUES (NULL);
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_c ALTER COLUMN a SET NOT NULL;			-- error
+ERROR:  column "a" of relation "nnsv_c" contains null values
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn      | f     | t        |      1 | f       | f
+ nnsv_p  | t          | nn      | f     | t        |      0 | t       | f
+(2 rows)
+
+DROP TABLE nnsv_p, nnsv_c;
+-- the constraint of a table with descendants: all of them get validated,
+-- unless ONLY is given
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE ONLY nnsv_m ALTER COLUMN a SET NOT NULL;		-- error
+ERROR:  constraint must be validated on child tables too
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+ALTER TABLE nnsv_m ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;	-- only nnsv_p is still NOT VALID
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn      | t     | t        |      1 | f       | f
+ nnsv_m  | t          | nn      | t     | t        |      1 | t       | f
+ nnsv_p  | t          | nn      | f     | t        |      0 | t       | f
+(3 rows)
+
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+NOTICE:  merging column "a" with inherited definition
+INSERT INTO nnsv_c VALUES (NULL);
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_m ALTER COLUMN a SET NOT NULL;			-- error
+ERROR:  column "a" of relation "nnsv_c" contains null values
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+-- diamond: inheritance counts come from the parents, not from the validation
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m1 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_m2 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_c () INHERITS (nnsv_m1, nnsv_m2);
+NOTICE:  merging multiple inherited definitions of column "a"
+CREATE TABLE nnsv_d () INHERITS (nnsv_c);
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE nnsv_c ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn      | t     | t        |      2 | t       | f
+ nnsv_d  | t          | nn      | t     | t        |      1 | f       | f
+ nnsv_m1 | t          | nn      | f     | t        |      1 | f       | f
+ nnsv_m2 | t          | nn      | f     | t        |      1 | f       | f
+ nnsv_p  | t          | nn      | f     | t        |      0 | t       | f
+(5 rows)
+
+ALTER TABLE nnsv_m1 ALTER COLUMN a SET NOT NULL;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_m1 ALTER COLUMN a SET NOT NULL;
+ALTER TABLE nnsv_c ALTER COLUMN a SET NOT NULL;
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn      | t     | t        |      2 | t       | f
+ nnsv_d  | t          | nn      | t     | t        |      1 | f       | f
+ nnsv_m1 | t          | nn      | t     | t        |      1 | t       | f
+ nnsv_m2 | t          | nn      | f     | t        |      1 | f       | f
+ nnsv_p  | t          | nn      | f     | t        |      0 | t       | f
+(5 rows)
+
+DROP TABLE nnsv_p, nnsv_m1, nnsv_m2, nnsv_c, nnsv_d;
+-- a local NOT VALID constraint is validated as before
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+NOTICE:  merging column "a" with inherited definition
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE nnsv_c ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_c  | t          | nn      | t     | t        |      0 | t       | f
+ nnsv_p  | f          |         |       |          |        |         | 
+(2 rows)
+
+DROP TABLE nnsv_p, nnsv_c;
+-- partitions
+CREATE TABLE nnsv_pp (a int) PARTITION BY LIST (a);
+CREATE TABLE nnsv_pc PARTITION OF nnsv_pp DEFAULT;
+INSERT INTO nnsv_pc VALUES (NULL);
+ALTER TABLE nnsv_pp ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_pc ALTER COLUMN a SET NOT NULL;		-- error
+ERROR:  column "a" of relation "nnsv_pc" contains null values
+SELECT nnsv_unchanged();
+ nnsv_unchanged 
+----------------
+ t
+(1 row)
+
+DELETE FROM nnsv_pc;
+ALTER TABLE nnsv_pc ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;
+ relname | attnotnull | conname | valid | enforced | inhcnt | islocal | noinherit 
+---------+------------+---------+-------+----------+--------+---------+-----------
+ nnsv_pc | t          | nn      | t     | t        |      1 | t       | f
+ nnsv_pp | t          | nn      | f     | t        |      0 | t       | f
+(2 rows)
+
+DROP TABLE nnsv_pp;
+DROP FUNCTION nnsv_unchanged();
+DROP PROCEDURE nnsv_snapshot();
+DROP TABLE nnsv_snap;
+DROP VIEW nnsv_info;
 -- Comments
 -- Setup a low-level role to enforce non-superuser checks.
 CREATE ROLE regress_constraint_comments;
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 9705962eb9f..37775afe61d 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -1071,6 +1071,366 @@ DEALLOCATE get_nnconstraint_info;
 
 -- end NOT NULL NOT VALID
 
+-- ALTER TABLE ... SET NOT NULL (and ALTER CONSTRAINT ... INHERIT) must not
+-- propagate a validated not-null constraint to a descendant that has an
+-- existing NOT VALID one, because the descendant might contain nulls.
+-- nnsv_info shows the state of the constraints involved; nnsv_snapshot() and
+-- nnsv_unchanged() check that a failed command left all of it untouched.
+CREATE VIEW nnsv_info AS
+  SELECT c.relname, a.attnotnull, co.conname, co.convalidated AS valid,
+         co.conenforced AS enforced, co.coninhcount AS inhcnt,
+         co.conislocal AS islocal, co.connoinherit AS noinherit
+  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 ~ '^nnsv_' AND c.relkind IN ('r', 'p')
+        AND pg_table_is_visible(c.oid)
+  ORDER BY c.relname, co.conname;
+CREATE TEMP TABLE nnsv_snap AS SELECT * FROM nnsv_info WITH NO DATA;
+CREATE PROCEDURE nnsv_snapshot() LANGUAGE plpgsql AS
+$$ BEGIN TRUNCATE nnsv_snap; INSERT INTO nnsv_snap SELECT * FROM nnsv_info; END $$;
+CREATE FUNCTION nnsv_unchanged() RETURNS bool LANGUAGE plpgsql AS
+$$ BEGIN
+  RETURN NOT EXISTS (SELECT * FROM nnsv_snap EXCEPT SELECT * FROM nnsv_info)
+     AND NOT EXISTS (SELECT * FROM nnsv_info EXCEPT SELECT * FROM nnsv_snap);
+END $$;
+
+-- inheritance, descendant has a null and a NOT VALID constraint
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+INSERT INTO nnsv_c VALUES (NULL);
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a;		-- error
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;			-- error
+SELECT nnsv_unchanged();
+SELECT * FROM nnsv_info;
+SELECT count(*) FROM nnsv_p WHERE a IS NULL;			-- still finds the null
+-- after repairing the descendant, SET NOT NULL works and is idempotent
+DELETE FROM nnsv_c;
+ALTER TABLE nnsv_c VALIDATE CONSTRAINT nn;
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;
+SELECT nnsv_unchanged();
+DROP TABLE nnsv_p, nnsv_c;
+
+-- the other subcommands of a failing command are rolled back as well
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ADD COLUMN b int, ALTER COLUMN a SET NOT NULL;	-- error
+SELECT nnsv_unchanged();
+SELECT count(*) FROM pg_attribute
+  WHERE attrelid IN ('nnsv_p'::regclass, 'nnsv_c'::regclass) AND attname = 'b';
+DROP TABLE nnsv_p, nnsv_c;
+
+-- partitioning: the parent must not claim "no nulls" while a partition has some
+CREATE TABLE nnsv_pp (a int) PARTITION BY LIST (a);
+CREATE TABLE nnsv_pc PARTITION OF nnsv_pp DEFAULT;
+INSERT INTO nnsv_pc VALUES (NULL);
+ALTER TABLE nnsv_pc ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_pp ALTER COLUMN a SET NOT NULL;		-- error
+SELECT nnsv_unchanged();
+SELECT * FROM nnsv_info;
+SELECT tableoid::regclass, a FROM nnsv_pp;
+SELECT tableoid::regclass, a FROM nnsv_pp WHERE a IS NULL;
+DELETE FROM nnsv_pc;
+ALTER TABLE nnsv_pc VALIDATE CONSTRAINT nn;
+ALTER TABLE nnsv_pp ALTER COLUMN a SET NOT NULL;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_pp ALTER COLUMN a SET NOT NULL;
+SELECT nnsv_unchanged();
+SELECT * FROM nnsv_info;
+INSERT INTO nnsv_pp VALUES (NULL);				-- error
+-- ONLY is not possible for a partitioned table with partitions
+ALTER TABLE nnsv_pp ALTER COLUMN a DROP NOT NULL;
+CALL nnsv_snapshot();
+ALTER TABLE ONLY nnsv_pp ALTER COLUMN a SET NOT NULL;		-- error
+SELECT nnsv_unchanged();
+DROP TABLE nnsv_pp;
+
+-- three levels: invalid constraint at the bottom, in the middle, or both
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;			-- error, nnsv_m is new
+SELECT nnsv_unchanged();
+ALTER TABLE nnsv_c VALIDATE CONSTRAINT nn;
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+ALTER TABLE nnsv_m ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;			-- error
+SELECT nnsv_unchanged();
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+
+-- diamond: a shared descendant and a further one below it
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m1 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_m2 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_c () INHERITS (nnsv_m1, nnsv_m2);
+CREATE TABLE nnsv_d () INHERITS (nnsv_c);
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;		-- nnsv_c has two parents, nnsv_d one
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;
+SELECT nnsv_unchanged();
+DROP TABLE nnsv_p, nnsv_m1, nnsv_m2, nnsv_c, nnsv_d;
+
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m1 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_m2 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_c () INHERITS (nnsv_m1, nnsv_m2);
+CREATE TABLE nnsv_d () INHERITS (nnsv_c);
+ALTER TABLE nnsv_d ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;			-- error
+SELECT nnsv_unchanged();
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;	-- now invalid in the middle, too
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;			-- error
+SELECT nnsv_unchanged();
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_m1, nnsv_m2, nnsv_c, nnsv_d;
+
+-- diamond whose shared descendant already has a valid constraint
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m1 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_m2 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_c (a int NOT NULL) INHERITS (nnsv_m1, nnsv_m2);
+CREATE TABLE nnsv_d () INHERITS (nnsv_c);
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_m1, nnsv_m2, nnsv_c, nnsv_d;
+
+-- multiple direct parents: a failure must not change any inheritance count
+CREATE TABLE nnsv_p1 (a int);
+CREATE TABLE nnsv_p2 (a int);
+CREATE TABLE nnsv_c () INHERITS (nnsv_p1, nnsv_p2);
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p1 ALTER COLUMN a SET NOT NULL;		-- error
+ALTER TABLE nnsv_p2 ALTER COLUMN a SET NOT NULL;		-- error
+SELECT nnsv_unchanged();
+ALTER TABLE nnsv_c VALIDATE CONSTRAINT nn;
+ALTER TABLE nnsv_p1 ALTER COLUMN a SET NOT NULL;
+ALTER TABLE nnsv_p2 ALTER COLUMN a SET NOT NULL;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p1 ALTER COLUMN a SET NOT NULL;
+ALTER TABLE nnsv_p2 ALTER COLUMN a SET NOT NULL;
+SELECT nnsv_unchanged();
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p1, nnsv_p2, nnsv_c;
+
+-- ONLY, and NO INHERIT constraints on the descendant
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE ONLY nnsv_p ALTER COLUMN a SET NOT NULL;		-- ok, NO INHERIT
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_c;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NO INHERIT NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER COLUMN a SET NOT NULL;			-- error
+SELECT nnsv_unchanged();
+DROP TABLE nnsv_p, nnsv_c;
+
+-- ALTER CONSTRAINT ... INHERIT propagates the constraint the same way
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT;
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;			-- child has none
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_c;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int NOT NULL) INHERITS (nnsv_p);
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT;
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;			-- child is valid
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_c;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+INSERT INTO nnsv_c VALUES (NULL);
+ALTER TABLE nnsv_c ADD CONSTRAINT nn2 NOT NULL a NOT VALID;
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;			-- error
+SELECT nnsv_unchanged();
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_c;
+-- a NOT VALID parent constraint may be inherited by NOT VALID or valid children
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+INSERT INTO nnsv_c VALUES (NULL);
+ALTER TABLE nnsv_c ADD CONSTRAINT nn2 NOT NULL a NOT VALID;
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT NOT VALID;
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_c;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int NOT NULL) INHERITS (nnsv_p);
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT NOT VALID;
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_c;
+-- three levels, valid parent, no constraint in the middle
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+ALTER TABLE nnsv_c ADD CONSTRAINT nn2 NOT NULL a NOT VALID;
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;			-- error
+SELECT nnsv_unchanged();
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+CREATE TABLE nnsv_c (a int NOT NULL) INHERITS (nnsv_m);
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NO INHERIT;
+ALTER TABLE nnsv_p ALTER CONSTRAINT nn INHERIT;
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+
+-- A NOT VALID parent constraint, no constraint in the middle, NOT VALID leaf:
+-- the constraint created for the middle table is valid, so it cannot be
+-- inherited by the leaf, even though the parent's own constraint is not valid
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+ALTER TABLE nnsv_c ADD CONSTRAINT leafnn NOT NULL a NOT VALID;
+ALTER TABLE nnsv_p ADD CONSTRAINT rootnn NOT NULL a NO INHERIT NOT VALID;
+SELECT * FROM nnsv_info;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER CONSTRAINT rootnn INHERIT;		-- error
+SELECT nnsv_unchanged();
+SELECT * FROM nnsv_info;	-- nnsv_m still has no constraint, a is nullable
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+-- same, with a null in the leaf
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+INSERT INTO nnsv_c VALUES (NULL);
+ALTER TABLE nnsv_c ADD CONSTRAINT leafnn NOT NULL a NOT VALID;
+ALTER TABLE nnsv_p ADD CONSTRAINT rootnn NOT NULL a NO INHERIT NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_p ALTER CONSTRAINT rootnn INHERIT;		-- error
+SELECT nnsv_unchanged();
+SELECT * FROM nnsv_info;
+SELECT count(*) FROM nnsv_p WHERE a IS NULL;
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+-- with a valid leaf the same command works
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+CREATE TABLE nnsv_c (a int NOT NULL) INHERITS (nnsv_m);
+ALTER TABLE nnsv_p ADD CONSTRAINT rootnn NOT NULL a NO INHERIT NOT VALID;
+ALTER TABLE nnsv_p ALTER CONSTRAINT rootnn INHERIT;
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+
+-- SET NOT NULL on a table whose not-null constraint is inherited only and
+-- NOT VALID must validate it right away, while making it local as well
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NOT VALID;
+SELECT * FROM nnsv_info;
+ALTER TABLE nnsv_c ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;	-- nnsv_c: valid, now local; nnsv_p is unchanged
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_c ALTER COLUMN a SET NOT NULL;
+SELECT nnsv_unchanged();
+DROP TABLE nnsv_p, nnsv_c;
+
+-- a failing validation must also undo the change to conislocal
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+INSERT INTO nnsv_c VALUES (NULL);
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_c ALTER COLUMN a SET NOT NULL;			-- error
+SELECT nnsv_unchanged();
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_c;
+
+-- the constraint of a table with descendants: all of them get validated,
+-- unless ONLY is given
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE ONLY nnsv_m ALTER COLUMN a SET NOT NULL;		-- error
+SELECT nnsv_unchanged();
+ALTER TABLE nnsv_m ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;	-- only nnsv_p is still NOT VALID
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m (a int) INHERITS (nnsv_p);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_m);
+INSERT INTO nnsv_c VALUES (NULL);
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_m ALTER COLUMN a SET NOT NULL;			-- error
+SELECT nnsv_unchanged();
+DROP TABLE nnsv_p, nnsv_m, nnsv_c;
+
+-- diamond: inheritance counts come from the parents, not from the validation
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_m1 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_m2 () INHERITS (nnsv_p);
+CREATE TABLE nnsv_c () INHERITS (nnsv_m1, nnsv_m2);
+CREATE TABLE nnsv_d () INHERITS (nnsv_c);
+ALTER TABLE nnsv_p ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE nnsv_c ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;
+ALTER TABLE nnsv_m1 ALTER COLUMN a SET NOT NULL;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_m1 ALTER COLUMN a SET NOT NULL;
+ALTER TABLE nnsv_c ALTER COLUMN a SET NOT NULL;
+SELECT nnsv_unchanged();
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_m1, nnsv_m2, nnsv_c, nnsv_d;
+
+-- a local NOT VALID constraint is validated as before
+CREATE TABLE nnsv_p (a int);
+CREATE TABLE nnsv_c (a int) INHERITS (nnsv_p);
+ALTER TABLE nnsv_c ADD CONSTRAINT nn NOT NULL a NOT VALID;
+ALTER TABLE nnsv_c ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_p, nnsv_c;
+
+-- partitions
+CREATE TABLE nnsv_pp (a int) PARTITION BY LIST (a);
+CREATE TABLE nnsv_pc PARTITION OF nnsv_pp DEFAULT;
+INSERT INTO nnsv_pc VALUES (NULL);
+ALTER TABLE nnsv_pp ADD CONSTRAINT nn NOT NULL a NOT VALID;
+CALL nnsv_snapshot();
+ALTER TABLE nnsv_pc ALTER COLUMN a SET NOT NULL;		-- error
+SELECT nnsv_unchanged();
+DELETE FROM nnsv_pc;
+ALTER TABLE nnsv_pc ALTER COLUMN a SET NOT NULL;
+SELECT * FROM nnsv_info;
+DROP TABLE nnsv_pp;
+
+DROP FUNCTION nnsv_unchanged();
+DROP PROCEDURE nnsv_snapshot();
+DROP TABLE nnsv_snap;
+DROP VIEW nnsv_info;
+
 
 -- Comments
 -- Setup a low-level role to enforce non-superuser checks.

base-commit: cf765d6e1451c133f131b2ce8aa2cdc306f451b9
-- 
2.56.0

