From 1c1e0a59854ece5b68f211f99a5d5e85488bf309 Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Tue, 8 Sep 2026 16:28:31 +0800
Subject: [PATCH v1] Preserve CHECK and NOT NULL constraints during SET
 EXPRESSION

Rebuilding an inherited constraint assumes that it will be recreated when
the parent constraint is added recursively. When SET EXPRESSION targets a
child directly, no parent rebuild takes place, leaving the child constraint
missing.

Keep table CHECK and NOT NULL constraints instead of rebuilding them.
Revalidate enforced, validated CHECK constraints in Phase 3, and rely on
the existing NOT NULL verification for stored and virtual generated columns.
Preserve local constraints too, to avoid merging rebuilt parent constraints
into retained child constraints.

Add regression coverage for ordinary inheritance and partitioning with
stored and virtual generated columns, including preservation of inheritance
metadata and NOT VALID semantics.

Author: Chao Li <lic@highgo.com>
---
 src/backend/commands/tablecmds.c              |  53 ++++++-
 .../regress/expected/generated_stored.out     | 150 ++++++++++++++++++
 .../regress/expected/generated_virtual.out    | 150 ++++++++++++++++++
 src/test/regress/sql/generated_stored.sql     |  72 +++++++++
 src/test/regress/sql/generated_virtual.sql    |  72 +++++++++
 5 files changed, 491 insertions(+), 6 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index a1519297ec7..bb85ec68e59 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -670,7 +670,7 @@ static void RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab);
 static void RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab);
 static void RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab);
 static void ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab,
-								   LOCKMODE lockmode);
+								   LOCKMODE lockmode, AlterTablePass pass);
 static void ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, Oid ownerId,
 								 char *cmd, List **wqueue, LOCKMODE lockmode,
 								 bool rewrite);
@@ -5400,7 +5400,7 @@ ATRewriteCatalogs(List **wqueue, LOCKMODE lockmode,
 			 * done only once if multiple columns of a table are altered).
 			 */
 			if (pass == AT_PASS_ALTER_TYPE || pass == AT_PASS_SET_EXPRESSION)
-				ATPostAlterTypeCleanup(wqueue, tab, lockmode);
+				ATPostAlterTypeCleanup(wqueue, tab, lockmode, pass);
 
 			if (tab->rel)
 			{
@@ -16086,12 +16086,14 @@ RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab)
 /*
  * Cleanup after we've finished all the ALTER TYPE or SET EXPRESSION
  * operations for a particular relation.  We have to drop and recreate all the
- * indexes and constraints that depend on the altered columns.  We do the
- * actual dropping here, but re-creation is managed by adding work queue
- * entries to do those steps later.
+ * indexes and constraints that depend on the altered columns, except that
+ * CHECK and NOT NULL constraints only need revalidation after SET EXPRESSION.
+ * We do the actual dropping here, but re-creation is managed by adding work
+ * queue entries to do those steps later.
  */
 static void
-ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
+ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode,
+					   AlterTablePass pass)
 {
 	ObjectAddress obj;
 	ObjectAddresses *objects;
@@ -16136,6 +16138,45 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
 		if (!HeapTupleIsValid(tup)) /* should not happen */
 			elog(ERROR, "cache lookup failed for constraint %u", oldId);
 		con = (Form_pg_constraint) GETSTRUCT(tup);
+
+		/*
+		 * SET EXPRESSION does not change CHECK or NOT NULL definitions. Keep
+		 * these constraints and recheck existing rows as needed. This avoids
+		 * losing inherited constraints when only a child is altered.
+		 */
+		if (pass == AT_PASS_SET_EXPRESSION &&
+			(con->contype == CONSTRAINT_CHECK ||
+			 con->contype == CONSTRAINT_NOTNULL) && OidIsValid(con->conrelid))
+		{
+			/*
+			 * Valid NOT NULL constraints are already checked in Phase 3.
+			 * Invalid constraints are skipped.
+			 */
+			if (con->contype == CONSTRAINT_CHECK &&
+				con->conenforced && con->convalidated)
+			{
+				NewConstraint *newcon = palloc0_object(NewConstraint);
+				Relation	conrel;
+				AlteredTableInfo *contab;
+				Datum		conbin;
+
+				/* The constraint can belong to another table. */
+				conrel = table_open(con->conrelid, AccessExclusiveLock);
+				contab = ATGetQueueEntry(wqueue, conrel);
+				newcon->name = pstrdup(NameStr(con->conname));
+				newcon->contype = CONSTR_CHECK;
+				newcon->conid = con->oid;
+				conbin = SysCacheGetAttrNotNull(CONSTROID, tup,
+												Anum_pg_constraint_conbin);
+				/* Virtual generated columns are expanded in Phase 3. */
+				newcon->qual = stringToNode(TextDatumGetCString(conbin));
+				contab->constraints = lappend(contab->constraints, newcon);
+				table_close(conrel, NoLock);
+			}
+			ReleaseSysCache(tup);
+			continue;
+		}
+
 		if (OidIsValid(con->conrelid))
 			relid = con->conrelid;
 		else
diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_stored.out
index fd6caf1cf2d..ff4bab95dab 100644
--- a/src/test/regress/expected/generated_stored.out
+++ b/src/test/regress/expected/generated_stored.out
@@ -681,6 +681,156 @@ INSERT INTO gtest20b (a) VALUES (30);
 ALTER TABLE gtest20b ADD CONSTRAINT chk CHECK (b < 50) NOT VALID;
 ALTER TABLE gtest20b VALIDATE CONSTRAINT chk;  -- fails on existing row
 ERROR:  check constraint "chk" of relation "gtest20b" is violated by some row
+-- SET EXPRESSION must preserve inherited CHECK and NOT NULL constraints.
+CREATE TABLE gtest_check_parent (a int, b int GENERATED ALWAYS AS (a * 2) STORED
+                               CONSTRAINT inherited_not_null NOT NULL,
+                               CONSTRAINT inherited_check CHECK (b < 100));
+CREATE TABLE gtest_check_child () INHERITS (gtest_check_parent);
+INSERT INTO gtest_check_child (a) VALUES (40);
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a * 3); -- error
+ERROR:  check constraint "inherited_check" of relation "gtest_check_child" is violated by some row
+SELECT * FROM gtest_check_child;
+ a  | b  
+----+----
+ 40 | 80
+(1 row)
+
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- error
+ERROR:  column "b" of relation "gtest_check_child" contains null values
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a + 1);
+INSERT INTO gtest_check_child (a) VALUES (1000); -- error
+ERROR:  new row for relation "gtest_check_child" violates check constraint "inherited_check"
+DETAIL:  Failing row contains (1000, 1001).
+INSERT INTO gtest_check_child (a) VALUES (NULL); -- error
+ERROR:  null value in column "b" of relation "gtest_check_child" violates not-null constraint
+DETAIL:  Failing row contains (null, null).
+SELECT contype, conislocal, coninhcount FROM pg_constraint
+  WHERE conrelid = 'gtest_check_child'::regclass ORDER BY contype;
+ contype | conislocal | coninhcount 
+---------+------------+-------------
+ c       | f          |           1
+ n       | f          |           1
+(2 rows)
+
+ALTER TABLE gtest_check_parent ALTER COLUMN b SET EXPRESSION AS (a + 2);
+SELECT * FROM gtest_check_child;
+ a  | b  
+----+----
+ 40 | 42
+(1 row)
+
+SELECT conrelid::regclass, contype, conislocal, coninhcount, convalidated
+  FROM pg_constraint
+  WHERE conrelid IN ('gtest_check_parent'::regclass, 'gtest_check_child'::regclass)
+  ORDER BY conrelid::regclass::text, contype;
+      conrelid      | contype | conislocal | coninhcount | convalidated 
+--------------------+---------+------------+-------------+--------------
+ gtest_check_child  | c       | f          |           1 | t
+ gtest_check_child  | n       | f          |           1 | t
+ gtest_check_parent | c       | t          |           0 | t
+ gtest_check_parent | n       | t          |           0 | t
+(4 rows)
+
+DROP TABLE gtest_check_child, gtest_check_parent;
+-- The same applies when altering a partition directly.
+CREATE TABLE gtest_check_parent (a int, b int GENERATED ALWAYS AS (a * 2) STORED
+                               CONSTRAINT inherited_not_null NOT NULL,
+                               CONSTRAINT inherited_check CHECK (b < 100))
+  PARTITION BY RANGE (a);
+CREATE TABLE gtest_check_child PARTITION OF gtest_check_parent DEFAULT;
+INSERT INTO gtest_check_parent (a) VALUES (40);
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a * 3); -- error
+ERROR:  check constraint "inherited_check" of relation "gtest_check_child" is violated by some row
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- error
+ERROR:  column "b" of relation "gtest_check_child" contains null values
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a + 1);
+INSERT INTO gtest_check_parent (a) VALUES (1000); -- error
+ERROR:  new row for relation "gtest_check_child" violates check constraint "inherited_check"
+DETAIL:  Failing row contains (1000, 1001).
+INSERT INTO gtest_check_parent (a) VALUES (NULL); -- error
+ERROR:  null value in column "b" of relation "gtest_check_child" violates not-null constraint
+DETAIL:  Failing row contains (null, null).
+SELECT contype, conislocal, coninhcount FROM pg_constraint
+  WHERE conrelid = 'gtest_check_child'::regclass ORDER BY contype;
+ contype | conislocal | coninhcount 
+---------+------------+-------------
+ c       | f          |           1
+ n       | f          |           1
+(2 rows)
+
+ALTER TABLE gtest_check_parent ALTER COLUMN b SET EXPRESSION AS (a * 3); -- error
+ERROR:  check constraint "inherited_check" of relation "gtest_check_child" is violated by some row
+ALTER TABLE gtest_check_parent ALTER COLUMN b SET EXPRESSION AS (a + 2);
+SELECT * FROM gtest_check_child;
+ a  | b  
+----+----
+ 40 | 42
+(1 row)
+
+SELECT conrelid::regclass, contype, conislocal, coninhcount, convalidated
+  FROM pg_constraint
+  WHERE conrelid IN ('gtest_check_parent'::regclass, 'gtest_check_child'::regclass)
+  ORDER BY conrelid::regclass::text, contype;
+      conrelid      | contype | conislocal | coninhcount | convalidated 
+--------------------+---------+------------+-------------+--------------
+ gtest_check_child  | c       | f          |           1 | t
+ gtest_check_child  | n       | f          |           1 | t
+ gtest_check_parent | c       | t          |           0 | t
+ gtest_check_parent | n       | t          |           0 | t
+(4 rows)
+
+DROP TABLE gtest_check_parent;
+-- Do not validate NOT VALID or NOT ENFORCED checks during SET EXPRESSION.
+CREATE TABLE gtest_check (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
+INSERT INTO gtest_check (a) VALUES (40);
+ALTER TABLE gtest_check ADD CONSTRAINT invalid_check CHECK (b < 10) NOT VALID;
+ALTER TABLE gtest_check ADD CONSTRAINT unenforced_check CHECK (b < 10) NOT ENFORCED;
+ALTER TABLE gtest_check ALTER COLUMN b SET EXPRESSION AS (a * 3);
+SELECT conname, convalidated, conenforced
+  FROM pg_constraint WHERE conrelid = 'gtest_check'::regclass ORDER BY conname;
+     conname      | convalidated | conenforced 
+------------------+--------------+-------------
+ invalid_check    | f            | t
+ unenforced_check | f            | f
+(2 rows)
+
+DROP TABLE gtest_check;
+-- SET EXPRESSION must not validate an invalid NOT NULL constraint.
+CREATE TABLE gtest_nn_invalid (a int, b int GENERATED ALWAYS AS (a) STORED);
+INSERT INTO gtest_nn_invalid (a) VALUES (NULL);
+ALTER TABLE gtest_nn_invalid ADD CONSTRAINT nn NOT NULL b NOT VALID;
+ALTER TABLE gtest_nn_invalid ALTER COLUMN b SET EXPRESSION AS (a + 1);
+SELECT b IS NULL AS existing_null FROM gtest_nn_invalid;
+ existing_null 
+---------------
+ t
+(1 row)
+
+SELECT attnotnull, convalidated FROM pg_attribute a JOIN pg_constraint c
+  ON c.conrelid = a.attrelid AND c.conkey = ARRAY[a.attnum]
+  WHERE a.attrelid = 'gtest_nn_invalid'::regclass AND a.attname = 'b';
+ attnotnull | convalidated 
+------------+--------------
+ t          | f
+(1 row)
+
+INSERT INTO gtest_nn_invalid (a) VALUES (NULL); -- error
+ERROR:  null value in column "b" of relation "gtest_nn_invalid" violates not-null constraint
+DETAIL:  Failing row contains (null, null).
+UPDATE gtest_nn_invalid SET a = a; -- error
+ERROR:  null value in column "b" of relation "gtest_nn_invalid" violates not-null constraint
+DETAIL:  Failing row contains (null, null).
+ALTER TABLE gtest_nn_invalid VALIDATE CONSTRAINT nn; -- error
+ERROR:  column "b" of relation "gtest_nn_invalid" contains null values
+UPDATE gtest_nn_invalid SET a = 1;
+ALTER TABLE gtest_nn_invalid VALIDATE CONSTRAINT nn;
+SELECT convalidated FROM pg_constraint WHERE conrelid = 'gtest_nn_invalid'::regclass;
+ convalidated 
+--------------
+ t
+(1 row)
+
+DROP TABLE gtest_nn_invalid;
 -- check with whole-row reference
 CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
 ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL);
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 6ee029796f1..31bbba59e12 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -687,6 +687,156 @@ INSERT INTO gtest20b (a) VALUES (30);
 ALTER TABLE gtest20b ADD CONSTRAINT chk CHECK (b < 50) NOT VALID;
 ALTER TABLE gtest20b VALIDATE CONSTRAINT chk;  -- fails on existing row
 ERROR:  check constraint "chk" of relation "gtest20b" is violated by some row
+-- SET EXPRESSION must preserve inherited CHECK and NOT NULL constraints.
+CREATE TABLE gtest_check_parent (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL
+                               CONSTRAINT inherited_not_null NOT NULL,
+                               CONSTRAINT inherited_check CHECK (b < 100));
+CREATE TABLE gtest_check_child () INHERITS (gtest_check_parent);
+INSERT INTO gtest_check_child (a) VALUES (40);
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a * 3); -- error
+ERROR:  check constraint "inherited_check" of relation "gtest_check_child" is violated by some row
+SELECT * FROM gtest_check_child;
+ a  | b  
+----+----
+ 40 | 80
+(1 row)
+
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- error
+ERROR:  column "b" of relation "gtest_check_child" contains null values
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a + 1);
+INSERT INTO gtest_check_child (a) VALUES (1000); -- error
+ERROR:  new row for relation "gtest_check_child" violates check constraint "inherited_check"
+DETAIL:  Failing row contains (1000, virtual).
+INSERT INTO gtest_check_child (a) VALUES (NULL); -- error
+ERROR:  null value in column "b" of relation "gtest_check_child" violates not-null constraint
+DETAIL:  Failing row contains (null, virtual).
+SELECT contype, conislocal, coninhcount FROM pg_constraint
+  WHERE conrelid = 'gtest_check_child'::regclass ORDER BY contype;
+ contype | conislocal | coninhcount 
+---------+------------+-------------
+ c       | f          |           1
+ n       | f          |           1
+(2 rows)
+
+ALTER TABLE gtest_check_parent ALTER COLUMN b SET EXPRESSION AS (a + 2);
+SELECT * FROM gtest_check_child;
+ a  | b  
+----+----
+ 40 | 42
+(1 row)
+
+SELECT conrelid::regclass, contype, conislocal, coninhcount, convalidated
+  FROM pg_constraint
+  WHERE conrelid IN ('gtest_check_parent'::regclass, 'gtest_check_child'::regclass)
+  ORDER BY conrelid::regclass::text, contype;
+      conrelid      | contype | conislocal | coninhcount | convalidated 
+--------------------+---------+------------+-------------+--------------
+ gtest_check_child  | c       | f          |           1 | t
+ gtest_check_child  | n       | f          |           1 | t
+ gtest_check_parent | c       | t          |           0 | t
+ gtest_check_parent | n       | t          |           0 | t
+(4 rows)
+
+DROP TABLE gtest_check_child, gtest_check_parent;
+-- The same applies when altering a partition directly.
+CREATE TABLE gtest_check_parent (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL
+                               CONSTRAINT inherited_not_null NOT NULL,
+                               CONSTRAINT inherited_check CHECK (b < 100))
+  PARTITION BY RANGE (a);
+CREATE TABLE gtest_check_child PARTITION OF gtest_check_parent DEFAULT;
+INSERT INTO gtest_check_parent (a) VALUES (40);
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a * 3); -- error
+ERROR:  check constraint "inherited_check" of relation "gtest_check_child" is violated by some row
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- error
+ERROR:  column "b" of relation "gtest_check_child" contains null values
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a + 1);
+INSERT INTO gtest_check_parent (a) VALUES (1000); -- error
+ERROR:  new row for relation "gtest_check_child" violates check constraint "inherited_check"
+DETAIL:  Failing row contains (1000, virtual).
+INSERT INTO gtest_check_parent (a) VALUES (NULL); -- error
+ERROR:  null value in column "b" of relation "gtest_check_child" violates not-null constraint
+DETAIL:  Failing row contains (null, virtual).
+SELECT contype, conislocal, coninhcount FROM pg_constraint
+  WHERE conrelid = 'gtest_check_child'::regclass ORDER BY contype;
+ contype | conislocal | coninhcount 
+---------+------------+-------------
+ c       | f          |           1
+ n       | f          |           1
+(2 rows)
+
+ALTER TABLE gtest_check_parent ALTER COLUMN b SET EXPRESSION AS (a * 3); -- error
+ERROR:  check constraint "inherited_check" of relation "gtest_check_child" is violated by some row
+ALTER TABLE gtest_check_parent ALTER COLUMN b SET EXPRESSION AS (a + 2);
+SELECT * FROM gtest_check_child;
+ a  | b  
+----+----
+ 40 | 42
+(1 row)
+
+SELECT conrelid::regclass, contype, conislocal, coninhcount, convalidated
+  FROM pg_constraint
+  WHERE conrelid IN ('gtest_check_parent'::regclass, 'gtest_check_child'::regclass)
+  ORDER BY conrelid::regclass::text, contype;
+      conrelid      | contype | conislocal | coninhcount | convalidated 
+--------------------+---------+------------+-------------+--------------
+ gtest_check_child  | c       | f          |           1 | t
+ gtest_check_child  | n       | f          |           1 | t
+ gtest_check_parent | c       | t          |           0 | t
+ gtest_check_parent | n       | t          |           0 | t
+(4 rows)
+
+DROP TABLE gtest_check_parent;
+-- Do not validate NOT VALID or NOT ENFORCED checks during SET EXPRESSION.
+CREATE TABLE gtest_check (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
+INSERT INTO gtest_check (a) VALUES (40);
+ALTER TABLE gtest_check ADD CONSTRAINT invalid_check CHECK (b < 10) NOT VALID;
+ALTER TABLE gtest_check ADD CONSTRAINT unenforced_check CHECK (b < 10) NOT ENFORCED;
+ALTER TABLE gtest_check ALTER COLUMN b SET EXPRESSION AS (a * 3);
+SELECT conname, convalidated, conenforced
+  FROM pg_constraint WHERE conrelid = 'gtest_check'::regclass ORDER BY conname;
+     conname      | convalidated | conenforced 
+------------------+--------------+-------------
+ invalid_check    | f            | t
+ unenforced_check | f            | f
+(2 rows)
+
+DROP TABLE gtest_check;
+-- SET EXPRESSION must not validate an invalid NOT NULL constraint.
+CREATE TABLE gtest_nn_invalid (a int, b int GENERATED ALWAYS AS (a) VIRTUAL);
+INSERT INTO gtest_nn_invalid (a) VALUES (NULL);
+ALTER TABLE gtest_nn_invalid ADD CONSTRAINT nn NOT NULL b NOT VALID;
+ALTER TABLE gtest_nn_invalid ALTER COLUMN b SET EXPRESSION AS (a + 1);
+SELECT b IS NULL AS existing_null FROM gtest_nn_invalid;
+ existing_null 
+---------------
+ t
+(1 row)
+
+SELECT attnotnull, convalidated FROM pg_attribute a JOIN pg_constraint c
+  ON c.conrelid = a.attrelid AND c.conkey = ARRAY[a.attnum]
+  WHERE a.attrelid = 'gtest_nn_invalid'::regclass AND a.attname = 'b';
+ attnotnull | convalidated 
+------------+--------------
+ t          | f
+(1 row)
+
+INSERT INTO gtest_nn_invalid (a) VALUES (NULL); -- error
+ERROR:  null value in column "b" of relation "gtest_nn_invalid" violates not-null constraint
+DETAIL:  Failing row contains (null, virtual).
+UPDATE gtest_nn_invalid SET a = a; -- error
+ERROR:  null value in column "b" of relation "gtest_nn_invalid" violates not-null constraint
+DETAIL:  Failing row contains (null, virtual).
+ALTER TABLE gtest_nn_invalid VALIDATE CONSTRAINT nn; -- error
+ERROR:  column "b" of relation "gtest_nn_invalid" contains null values
+UPDATE gtest_nn_invalid SET a = 1;
+ALTER TABLE gtest_nn_invalid VALIDATE CONSTRAINT nn;
+SELECT convalidated FROM pg_constraint WHERE conrelid = 'gtest_nn_invalid'::regclass;
+ convalidated 
+--------------
+ t
+(1 row)
+
+DROP TABLE gtest_nn_invalid;
 -- check with whole-row reference
 CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL);
diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_stored.sql
index 235bc28db81..7dc1b8f1405 100644
--- a/src/test/regress/sql/generated_stored.sql
+++ b/src/test/regress/sql/generated_stored.sql
@@ -340,6 +340,78 @@ INSERT INTO gtest20b (a) VALUES (30);
 ALTER TABLE gtest20b ADD CONSTRAINT chk CHECK (b < 50) NOT VALID;
 ALTER TABLE gtest20b VALIDATE CONSTRAINT chk;  -- fails on existing row
 
+-- SET EXPRESSION must preserve inherited CHECK and NOT NULL constraints.
+CREATE TABLE gtest_check_parent (a int, b int GENERATED ALWAYS AS (a * 2) STORED
+                               CONSTRAINT inherited_not_null NOT NULL,
+                               CONSTRAINT inherited_check CHECK (b < 100));
+CREATE TABLE gtest_check_child () INHERITS (gtest_check_parent);
+INSERT INTO gtest_check_child (a) VALUES (40);
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a * 3); -- error
+SELECT * FROM gtest_check_child;
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- error
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a + 1);
+INSERT INTO gtest_check_child (a) VALUES (1000); -- error
+INSERT INTO gtest_check_child (a) VALUES (NULL); -- error
+SELECT contype, conislocal, coninhcount FROM pg_constraint
+  WHERE conrelid = 'gtest_check_child'::regclass ORDER BY contype;
+ALTER TABLE gtest_check_parent ALTER COLUMN b SET EXPRESSION AS (a + 2);
+SELECT * FROM gtest_check_child;
+SELECT conrelid::regclass, contype, conislocal, coninhcount, convalidated
+  FROM pg_constraint
+  WHERE conrelid IN ('gtest_check_parent'::regclass, 'gtest_check_child'::regclass)
+  ORDER BY conrelid::regclass::text, contype;
+DROP TABLE gtest_check_child, gtest_check_parent;
+
+-- The same applies when altering a partition directly.
+CREATE TABLE gtest_check_parent (a int, b int GENERATED ALWAYS AS (a * 2) STORED
+                               CONSTRAINT inherited_not_null NOT NULL,
+                               CONSTRAINT inherited_check CHECK (b < 100))
+  PARTITION BY RANGE (a);
+CREATE TABLE gtest_check_child PARTITION OF gtest_check_parent DEFAULT;
+INSERT INTO gtest_check_parent (a) VALUES (40);
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a * 3); -- error
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- error
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a + 1);
+INSERT INTO gtest_check_parent (a) VALUES (1000); -- error
+INSERT INTO gtest_check_parent (a) VALUES (NULL); -- error
+SELECT contype, conislocal, coninhcount FROM pg_constraint
+  WHERE conrelid = 'gtest_check_child'::regclass ORDER BY contype;
+ALTER TABLE gtest_check_parent ALTER COLUMN b SET EXPRESSION AS (a * 3); -- error
+ALTER TABLE gtest_check_parent ALTER COLUMN b SET EXPRESSION AS (a + 2);
+SELECT * FROM gtest_check_child;
+SELECT conrelid::regclass, contype, conislocal, coninhcount, convalidated
+  FROM pg_constraint
+  WHERE conrelid IN ('gtest_check_parent'::regclass, 'gtest_check_child'::regclass)
+  ORDER BY conrelid::regclass::text, contype;
+DROP TABLE gtest_check_parent;
+
+-- Do not validate NOT VALID or NOT ENFORCED checks during SET EXPRESSION.
+CREATE TABLE gtest_check (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
+INSERT INTO gtest_check (a) VALUES (40);
+ALTER TABLE gtest_check ADD CONSTRAINT invalid_check CHECK (b < 10) NOT VALID;
+ALTER TABLE gtest_check ADD CONSTRAINT unenforced_check CHECK (b < 10) NOT ENFORCED;
+ALTER TABLE gtest_check ALTER COLUMN b SET EXPRESSION AS (a * 3);
+SELECT conname, convalidated, conenforced
+  FROM pg_constraint WHERE conrelid = 'gtest_check'::regclass ORDER BY conname;
+DROP TABLE gtest_check;
+
+-- SET EXPRESSION must not validate an invalid NOT NULL constraint.
+CREATE TABLE gtest_nn_invalid (a int, b int GENERATED ALWAYS AS (a) STORED);
+INSERT INTO gtest_nn_invalid (a) VALUES (NULL);
+ALTER TABLE gtest_nn_invalid ADD CONSTRAINT nn NOT NULL b NOT VALID;
+ALTER TABLE gtest_nn_invalid ALTER COLUMN b SET EXPRESSION AS (a + 1);
+SELECT b IS NULL AS existing_null FROM gtest_nn_invalid;
+SELECT attnotnull, convalidated FROM pg_attribute a JOIN pg_constraint c
+  ON c.conrelid = a.attrelid AND c.conkey = ARRAY[a.attnum]
+  WHERE a.attrelid = 'gtest_nn_invalid'::regclass AND a.attname = 'b';
+INSERT INTO gtest_nn_invalid (a) VALUES (NULL); -- error
+UPDATE gtest_nn_invalid SET a = a; -- error
+ALTER TABLE gtest_nn_invalid VALIDATE CONSTRAINT nn; -- error
+UPDATE gtest_nn_invalid SET a = 1;
+ALTER TABLE gtest_nn_invalid VALIDATE CONSTRAINT nn;
+SELECT convalidated FROM pg_constraint WHERE conrelid = 'gtest_nn_invalid'::regclass;
+DROP TABLE gtest_nn_invalid;
+
 -- check with whole-row reference
 CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
 ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL);
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
index e4ea63bb3a1..eed9d093ba9 100644
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -346,6 +346,78 @@ INSERT INTO gtest20b (a) VALUES (30);
 ALTER TABLE gtest20b ADD CONSTRAINT chk CHECK (b < 50) NOT VALID;
 ALTER TABLE gtest20b VALIDATE CONSTRAINT chk;  -- fails on existing row
 
+-- SET EXPRESSION must preserve inherited CHECK and NOT NULL constraints.
+CREATE TABLE gtest_check_parent (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL
+                               CONSTRAINT inherited_not_null NOT NULL,
+                               CONSTRAINT inherited_check CHECK (b < 100));
+CREATE TABLE gtest_check_child () INHERITS (gtest_check_parent);
+INSERT INTO gtest_check_child (a) VALUES (40);
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a * 3); -- error
+SELECT * FROM gtest_check_child;
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- error
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a + 1);
+INSERT INTO gtest_check_child (a) VALUES (1000); -- error
+INSERT INTO gtest_check_child (a) VALUES (NULL); -- error
+SELECT contype, conislocal, coninhcount FROM pg_constraint
+  WHERE conrelid = 'gtest_check_child'::regclass ORDER BY contype;
+ALTER TABLE gtest_check_parent ALTER COLUMN b SET EXPRESSION AS (a + 2);
+SELECT * FROM gtest_check_child;
+SELECT conrelid::regclass, contype, conislocal, coninhcount, convalidated
+  FROM pg_constraint
+  WHERE conrelid IN ('gtest_check_parent'::regclass, 'gtest_check_child'::regclass)
+  ORDER BY conrelid::regclass::text, contype;
+DROP TABLE gtest_check_child, gtest_check_parent;
+
+-- The same applies when altering a partition directly.
+CREATE TABLE gtest_check_parent (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL
+                               CONSTRAINT inherited_not_null NOT NULL,
+                               CONSTRAINT inherited_check CHECK (b < 100))
+  PARTITION BY RANGE (a);
+CREATE TABLE gtest_check_child PARTITION OF gtest_check_parent DEFAULT;
+INSERT INTO gtest_check_parent (a) VALUES (40);
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a * 3); -- error
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (NULL::int); -- error
+ALTER TABLE gtest_check_child ALTER COLUMN b SET EXPRESSION AS (a + 1);
+INSERT INTO gtest_check_parent (a) VALUES (1000); -- error
+INSERT INTO gtest_check_parent (a) VALUES (NULL); -- error
+SELECT contype, conislocal, coninhcount FROM pg_constraint
+  WHERE conrelid = 'gtest_check_child'::regclass ORDER BY contype;
+ALTER TABLE gtest_check_parent ALTER COLUMN b SET EXPRESSION AS (a * 3); -- error
+ALTER TABLE gtest_check_parent ALTER COLUMN b SET EXPRESSION AS (a + 2);
+SELECT * FROM gtest_check_child;
+SELECT conrelid::regclass, contype, conislocal, coninhcount, convalidated
+  FROM pg_constraint
+  WHERE conrelid IN ('gtest_check_parent'::regclass, 'gtest_check_child'::regclass)
+  ORDER BY conrelid::regclass::text, contype;
+DROP TABLE gtest_check_parent;
+
+-- Do not validate NOT VALID or NOT ENFORCED checks during SET EXPRESSION.
+CREATE TABLE gtest_check (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
+INSERT INTO gtest_check (a) VALUES (40);
+ALTER TABLE gtest_check ADD CONSTRAINT invalid_check CHECK (b < 10) NOT VALID;
+ALTER TABLE gtest_check ADD CONSTRAINT unenforced_check CHECK (b < 10) NOT ENFORCED;
+ALTER TABLE gtest_check ALTER COLUMN b SET EXPRESSION AS (a * 3);
+SELECT conname, convalidated, conenforced
+  FROM pg_constraint WHERE conrelid = 'gtest_check'::regclass ORDER BY conname;
+DROP TABLE gtest_check;
+
+-- SET EXPRESSION must not validate an invalid NOT NULL constraint.
+CREATE TABLE gtest_nn_invalid (a int, b int GENERATED ALWAYS AS (a) VIRTUAL);
+INSERT INTO gtest_nn_invalid (a) VALUES (NULL);
+ALTER TABLE gtest_nn_invalid ADD CONSTRAINT nn NOT NULL b NOT VALID;
+ALTER TABLE gtest_nn_invalid ALTER COLUMN b SET EXPRESSION AS (a + 1);
+SELECT b IS NULL AS existing_null FROM gtest_nn_invalid;
+SELECT attnotnull, convalidated FROM pg_attribute a JOIN pg_constraint c
+  ON c.conrelid = a.attrelid AND c.conkey = ARRAY[a.attnum]
+  WHERE a.attrelid = 'gtest_nn_invalid'::regclass AND a.attname = 'b';
+INSERT INTO gtest_nn_invalid (a) VALUES (NULL); -- error
+UPDATE gtest_nn_invalid SET a = a; -- error
+ALTER TABLE gtest_nn_invalid VALIDATE CONSTRAINT nn; -- error
+UPDATE gtest_nn_invalid SET a = 1;
+ALTER TABLE gtest_nn_invalid VALIDATE CONSTRAINT nn;
+SELECT convalidated FROM pg_constraint WHERE conrelid = 'gtest_nn_invalid'::regclass;
+DROP TABLE gtest_nn_invalid;
+
 -- check with whole-row reference
 CREATE TABLE gtest20c (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
 ALTER TABLE gtest20c ADD CONSTRAINT whole_row_check CHECK (gtest20c IS NOT NULL);
-- 
2.50.1 (Apple Git-155)

