From 63bcfe1baad72a166f957c1b6ed70d88f6b469cd Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 22 Dec 2025 23:28:00 +0800
Subject: [PATCH v6 1/2] fix DDL wholerow referenced constraints and indexes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

1. ALTER TABLE DROP COLUMN.
ALTER TABLE DROP COLUMN should remove indexes or constraints contain
whole-row expression.
We enumerate all constraints and indexes and check one by one to determine
whether it contains a whole-row Var reference. If such a reference is found, we
record the dependency and later performMultipleDeletions will do the job.

for example:
CREATE TABLE ts (a int, constraint cc check((ts = ROW(1))));
CREATE INDEX tsi3 on ts ((ts is null));
ALTER TABLE DROP COLUMN should drop above all indexes, constraints on table ts.

2. ALTER COLUMN SET DATA TYPE
ALTER COLUMN SET DATA TYPE should error out when whole-row referenced object
(index, constraints, etc) exists.
ALTER COLUMN SET DATA TYPE fundamentally changes the table’s record type; At
present, we cannot compare records that contain columns of dissimilar types, see
function record_eq.  As a result, ALTER COLUMN SET DATA TYPE does not work for
whole-row reference objects (such as constraints and indexes), and must
therefore raise an error.

For example, below ALTER COLUMN SET DATA TYPE should fail.
CREATE TABLE ts (a int, CONSTRAINT cc CHECK ((ts = ROW(1))));
CREATE INDEX ON ts ((ts IS NOT NULL));
ALTER TABLE ts ALTER COLUMN a SET DATA TYPE int8;

discussion: https://postgr.es/m/CACJufxGA6KVQy7DbHGLVw9s9KKmpGyZt5ME6C7kEfjDpr2wZCw@mail.gmail.com
---
 src/backend/commands/tablecmds.c          | 248 +++++++++++++++++++++-
 src/test/regress/expected/constraints.out |  26 +++
 src/test/regress/expected/indexing.out    |  22 ++
 src/test/regress/sql/constraints.sql      |  21 ++
 src/test/regress/sql/indexing.sql         |  13 ++
 5 files changed, 327 insertions(+), 3 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 6b1a00ed477..a541b795d48 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -745,6 +745,9 @@ static void ATExecMergePartitions(List **wqueue, AlteredTableInfo *tab, Relation
 static void ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab,
 								 Relation rel, PartitionCmd *cmd,
 								 AlterTableUtilityContext *context);
+static void recordWholeRowDependencyOnOrError(Relation rel,
+											  const ObjectAddress *object,
+											  bool error_out);
 
 /* ----------------------------------------------------------------
  *		DefineRelation
@@ -9393,6 +9396,21 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 
 	ReleaseSysCache(tuple);
 
+	object.classId = RelationRelationId;
+	object.objectId = RelationGetRelid(rel);
+	object.objectSubId = attnum;
+
+	/*
+	 * We should also remove indexes or constraints that contain whole-row
+	 * expression. Using recordWholeRowDependencyOnOrError to establish a
+	 * dependency between the column and any constraint or index involving
+	 * whole-row Vars.  performMultipleDeletions will then take care of
+	 * removing them later.
+	 */
+	recordWholeRowDependencyOnOrError(rel, &object, false);
+
+	CommandCounterIncrement();
+
 	/*
 	 * Propagate to children as appropriate.  Unlike most other ALTER
 	 * routines, we have to do this one level of recursion at a time; we can't
@@ -9486,9 +9504,6 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 	}
 
 	/* Add object to delete */
-	object.classId = RelationRelationId;
-	object.objectId = RelationGetRelid(rel);
-	object.objectSubId = attnum;
 	add_exact_object_address(&object, addrs);
 
 	if (!recursing)
@@ -14773,6 +14788,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 	SysScanDesc scan;
 	HeapTuple	depTup;
 	ObjectAddress address;
+	ObjectAddress object;
 
 	/*
 	 * Clear all the missing values if we're rewriting the table, since this
@@ -14868,6 +14884,16 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 	 */
 	RememberAllDependentForRebuilding(tab, AT_AlterColumnType, rel, attnum, colName);
 
+	object.classId = RelationRelationId;
+	object.objectId = RelationGetRelid(rel);
+	object.objectSubId = attnum;
+
+	/*
+	 * Check for whole-row referenced objects (constraints, indexes etc) --
+	 * can't cope
+	 */
+	recordWholeRowDependencyOnOrError(rel, &object, true);
+
 	/*
 	 * Now scan for dependencies of this column on other things.  The only
 	 * things we should find are the dependency on the column datatype and
@@ -23337,3 +23363,219 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	/* Restore the userid and security context. */
 	SetUserIdAndSecContext(save_userid, save_sec_context);
 }
+
+/*
+ * Record dependencies between whole-row objects (indexes, CHECK constraints)
+ * and the relation's ObjectAddress.
+ *
+ * error_out means can not install such dependency, error out explicitly.
+ */
+static void
+recordWholeRowDependencyOnOrError(Relation rel, const ObjectAddress *object, bool error_out)
+{
+	Node	   *expr = NULL;
+	ScanKeyData skey;
+	Relation	pg_index;
+	SysScanDesc indscan;
+	HeapTuple	htup;
+	ObjectAddress idx_obj;
+	HeapTuple	indexTuple;
+	Form_pg_index indexStruct;
+	List	   *indexlist = NIL;
+	Bitmapset  *expr_attrs = NULL;
+	Datum		exprDatum;
+	char	   *exprString;
+	bool		isnull;
+	bool		find_wholerow = false;
+	TupleConstr *constr = RelationGetDescr(rel)->constr;
+
+	/*
+	 * Loop through each CHECK constraint, see if it contain whole-row
+	 * references or not
+	 */
+	if (constr && constr->num_check > 0)
+	{
+		Relation	pg_constraint;
+		SysScanDesc conscan;
+		ObjectAddress con_obj;
+
+		pg_constraint = table_open(ConstraintRelationId, AccessShareLock);
+
+		/* Search pg_constraint for relevant entries */
+		ScanKeyInit(&skey,
+					Anum_pg_constraint_conrelid,
+					BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(RelationGetRelid(rel)));
+
+		conscan = systable_beginscan(pg_constraint, ConstraintRelidTypidNameIndexId, true,
+									 NULL, 1, &skey);
+		while (HeapTupleIsValid(htup = systable_getnext(conscan)))
+		{
+			Form_pg_constraint conform = (Form_pg_constraint) GETSTRUCT(htup);
+
+			if (conform->contype != CONSTRAINT_CHECK)
+				continue;
+
+			/* Grab and test conbin is actually set */
+			exprDatum = fastgetattr(htup,
+									Anum_pg_constraint_conbin,
+									RelationGetDescr(pg_constraint), &isnull);
+			if (isnull)
+				elog(WARNING, "null conbin for relation \"%s\"",
+					 RelationGetRelationName(rel));
+			else
+			{
+				char	   *s = TextDatumGetCString(exprDatum);
+
+				expr = stringToNode(s);
+				pfree(s);
+
+				/* Find all attributes referenced */
+				pull_varattnos(expr, 1, &expr_attrs);
+
+				find_wholerow = bms_is_member(0 - FirstLowInvalidHeapAttributeNumber,
+											  expr_attrs);
+
+				if (find_wholerow)
+				{
+					if (error_out)
+						ereport(ERROR,
+								errcode(ERRCODE_DATATYPE_MISMATCH),
+								errmsg("cannot alter table \"%s\" because constraint \"%s\" uses its row type",
+									   RelationGetRelationName(rel),
+									   NameStr(conform->conname)),
+								errhint("You might need to drop constraint \"%s\" first",
+										NameStr(conform->conname)));
+					else
+					{
+						con_obj.classId = ConstraintRelationId;
+						con_obj.objectId = conform->oid;
+						con_obj.objectSubId = 0;
+
+						/*
+						 * record dependency for constraints that references
+						 * whole-row
+						 */
+						recordDependencyOn(&con_obj, object, DEPENDENCY_AUTO);
+					}
+				}
+			}
+		}
+		systable_endscan(conscan);
+		table_close(pg_constraint, AccessShareLock);
+	}
+
+	/* now checking indexes contain whole-row references or not */
+	/* Prepare to scan pg_index for entries having indrelid = this rel */
+	ScanKeyInit(&skey,
+				Anum_pg_index_indrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(rel)));
+
+	pg_index = table_open(IndexRelationId, AccessShareLock);
+
+	indscan = systable_beginscan(pg_index, IndexIndrelidIndexId, true,
+								 NULL, 1, &skey);
+	while (HeapTupleIsValid(htup = systable_getnext(indscan)))
+	{
+		Form_pg_index index = (Form_pg_index) GETSTRUCT(htup);
+
+		/* add index's OID to result list */
+		indexlist = lappend_oid(indexlist, index->indexrelid);
+	}
+	systable_endscan(indscan);
+
+	table_close(pg_index, AccessShareLock);
+
+	foreach_oid(indexoid, indexlist)
+	{
+		indexTuple = SearchSysCache1(INDEXRELID, ObjectIdGetDatum(indexoid));
+		if (!HeapTupleIsValid(indexTuple))
+			elog(ERROR, "cache lookup failed for index %u", indexoid);
+		indexStruct = (Form_pg_index) GETSTRUCT(indexTuple);
+
+		if (!heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL))
+		{
+			expr_attrs = NULL;
+
+			/* Convert text string to node tree */
+			exprDatum = SysCacheGetAttrNotNull(INDEXRELID, indexTuple,
+											   Anum_pg_index_indexprs);
+			exprString = TextDatumGetCString(exprDatum);
+			expr = (Node *) stringToNode(exprString);
+			pfree(exprString);
+
+			pull_varattnos(expr, 1, &expr_attrs);
+
+			find_wholerow = bms_is_member(0 - FirstLowInvalidHeapAttributeNumber,
+										  expr_attrs);
+
+			if (find_wholerow)
+			{
+				if (error_out)
+					ereport(ERROR,
+							errcode(ERRCODE_DATATYPE_MISMATCH),
+							errmsg("cannot alter table \"%s\" because index \"%s\" uses its row type",
+								   RelationGetRelationName(rel),
+								   get_rel_name(indexStruct->indexrelid)),
+							errhint("You might need to drop index \"%s\" first",
+									get_rel_name(indexStruct->indexrelid)));
+				else
+				{
+					idx_obj.classId = RelationRelationId;
+					idx_obj.objectId = indexStruct->indexrelid;
+					idx_obj.objectSubId = 0;
+
+					/* record dependency for indexes that references whole-row */
+					recordDependencyOn(&idx_obj, object, DEPENDENCY_AUTO);
+
+					ReleaseSysCache(indexTuple);
+
+					continue;
+				}
+			}
+		}
+
+		if (!heap_attisnull(indexTuple, Anum_pg_index_indpred, NULL))
+		{
+			expr_attrs = NULL;
+
+			/* Convert text string to node tree */
+			exprDatum = SysCacheGetAttrNotNull(INDEXRELID, indexTuple,
+											   Anum_pg_index_indpred);
+			exprString = TextDatumGetCString(exprDatum);
+			expr = (Node *) stringToNode(exprString);
+			pfree(exprString);
+
+			pull_varattnos(expr, 1, &expr_attrs);
+
+			find_wholerow = bms_is_member(0 - FirstLowInvalidHeapAttributeNumber,
+										  expr_attrs);
+			if (find_wholerow)
+			{
+				if (error_out)
+					ereport(ERROR,
+							errcode(ERRCODE_DATATYPE_MISMATCH),
+							errmsg("cannot alter table \"%s\" because index \"%s\" uses its row type",
+								   RelationGetRelationName(rel),
+								   get_rel_name(indexStruct->indexrelid)),
+							errhint("You might need to drop index \"%s\" first",
+									get_rel_name(indexStruct->indexrelid)));
+				else
+				{
+					idx_obj.classId = RelationRelationId;
+					idx_obj.objectId = indexStruct->indexrelid;
+					idx_obj.objectSubId = 0;
+
+					/* record dependency for indexes that references whole-row */
+					recordDependencyOn(&idx_obj, object, DEPENDENCY_AUTO);
+
+					ReleaseSysCache(indexTuple);
+
+					continue;
+				}
+			}
+		}
+		ReleaseSysCache(indexTuple);
+	}
+}
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 1bbf59cca02..6e26e9fbcfc 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -254,6 +254,32 @@ ERROR:  system column "ctid" reference in check constraint is invalid
 LINE 3:       CHECK (NOT (is_capital AND ctid::text = 'sys_col_check...
                                          ^
 --
+-- Drop column also drop check constraints that have whole-row reference
+--
+CREATE TABLE DROP_COL_CHECK_TBL (
+    city TEXT, state TEXT,
+    CONSTRAINT cc0 CHECK (DROP_COL_CHECK_TBL is null) NOT ENFORCED,
+    CONSTRAINT cc1 CHECK (DROP_COL_CHECK_TBL is not null) NOT ENFORCED);
+ALTER TABLE DROP_COL_CHECK_TBL DROP COLUMN city;
+\d DROP_COL_CHECK_TBL
+       Table "public.drop_col_check_tbl"
+ Column | Type | Collation | Nullable | Default 
+--------+------+-----------+----------+---------
+ state  | text |           |          | 
+
+DROP TABLE DROP_COL_CHECK_TBL;
+--
+-- Change column data type should error out because the whole-row referenced
+-- check constraint is still need it.
+--
+CREATE TABLE ALTER_COL_CHECK_TBL (
+    city INT, state TEXT,
+    CONSTRAINT cc1 CHECK (ALTER_COL_CHECK_TBL is not null) NOT ENFORCED);
+ALTER TABLE ALTER_COL_CHECK_TBL ALTER COLUMN city SET DATA TYPE int8;
+ERROR:  cannot alter table "alter_col_check_tbl" because constraint "cc1" uses its row type
+HINT:  You might need to drop constraint "cc1" first
+DROP TABLE ALTER_COL_CHECK_TBL;
+--
 -- Check inheritance of defaults and constraints
 --
 CREATE TABLE INSERT_CHILD (cx INT default 42,
diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out
index 4d29fb85293..4b683c679a8 100644
--- a/src/test/regress/expected/indexing.out
+++ b/src/test/regress/expected/indexing.out
@@ -654,6 +654,28 @@ alter table idxpart2 drop column c;
  b      | integer |           |          | 
 
 drop table idxpart, idxpart2;
+create table idxpart (a int, b int, c int);
+create index on idxpart(c);
+create index idxpart_idx1 on idxpart((idxpart is not null));
+create index idxpart_idx2 on idxpart(a) where idxpart is not null;
+alter table idxpart drop column c;
+\d idxpart
+              Table "public.idxpart"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+
+create index idxpart_idx1 on idxpart((idxpart is not null));
+create index idxpart_idx2 on idxpart(a) where idxpart is not null;
+alter table idxpart alter column a set data type int8; --error
+ERROR:  cannot alter table "idxpart" because index "idxpart_idx1" uses its row type
+HINT:  You might need to drop index "idxpart_idx1" first
+drop index idxpart_idx1;
+alter table idxpart alter column b set data type int8; --error
+ERROR:  cannot alter table "idxpart" because index "idxpart_idx2" uses its row type
+HINT:  You might need to drop index "idxpart_idx2" first
+drop table idxpart;
 -- Verify that expression indexes inherit correctly
 create table idxpart (a int, b int) partition by range (a);
 create table idxpart1 (like idxpart);
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 733a1dbccfe..c7ae3fb6641 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -165,6 +165,27 @@ CREATE TABLE SYS_COL_CHECK_TBL (city text, state text, is_capital bool,
                   altitude int,
 				  CHECK (NOT (is_capital AND ctid::text = 'sys_col_check_tbl')));
 
+--
+-- Drop column also drop check constraints that have whole-row reference
+--
+CREATE TABLE DROP_COL_CHECK_TBL (
+    city TEXT, state TEXT,
+    CONSTRAINT cc0 CHECK (DROP_COL_CHECK_TBL is null) NOT ENFORCED,
+    CONSTRAINT cc1 CHECK (DROP_COL_CHECK_TBL is not null) NOT ENFORCED);
+ALTER TABLE DROP_COL_CHECK_TBL DROP COLUMN city;
+\d DROP_COL_CHECK_TBL
+DROP TABLE DROP_COL_CHECK_TBL;
+
+--
+-- Change column data type should error out because the whole-row referenced
+-- check constraint is still need it.
+--
+CREATE TABLE ALTER_COL_CHECK_TBL (
+    city INT, state TEXT,
+    CONSTRAINT cc1 CHECK (ALTER_COL_CHECK_TBL is not null) NOT ENFORCED);
+ALTER TABLE ALTER_COL_CHECK_TBL ALTER COLUMN city SET DATA TYPE int8;
+DROP TABLE ALTER_COL_CHECK_TBL;
+
 --
 -- Check inheritance of defaults and constraints
 --
diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql
index b5cb01c2d70..b7c08c9ff5a 100644
--- a/src/test/regress/sql/indexing.sql
+++ b/src/test/regress/sql/indexing.sql
@@ -295,6 +295,19 @@ alter table idxpart2 drop column c;
 \d idxpart2
 drop table idxpart, idxpart2;
 
+create table idxpart (a int, b int, c int);
+create index on idxpart(c);
+create index idxpart_idx1 on idxpart((idxpart is not null));
+create index idxpart_idx2 on idxpart(a) where idxpart is not null;
+alter table idxpart drop column c;
+\d idxpart
+create index idxpart_idx1 on idxpart((idxpart is not null));
+create index idxpart_idx2 on idxpart(a) where idxpart is not null;
+alter table idxpart alter column a set data type int8; --error
+drop index idxpart_idx1;
+alter table idxpart alter column b set data type int8; --error
+drop table idxpart;
+
 -- Verify that expression indexes inherit correctly
 create table idxpart (a int, b int) partition by range (a);
 create table idxpart1 (like idxpart);
-- 
2.34.1

