From 8ce42478049de4fa74cdf0ee85431fd86cad64bf Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 10 Aug 2026 15:48:57 +0800
Subject: [PATCH v3 1/1] Disallow directly ALTER TABLE SET EXPRESSION on child
 table if dependency exists

1. Directly dropping and recreating an index on the child table will fail if the
index is part of an index hierarchy.
2. Directly dropping and recreating a constraint on the child table will
silently drop the constraint without recreating it. This is because the existing
constraint is not local to the partition, so ALTER TABLE ADD CONSTRAINT command
will not issued. See ATPostAlterTypeCleanup for details.

ALTER TABLE ONLY ... SET EXPRESSION should fail if the generated column has
dependencies, because rebuilding dependent objects would violate the semantics
of ONLY.

commitfest: https://commitfest.postgresql.org/patch/7117
discussion: https://postgr.es/m/CACJufxEomSz3BwUGfk8A6MeAKj=Cki5B+gYTbJO=ACCJyytWZg@mail.gmail.com
---
 src/backend/commands/tablecmds.c              | 82 ++++++++++++++++---
 .../regress/expected/generated_stored.out     | 39 +++++++++
 .../regress/expected/generated_virtual.out    | 40 +++++++++
 src/test/regress/sql/generated_stored.sql     | 29 +++++++
 src/test/regress/sql/generated_virtual.sql    | 29 +++++++
 5 files changed, 206 insertions(+), 13 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2fa534413ea..2d67320b315 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -794,6 +794,9 @@ static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab,
 static List *collectPartitionIndexExtDeps(List *partitionOids);
 static void applyPartitionIndexExtDeps(Oid newPartOid, List *extDepState);
 static void freePartitionIndexExtDeps(List *extDepState);
+static void ATPrepSetExpression(List **wqueue, AlteredTableInfo *tab, Relation rel,
+								AlterTableCmd *cmd, bool recurse, bool recursing,
+								LOCKMODE lockmode);
 
 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -5126,6 +5129,7 @@ ATPrepCmd(List **wqueue, Relation rel, AlterTableCmd *cmd,
 			ATSimplePermissions(cmd->subtype, rel,
 								ATT_TABLE | ATT_PARTITIONED_TABLE | ATT_FOREIGN_TABLE);
 			ATSimpleRecursion(wqueue, rel, cmd, recurse, lockmode, context);
+			ATPrepSetExpression(wqueue, tab, rel, cmd, recurse, recursing, lockmode);
 			pass = AT_PASS_SET_EXPRESSION;
 			break;
 		case AT_DropExpression: /* ALTER COLUMN DROP EXPRESSION */
@@ -8807,19 +8811,6 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 		CommandCounterIncrement();
 	}
 
-	/*
-	 * Find everything that depends on the column (constraints, indexes, etc),
-	 * and record enough information to let us recreate the objects.
-	 */
-	RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName);
-
-	/*
-	 * Find whole-row referenced objects that depend on the column
-	 * (constraints, indexes, etc.), and record enough information to let us
-	 * recreate the objects.
-	 */
-	RememberWholeRowDependentForRebuilding(tab, AT_SetExpression, rel);
-
 	/*
 	 * Drop the dependency records of the GENERATED expression, in particular
 	 * its INTERNAL dependency on the column, which would otherwise cause
@@ -8880,6 +8871,71 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	return address;
 }
 
+/*
+ * ALTER TABLE ALTER COLUMN SET EXPRESSION
+ */
+static void
+ATPrepSetExpression(List **wqueue, AlteredTableInfo *tab, Relation rel,
+					AlterTableCmd *cmd, bool recurse, bool recursing,
+					LOCKMODE lockmode)
+{
+	char	   *colName = cmd->name;
+	Form_pg_attribute attTup;
+	AttrNumber	attnum;
+
+	HeapTuple	tuple = SearchSysCacheAttName(RelationGetRelid(rel),
+											  colName);
+
+	if (!HeapTupleIsValid(tuple))
+	{
+		/*
+		 * it can happen, for example: -------------------------------------
+		 * ALTER TABLE ADD COLUMN b int GENERATED ALWAYS AS (2) STORED, ALTER
+		 * COLUMN b SET EXPRESSION AS (a * 3);
+		 */
+		return;
+	}
+	attTup = (Form_pg_attribute) GETSTRUCT(tuple);
+	attnum = attTup->attnum;
+	ReleaseSysCache(tuple);
+
+	/*
+	 * Find everything that depends on the column (constraints, indexes, etc),
+	 * and record enough information to let us recreate the objects.
+	 */
+	RememberAllDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName);
+
+	/*
+	 * Find whole-row referenced objects that depend on the column
+	 * (constraints, indexes, etc.), and record enough information to let us
+	 * recreate the objects.
+	 */
+	RememberWholeRowDependentForRebuilding(tab, AT_SetExpression, rel);
+
+	if (tab->changedConstraintOids != NIL || tab->changedIndexOids != NIL)
+	{
+		List	   *children = find_inheritance_children(RelationGetRelid(rel), NoLock);
+
+		/*
+		 * Cannot use ONLY to modify a generated column's expression when
+		 * there are dependencies (e.g. constraints, indexes) that need to be
+		 * rebuilt, since that rebuild must cascade to the children too.
+		 */
+		if (!recurse && !recursing && children != NIL)
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("ALTER TABLE ONLY ... SET EXPRESSION is not supported for generated columns with dependent objects on a parent table"),
+					errdetail("Dependent objects, such as constraints and indexes, must be rebuilt in child tables as well, which conflicts with ONLY."));
+
+		/* cannot rebuild partition child table index */
+		if (!recursing && (rel->rd_rel->relispartition || has_superclass(RelationGetRelid(rel))))
+			ereport(ERROR,
+					errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					errmsg("cannot use ALTER TABLE ... SET EXPRESSION on a generated column in a child table with dependent objects"),
+					errdetail("Inherited objects, such as indexes, cannot be rebuilt independently for a child table."));
+	}
+}
+
 /*
  * ALTER TABLE ALTER COLUMN DROP EXPRESSION
  */
diff --git a/src/test/regress/expected/generated_stored.out b/src/test/regress/expected/generated_stored.out
index fd6caf1cf2d..947b443b22e 100644
--- a/src/test/regress/expected/generated_stored.out
+++ b/src/test/regress/expected/generated_stored.out
@@ -1683,6 +1683,45 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
  c      | integer |           |          | 
  x      | integer |           |          | generated always as (b * 2) stored
 
+-- Test table partitioning, change child table generation expression
+CREATE TABLE gtest_pk AS SELECT g::int as b FROM generate_series(1, 100) g;
+ALTER TABLE gtest_pk ADD PRIMARY KEY (b);
+CREATE TABLE gtest35(a int, b int GENERATED ALWAYS AS (a) STORED NOT NULL, CONSTRAINT cc check (b < 120)) PARTITION BY RANGE (a);
+CREATE TABLE gtest35_1 PARTITION OF gtest35 FOR VALUES FROM (1) TO (10);
+CREATE TABLE gtest35_2 PARTITION OF gtest35 FOR VALUES FROM (10) TO (20);
+CREATE INDEX gtest35_b_idx ON gtest35(b);
+INSERT INTO gtest35 SELECT g FROM generate_series(5, 14) g;
+ALTER TABLE gtest35 ADD CONSTRAINT gtest35_fk FOREIGN KEY (b) REFERENCES gtest_pk;
+ALTER TABLE ONLY gtest35 ALTER COLUMN b SET EXPRESSION AS (a); -- error
+ERROR:  ALTER TABLE ONLY ... SET EXPRESSION is not supported for generated columns with dependent objects on a parent table
+DETAIL:  Dependent objects, such as constraints and indexes, must be rebuilt in child tables as well, which conflicts with ONLY.
+ALTER TABLE gtest35_1 ALTER COLUMN b SET EXPRESSION AS (a+1); -- error
+ERROR:  cannot use ALTER TABLE ... SET EXPRESSION on a generated column in a child table with dependent objects
+DETAIL:  Inherited objects, such as indexes, cannot be rebuilt independently for a child table.
+DROP INDEX gtest35_b_idx;
+ALTER TABLE gtest35_1 ALTER COLUMN b SET EXPRESSION AS (case when a=7 then 200 else a + 112 end); -- error
+ERROR:  cannot use ALTER TABLE ... SET EXPRESSION on a generated column in a child table with dependent objects
+DETAIL:  Inherited objects, such as indexes, cannot be rebuilt independently for a child table.
+ALTER TABLE gtest35_2 ALTER COLUMN b SET EXPRESSION AS (case when a=10 then 200 else a + 2 end); -- error
+ERROR:  cannot use ALTER TABLE ... SET EXPRESSION on a generated column in a child table with dependent objects
+DETAIL:  Inherited objects, such as indexes, cannot be rebuilt independently for a child table.
+-- Test inheritance, change child table generation expression
+CREATE TABLE gtest36(a int, b int GENERATED ALWAYS AS (a) STORED NOT NULL, CONSTRAINT cc check (b < 120));
+CREATE TABLE gtest36_1 () INHERITS (gtest36);
+CREATE TABLE gtest36_12 () INHERITS (gtest36, gtest36_1);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+INSERT INTO gtest36 VALUES (8), (9);
+INSERT INTO gtest36_1 VALUES (10), (11);
+INSERT INTO gtest36_12 VALUES (12), (13);
+CREATE INDEX gtest36_b_idx ON gtest36(b);
+ALTER TABLE gtest36 ADD CONSTRAINT gtest36_fk FOREIGN KEY (b) REFERENCES gtest_pk;
+ALTER TABLE ONLY gtest36 ALTER COLUMN b SET EXPRESSION AS (a); -- error
+ERROR:  ALTER TABLE ONLY ... SET EXPRESSION is not supported for generated columns with dependent objects on a parent table
+DETAIL:  Dependent objects, such as constraints and indexes, must be rebuilt in child tables as well, which conflicts with ONLY.
+ALTER TABLE gtest36_1 ALTER COLUMN b SET EXPRESSION AS (a+1); -- error
+ERROR:  cannot use ALTER TABLE ... SET EXPRESSION on a generated column in a child table with dependent objects
+DETAIL:  Inherited objects, such as indexes, cannot be rebuilt independently for a child table.
 -- rule actions referring to generated columns:
 -- NEW.b in a rule action should reflect the generated column's new value
 CREATE TABLE gtest_rule (a int, b int GENERATED ALWAYS AS (a * 2) STORED);
diff --git a/src/test/regress/expected/generated_virtual.out b/src/test/regress/expected/generated_virtual.out
index 6ee029796f1..2762d882581 100644
--- a/src/test/regress/expected/generated_virtual.out
+++ b/src/test/regress/expected/generated_virtual.out
@@ -1577,6 +1577,46 @@ CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
  c      | integer |           |          | 
  x      | integer |           |          | generated always as (b * 2)
 
+-- Test table partitioning, change child table generation expression
+CREATE TABLE gtest_pk AS SELECT g::int as b FROM generate_series(1, 100) g;
+ALTER TABLE gtest_pk ADD PRIMARY KEY (b);
+CREATE TABLE gtest35(a int, b int GENERATED ALWAYS AS (a) STORED NOT NULL, CONSTRAINT cc check (b < 120)) PARTITION BY RANGE (a);
+CREATE TABLE gtest35_1 PARTITION OF gtest35 FOR VALUES FROM (1) TO (10);
+CREATE TABLE gtest35_2 PARTITION OF gtest35 FOR VALUES FROM (10) TO (20);
+-- CREATE INDEX gtest35_b_idx ON gtest35(b);
+INSERT INTO gtest35 SELECT g FROM generate_series(5, 14) g;
+-- ALTER TABLE gtest35 ADD CONSTRAINT gtest35_fk FOREIGN KEY (b) REFERENCES gtest_pk;
+ALTER TABLE ONLY gtest35 ALTER COLUMN b SET EXPRESSION AS (a); -- error
+ERROR:  ALTER TABLE ONLY ... SET EXPRESSION is not supported for generated columns with dependent objects on a parent table
+DETAIL:  Dependent objects, such as constraints and indexes, must be rebuilt in child tables as well, which conflicts with ONLY.
+ALTER TABLE gtest35_1 ALTER COLUMN b SET EXPRESSION AS (a+1); -- error
+ERROR:  cannot use ALTER TABLE ... SET EXPRESSION on a generated column in a child table with dependent objects
+DETAIL:  Inherited objects, such as indexes, cannot be rebuilt independently for a child table.
+DROP INDEX gtest35_b_idx;
+ERROR:  index "gtest35_b_idx" does not exist
+ALTER TABLE gtest35_1 ALTER COLUMN b SET EXPRESSION AS (case when a=7 then 200 else a + 112 end); -- error
+ERROR:  cannot use ALTER TABLE ... SET EXPRESSION on a generated column in a child table with dependent objects
+DETAIL:  Inherited objects, such as indexes, cannot be rebuilt independently for a child table.
+ALTER TABLE gtest35_2 ALTER COLUMN b SET EXPRESSION AS (case when a=10 then 200 else a + 2 end); -- error
+ERROR:  cannot use ALTER TABLE ... SET EXPRESSION on a generated column in a child table with dependent objects
+DETAIL:  Inherited objects, such as indexes, cannot be rebuilt independently for a child table.
+-- Test inheritance, change child table generation expression
+CREATE TABLE gtest36(a int, b int GENERATED ALWAYS AS (a) STORED NOT NULL, CONSTRAINT cc check (b < 120));
+CREATE TABLE gtest36_1 () INHERITS (gtest36);
+CREATE TABLE gtest36_12 () INHERITS (gtest36, gtest36_1);
+NOTICE:  merging multiple inherited definitions of column "a"
+NOTICE:  merging multiple inherited definitions of column "b"
+INSERT INTO gtest36 VALUES (8), (9);
+INSERT INTO gtest36_1 VALUES (10), (11);
+INSERT INTO gtest36_12 VALUES (12), (13);
+CREATE INDEX gtest36_b_idx ON gtest36(b);
+ALTER TABLE gtest36 ADD CONSTRAINT gtest36_fk FOREIGN KEY (b) REFERENCES gtest_pk;
+ALTER TABLE ONLY gtest36 ALTER COLUMN b SET EXPRESSION AS (a); -- error
+ERROR:  ALTER TABLE ONLY ... SET EXPRESSION is not supported for generated columns with dependent objects on a parent table
+DETAIL:  Dependent objects, such as constraints and indexes, must be rebuilt in child tables as well, which conflicts with ONLY.
+ALTER TABLE gtest36_1 ALTER COLUMN b SET EXPRESSION AS (a+1); -- error
+ERROR:  cannot use ALTER TABLE ... SET EXPRESSION on a generated column in a child table with dependent objects
+DETAIL:  Inherited objects, such as indexes, cannot be rebuilt independently for a child table.
 -- rule actions referring to generated columns:
 -- NEW.b in a rule action should reflect the generated column's new value
 CREATE TABLE gtest_rule (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL);
diff --git a/src/test/regress/sql/generated_stored.sql b/src/test/regress/sql/generated_stored.sql
index 9eecd13dd9e..b75e0abd7d3 100644
--- a/src/test/regress/sql/generated_stored.sql
+++ b/src/test/regress/sql/generated_stored.sql
@@ -844,6 +844,35 @@ ALTER TABLE gtest28a DROP COLUMN a;
 CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
 
 \d gtest28*
+-- Test table partitioning, change child table generation expression
+CREATE TABLE gtest_pk AS SELECT g::int as b FROM generate_series(1, 100) g;
+ALTER TABLE gtest_pk ADD PRIMARY KEY (b);
+CREATE TABLE gtest35(a int, b int GENERATED ALWAYS AS (a) STORED NOT NULL, CONSTRAINT cc check (b < 120)) PARTITION BY RANGE (a);
+CREATE TABLE gtest35_1 PARTITION OF gtest35 FOR VALUES FROM (1) TO (10);
+CREATE TABLE gtest35_2 PARTITION OF gtest35 FOR VALUES FROM (10) TO (20);
+CREATE INDEX gtest35_b_idx ON gtest35(b);
+INSERT INTO gtest35 SELECT g FROM generate_series(5, 14) g;
+ALTER TABLE gtest35 ADD CONSTRAINT gtest35_fk FOREIGN KEY (b) REFERENCES gtest_pk;
+
+ALTER TABLE ONLY gtest35 ALTER COLUMN b SET EXPRESSION AS (a); -- error
+ALTER TABLE gtest35_1 ALTER COLUMN b SET EXPRESSION AS (a+1); -- error
+DROP INDEX gtest35_b_idx;
+
+ALTER TABLE gtest35_1 ALTER COLUMN b SET EXPRESSION AS (case when a=7 then 200 else a + 112 end); -- error
+ALTER TABLE gtest35_2 ALTER COLUMN b SET EXPRESSION AS (case when a=10 then 200 else a + 2 end); -- error
+
+-- Test inheritance, change child table generation expression
+CREATE TABLE gtest36(a int, b int GENERATED ALWAYS AS (a) STORED NOT NULL, CONSTRAINT cc check (b < 120));
+CREATE TABLE gtest36_1 () INHERITS (gtest36);
+CREATE TABLE gtest36_12 () INHERITS (gtest36, gtest36_1);
+INSERT INTO gtest36 VALUES (8), (9);
+INSERT INTO gtest36_1 VALUES (10), (11);
+INSERT INTO gtest36_12 VALUES (12), (13);
+CREATE INDEX gtest36_b_idx ON gtest36(b);
+ALTER TABLE gtest36 ADD CONSTRAINT gtest36_fk FOREIGN KEY (b) REFERENCES gtest_pk;
+
+ALTER TABLE ONLY gtest36 ALTER COLUMN b SET EXPRESSION AS (a); -- error
+ALTER TABLE gtest36_1 ALTER COLUMN b SET EXPRESSION AS (a+1); -- error
 
 -- rule actions referring to generated columns:
 -- NEW.b in a rule action should reflect the generated column's new value
diff --git a/src/test/regress/sql/generated_virtual.sql b/src/test/regress/sql/generated_virtual.sql
index ed9d50fe784..b8b2e53ac19 100644
--- a/src/test/regress/sql/generated_virtual.sql
+++ b/src/test/regress/sql/generated_virtual.sql
@@ -844,6 +844,35 @@ ALTER TABLE gtest28a DROP COLUMN a;
 CREATE TABLE gtest28b (LIKE gtest28a INCLUDING GENERATED);
 
 \d gtest28*
+-- Test table partitioning, change child table generation expression
+CREATE TABLE gtest_pk AS SELECT g::int as b FROM generate_series(1, 100) g;
+ALTER TABLE gtest_pk ADD PRIMARY KEY (b);
+CREATE TABLE gtest35(a int, b int GENERATED ALWAYS AS (a) STORED NOT NULL, CONSTRAINT cc check (b < 120)) PARTITION BY RANGE (a);
+CREATE TABLE gtest35_1 PARTITION OF gtest35 FOR VALUES FROM (1) TO (10);
+CREATE TABLE gtest35_2 PARTITION OF gtest35 FOR VALUES FROM (10) TO (20);
+-- CREATE INDEX gtest35_b_idx ON gtest35(b);
+INSERT INTO gtest35 SELECT g FROM generate_series(5, 14) g;
+-- ALTER TABLE gtest35 ADD CONSTRAINT gtest35_fk FOREIGN KEY (b) REFERENCES gtest_pk;
+
+ALTER TABLE ONLY gtest35 ALTER COLUMN b SET EXPRESSION AS (a); -- error
+ALTER TABLE gtest35_1 ALTER COLUMN b SET EXPRESSION AS (a+1); -- error
+DROP INDEX gtest35_b_idx;
+
+ALTER TABLE gtest35_1 ALTER COLUMN b SET EXPRESSION AS (case when a=7 then 200 else a + 112 end); -- error
+ALTER TABLE gtest35_2 ALTER COLUMN b SET EXPRESSION AS (case when a=10 then 200 else a + 2 end); -- error
+
+-- Test inheritance, change child table generation expression
+CREATE TABLE gtest36(a int, b int GENERATED ALWAYS AS (a) STORED NOT NULL, CONSTRAINT cc check (b < 120));
+CREATE TABLE gtest36_1 () INHERITS (gtest36);
+CREATE TABLE gtest36_12 () INHERITS (gtest36, gtest36_1);
+INSERT INTO gtest36 VALUES (8), (9);
+INSERT INTO gtest36_1 VALUES (10), (11);
+INSERT INTO gtest36_12 VALUES (12), (13);
+CREATE INDEX gtest36_b_idx ON gtest36(b);
+ALTER TABLE gtest36 ADD CONSTRAINT gtest36_fk FOREIGN KEY (b) REFERENCES gtest_pk;
+
+ALTER TABLE ONLY gtest36 ALTER COLUMN b SET EXPRESSION AS (a); -- error
+ALTER TABLE gtest36_1 ALTER COLUMN b SET EXPRESSION AS (a+1); -- error
 
 -- rule actions referring to generated columns:
 -- NEW.b in a rule action should reflect the generated column's new value
-- 
2.34.1

