From 6973c85d1868ce0a39596a2415a66c47affd9637 Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Tue, 14 Jul 2026 11:57:52 +0800
Subject: [PATCH v14 1/1] Whole-row fixes for DROP COLUMN, SET COLUMN DATA TYPE
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

ALTER TABLE DROP COLUMN should remove indexes or constraints contain whole-row
references, just like non-whole-row column.

ALTER TABLE DROP COLUMN should fail if a trigger WHEN clause or row-level
security policy contains a whole-row reference. To do this, record a dependency
between the relation and the trigger or policy in
RememberWholeRowDependentForRebuilding; performMultipleDeletions then handles
the deletion checks.

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.

discussion: https://postgr.es/m/CACJufxGA6KVQy7DbHGLVw9s9KKmpGyZt5ME6C7kEfjDpr2wZCw@mail.gmail.com
commitfest: https://commitfest.postgresql.org/patch/6055
---
 src/backend/commands/tablecmds.c           | 319 ++++++++++++++++++++-
 src/backend/optimizer/util/var.c           |  46 +++
 src/include/optimizer/optimizer.h          |   1 +
 src/test/regress/expected/constraints.out  |  23 ++
 src/test/regress/expected/foreign_data.out |  14 +
 src/test/regress/expected/indexing.out     |  32 +++
 src/test/regress/expected/rowsecurity.out  |  29 +-
 src/test/regress/expected/triggers.out     |  28 ++
 src/test/regress/sql/constraints.sql       |  17 ++
 src/test/regress/sql/foreign_data.sql      |  10 +
 src/test/regress/sql/indexing.sql          |  17 ++
 src/test/regress/sql/rowsecurity.sql       |  17 ++
 src/test/regress/sql/triggers.sql          |  17 ++
 13 files changed, 560 insertions(+), 10 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index cb93c3e935a..d53f410efe1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -693,7 +693,13 @@ static ObjectAddress ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 										   AlterTableCmd *cmd, LOCKMODE lockmode);
 static void RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 											  Relation rel, AttrNumber attnum, const char *colName);
-static void RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel);
+static void RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
+												   Relation rel, AttrNumber attnum,
+												   const char *colName);
+static void recordWholeRowDependencyOrError(AlteredTableInfo *tab, AlterTableType subtype,
+											Relation rel, AttrNumber attnum,
+											const ObjectAddress *depender,
+											DependencyType behavior);
 static void RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab);
 static void RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab);
 static void RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab);
@@ -795,6 +801,8 @@ static List *collectPartitionIndexExtDeps(List *partitionOids);
 static void applyPartitionIndexExtDeps(Oid newPartOid, List *extDepState);
 static void freePartitionIndexExtDeps(List *extDepState);
 
+static List *GetAllRelAssociatedPolicies(Relation rel);
+
 /* ----------------------------------------------------------------
  *		DefineRelation
  *				Creates a new relation.
@@ -8822,7 +8830,7 @@ ATExecSetExpression(AlteredTableInfo *tab, Relation rel, const char *colName,
 	 * (constraints, indexes, etc.), and record enough information to let us
 	 * recreate the objects.
 	 */
-	RememberWholeRowDependentForRebuilding(tab, AT_SetExpression, rel);
+	RememberWholeRowDependentForRebuilding(tab, AT_SetExpression, rel, attnum, colName);
 
 	/*
 	 * Drop the dependency records of the GENERATED expression, in particular
@@ -9428,6 +9436,10 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 	List	   *children;
 	ObjectAddress object;
 	bool		is_expr;
+	AlteredTableInfo *tab;
+
+	/* Find or create work queue entry for this table */
+	tab = ATGetQueueEntry(wqueue, rel);
 
 	/* At top level, permission check was done in ATPrepCmd, else do it */
 	if (recursing)
@@ -9500,6 +9512,15 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 
 	ReleaseSysCache(tuple);
 
+	/*
+	 * Record dependencies between this relation and any objects containing
+	 * whole-row Var references. performMultipleDeletions will take care of
+	 * removing these dependencies later.
+	 */
+	RememberWholeRowDependentForRebuilding(tab, AT_DropColumn, rel, attnum, colName);
+
+	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
@@ -15377,6 +15398,14 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
 	 */
 	RememberAllDependentForRebuilding(tab, AT_AlterColumnType, rel, attnum, colName);
 
+	/*
+	 * 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, so errr out for wholerow
+	 * dependencies.
+	 */
+	RememberWholeRowDependentForRebuilding(tab, AT_AlterColumnType, rel, attnum, colName);
+
 	/*
 	 * Now scan for dependencies of this column on other things.  The only
 	 * things we should find are the dependency on the column datatype and
@@ -15805,21 +15834,30 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
  * See also RememberAllDependentForRebuilding, which handles non-whole-row Var
  * references.
  *
- * This function currently applies only to ALTER COLUMN SET EXPRESSION.
+ * This function currently applies to ALTER COLUMN SET EXPRESSION,
+ * ALTER COLUMN SET DATA TYPE, and ALTER TABLE DROP COLUMN.
  */
 static void
-RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel)
+RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype, Relation rel,
+									   AttrNumber attnum, const char *colName)
 {
 	ScanKeyData skey;
 	Relation	pg_constraint;
+	Relation	pg_policy;
 	Relation	pg_index;
 	SysScanDesc conscan;
 	SysScanDesc indscan;
+	SysScanDesc policyscan;
 	HeapTuple	constrTuple;
 	HeapTuple	indexTuple;
+	HeapTuple	policyTuple;
 	bool		isnull;
+	List	   *pols = NIL;
+	Oid			reltypid;
 
-	Assert(subtype == AT_SetExpression);
+	Assert(subtype == AT_SetExpression ||
+		   subtype == AT_AlterColumnType ||
+		   subtype == AT_DropColumn);
 
 	/*
 	 * Check CHECK constraints with whole-row references first.
@@ -15875,7 +15913,21 @@ RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType sub
 				 */
 				if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs))
 				{
-					RememberConstraintForRebuilding(conform->oid, tab);
+					if (subtype == AT_SetExpression)
+						RememberConstraintForRebuilding(conform->oid, tab);
+					else
+					{
+						ObjectAddress con_obj;
+
+						ObjectAddressSet(con_obj, ConstraintRelationId, conform->oid);
+
+						/*
+						 * The dependency between the CHECK constraint and its
+						 * relation is DEPENDENCY_AUTO.
+						 */
+						recordWholeRowDependencyOrError(tab, subtype, rel, attnum,
+														&con_obj, DEPENDENCY_AUTO);
+					}
 				}
 			}
 		}
@@ -15929,7 +15981,21 @@ RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType sub
 			 */
 			if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs))
 			{
-				RememberIndexForRebuilding(index->indexrelid, tab);
+				if (subtype == AT_SetExpression)
+					RememberIndexForRebuilding(index->indexrelid, tab);
+				else
+				{
+					ObjectAddress idx_obj;
+
+					ObjectAddressSet(idx_obj, RelationRelationId, index->indexrelid);
+
+					/*
+					 * The index has a DEPENDENCY_AUTO relationship with its
+					 * relation.
+					 */
+					recordWholeRowDependencyOrError(tab, subtype, rel, attnum,
+													&idx_obj, DEPENDENCY_AUTO);
+				}
 				continue;
 			}
 		}
@@ -15957,13 +16023,172 @@ RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType sub
 			 */
 			if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber, expr_attrs))
 			{
-				RememberIndexForRebuilding(index->indexrelid, tab);
+				if (subtype == AT_SetExpression)
+					RememberIndexForRebuilding(index->indexrelid, tab);
+				else
+				{
+					ObjectAddress idx_obj;
+
+					ObjectAddressSet(idx_obj, RelationRelationId, index->indexrelid);
+
+					/*
+					 * The index has a DEPENDENCY_AUTO relationship with its
+					 * relation
+					 */
+					recordWholeRowDependencyOrError(tab, subtype, rel, attnum,
+													&idx_obj, DEPENDENCY_AUTO);
+				}
 			}
 		}
 	}
 
 	systable_endscan(indscan);
 	table_close(pg_index, AccessShareLock);
+
+	/*
+	 * Now checking trigger, policy whole-row references, ALTER COLUMN SET
+	 * EXPRESSION does not do anything about it.
+	 */
+
+	/*
+	 * For ALTER TABLE SET EXPRESSION:
+	 *
+	 * 1.No need to check trigger with whole-row references. Creation of
+	 * BEFORE triggers with whole-row Vars referencing (some column is
+	 * generated column) is disallowed; see CreateTriggerFiringOn().
+	 * Additionally, there is no need to worry about AFTER triggers; even if
+	 * the trigger were recreated (which it is not), its WHEN qualification
+	 * would remain unchanged.
+	 *
+	 * 2. No need to recheck policies with whole-row references, since we do
+	 * not recreate and re-evaluate the policy condition when a dependent
+	 * column's generated expression changes.
+	 */
+	if (subtype == AT_AlterColumnType || subtype == AT_DropColumn)
+	{
+		if (rel->trigdesc != NULL)
+		{
+			Node	   *expr;
+
+			for (int i = 0; i < rel->trigdesc->numtriggers; i++)
+			{
+				Bitmapset  *expr_attrs = NULL;
+				Trigger    *trig = &rel->trigdesc->triggers[i];
+
+				if (trig->tgqual == NULL)
+					continue;
+
+				expr = stringToNode(trig->tgqual);
+
+				pull_varattnos(expr, PRS2_OLD_VARNO, &expr_attrs);
+
+				pull_varattnos(expr, PRS2_NEW_VARNO, &expr_attrs);
+
+				/*
+				 * If the triger WHEN qual contains whole-row reference then
+				 * remember it
+				 */
+				if (bms_is_member(InvalidAttrNumber - FirstLowInvalidHeapAttributeNumber,
+								  expr_attrs))
+				{
+					ObjectAddress trigObj;
+
+					ObjectAddressSet(trigObj, TriggerRelationId, trig->tgoid);
+
+					/*
+					 * The dependency between the trigger and its relation is
+					 * DEPENDENCY_NORMAL
+					 */
+					recordWholeRowDependencyOrError(tab, subtype, rel, attnum,
+													&trigObj, DEPENDENCY_NORMAL);
+				}
+			}
+		}
+
+		/* Now checking policy whole-row references */
+		reltypid = get_rel_type_id(RelationGetRelid(rel));
+
+		pg_policy = table_open(PolicyRelationId, AccessShareLock);
+
+		/*
+		 * We cannot simply look up pg_policy->polrelid because a policy's
+		 * USING or CHECK expression may reference other relations. Instead,
+		 * we must scan pg_depend to identify all policies that depend on this
+		 * relation.
+		 */
+		pols = GetAllRelAssociatedPolicies(rel);
+
+		foreach_oid(policyoid, pols)
+		{
+			Datum		exprDatum;
+			char	   *exprString;
+			Node	   *expr;
+			ObjectAddress polObj;
+
+			ScanKeyInit(&skey,
+						Anum_pg_policy_oid,
+						BTEqualStrategyNumber, F_OIDEQ,
+						ObjectIdGetDatum(policyoid));
+			policyscan = systable_beginscan(pg_policy,
+											PolicyOidIndexId,
+											true,
+											NULL,
+											1,
+											&skey);
+			while (HeapTupleIsValid(policyTuple = systable_getnext(policyscan)))
+			{
+				Form_pg_policy policy = (Form_pg_policy) GETSTRUCT(policyTuple);
+
+				exprDatum = heap_getattr(policyTuple, Anum_pg_policy_polqual,
+										 RelationGetDescr(pg_policy),
+										 &isnull);
+				if (!isnull)
+				{
+					exprString = TextDatumGetCString(exprDatum);
+					expr = (Node *) stringToNode(exprString);
+					pfree(exprString);
+
+					if (expr_contain_wholerow(expr, reltypid))
+					{
+						ObjectAddressSet(polObj, PolicyRelationId, policy->oid);
+
+						/*
+						 * The dependency between the policy and it's relation
+						 * is DEPENDENCY_NORMAL
+						 */
+						recordWholeRowDependencyOrError(tab, subtype, rel, attnum,
+														&polObj, DEPENDENCY_NORMAL);
+
+						continue;
+					}
+				}
+
+				exprDatum = heap_getattr(policyTuple, Anum_pg_policy_polwithcheck,
+										 RelationGetDescr(pg_policy),
+										 &isnull);
+				if (!isnull)
+				{
+					exprString = TextDatumGetCString(exprDatum);
+					expr = (Node *) stringToNode(exprString);
+					pfree(exprString);
+
+					if (expr_contain_wholerow(expr, reltypid))
+					{
+						ObjectAddressSet(polObj, PolicyRelationId, policy->oid);
+
+						/*
+						 * The dependency between the policy and it's relation
+						 * is DEPENDENCY_NORMAL
+						 */
+						recordWholeRowDependencyOrError(tab, subtype, rel, attnum,
+														&polObj, DEPENDENCY_NORMAL);
+					}
+				}
+			}
+			systable_endscan(policyscan);
+		}
+		table_close(pg_policy, AccessShareLock);
+	}
 }
 
 /*
@@ -24390,3 +24615,81 @@ ATExecSplitPartition(List **wqueue, AlteredTableInfo *tab, Relation rel,
 	/* Restore the userid and security context. */
 	SetUserIdAndSecContext(save_userid, save_sec_context);
 }
+
+/*
+ * Record dependencies between objects contains whole-row Var references
+ * (indexes, CHECK constraints, etc.) and the relation, or report an error. This
+ * is used for ALTER TABLE DROP COLUMN, ALTER COLUMN SET DATA TYPE only.  ALTER
+ * TABLE SET EXPRESSION relies on the existing Remember...ForRebuilding to
+ * perform the required work.
+ */
+static void
+recordWholeRowDependencyOrError(AlteredTableInfo *tab, AlterTableType subtype,
+								Relation rel, AttrNumber attnum,
+								const ObjectAddress *depender,
+								DependencyType behavior)
+{
+	if (subtype == AT_AlterColumnType)
+		ereport(ERROR,
+				errcode(ERRCODE_DATATYPE_MISMATCH),
+				errmsg("cannot alter table \"%s\" because %s uses its row type",
+					   RelationGetRelationName(rel),
+					   getObjectDescription(depender, false)),
+				errhint("You might need to drop %s first",
+						getObjectDescription(depender, false)));
+	else
+	{
+		ObjectAddress referenced;
+
+		Assert(subtype == AT_DropColumn);
+
+		ObjectAddressSubSet(referenced, RelationRelationId,
+							RelationGetRelid(rel), attnum);
+
+		recordDependencyOn(depender, &referenced, behavior);
+	}
+}
+
+/*
+ * GetAllRelAssociatedPolicies
+ *
+ * Returns a list of OIDs of all row-level security policies associated with the
+ * given relation.
+ */
+static List *
+GetAllRelAssociatedPolicies(Relation rel)
+{
+	Relation	depRel;
+	ScanKeyData key[3];
+	SysScanDesc scan;
+	HeapTuple	depTup;
+	List	   *result = NIL;
+
+	depRel = table_open(DependRelationId, AccessShareLock);
+	ScanKeyInit(&key[0],
+				Anum_pg_depend_refclassid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationRelationId));
+	ScanKeyInit(&key[1],
+				Anum_pg_depend_refobjid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(rel)));
+	ScanKeyInit(&key[2],
+				Anum_pg_depend_refobjsubid,
+				BTEqualStrategyNumber, F_INT4EQ,
+				Int32GetDatum((int32) 0));
+
+	scan = systable_beginscan(depRel, DependReferenceIndexId, true,
+							  NULL, 3, key);
+	while (HeapTupleIsValid(depTup = systable_getnext(scan)))
+	{
+		Form_pg_depend foundDep = (Form_pg_depend) GETSTRUCT(depTup);
+
+		if (foundDep->classid == PolicyRelationId)
+			result = list_append_unique_oid(result, foundDep->objid);
+	}
+	systable_endscan(scan);
+	table_close(depRel, AccessShareLock);
+
+	return result;
+}
diff --git a/src/backend/optimizer/util/var.c b/src/backend/optimizer/util/var.c
index 907a255c36f..8fcf04c3057 100644
--- a/src/backend/optimizer/util/var.c
+++ b/src/backend/optimizer/util/var.c
@@ -73,6 +73,7 @@ typedef struct
 static bool pull_varnos_walker(Node *node,
 							   pull_varnos_context *context);
 static bool pull_varattnos_walker(Node *node, pull_varattnos_context *context);
+static bool expr_contain_wholerow_walker(Node *node, Oid *context);
 static bool pull_vars_walker(Node *node, pull_vars_context *context);
 static bool contain_var_clause_walker(Node *node, void *context);
 static bool contain_vars_of_level_walker(Node *node, int *sublevels_up);
@@ -327,6 +328,51 @@ pull_varattnos_walker(Node *node, pull_varattnos_context *context)
 	return expression_tree_walker(node, pull_varattnos_walker, context);
 }
 
+static bool
+expr_contain_wholerow_walker(Node *node, Oid *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, Var))
+	{
+		Var		   *var = (Var *) node;
+
+		if (var->varattno == InvalidAttrNumber &&
+			var->vartype == *context)
+			return true;
+
+		return false;
+	}
+
+	if (IsA(node, Query))
+		return query_tree_walker((Query *) node, expr_contain_wholerow_walker,
+								 context, 0);
+
+	return expression_tree_walker(node, expr_contain_wholerow_walker, context);
+}
+
+/*
+ * expr_contain_wholerow -
+ *
+ * Determine whether an expression contains whole-row Var reference, recursing as needed.
+ * For simple expressions without sublinks, pull_varattnos is usually sufficient
+ * to detect a whole-row Var. But if the node contains sublinks (unplanned
+ * subqueries), the check must instead rely on the whole-row type OID.
+ *
+ * Use expr_contain_wholerow to check whole-row var existsence when in doubt.
+ */
+bool
+expr_contain_wholerow(Node *node, Oid reltypid)
+{
+	Assert(OidIsValid(reltypid));
+
+	return query_or_expression_tree_walker(node,
+										   expr_contain_wholerow_walker,
+										   &reltypid,
+										   0);
+}
+
 
 /*
  * pull_vars_of_level
diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h
index cb6241e2bdd..0491059c7cc 100644
--- a/src/include/optimizer/optimizer.h
+++ b/src/include/optimizer/optimizer.h
@@ -206,6 +206,7 @@ extern SortGroupClause *get_sortgroupref_clause_noerr(Index sortref,
 extern Bitmapset *pull_varnos(PlannerInfo *root, Node *node);
 extern Bitmapset *pull_varnos_of_level(PlannerInfo *root, Node *node, int levelsup);
 extern void pull_varattnos(Node *node, Index varno, Bitmapset **varattnos);
+extern bool expr_contain_wholerow(Node *node, Oid reltypid);
 extern List *pull_vars_of_level(Node *node, int levelsup);
 extern bool contain_var_clause(Node *node);
 extern bool contain_vars_of_level(Node *node, int levelsup);
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 83f97f684d5..211583d3769 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -254,6 +254,29 @@ ERROR:  system column "ctid" reference in check constraint is invalid
 LINE 3:       CHECK (NOT (is_capital AND ctid::text = 'sys_col_check...
                                          ^
 --
+-- Drop column should also drop all check constraints that contains whole-row references
+--
+-- Change column data type should fail since whole-row referenced check constraint still exists
+--
+-- No need to worry about partitioned tables, since the whole-row check constraint
+-- can not span multi relations
+CREATE TABLE wholerow_check_tbl (
+    city int,
+    state int,
+    CONSTRAINT cc0 CHECK (wholerow_check_tbl is null) NOT ENFORCED,
+    CONSTRAINT cc1 CHECK (wholerow_check_tbl is not null) NOT ENFORCED);
+ALTER TABLE wholerow_check_tbl ALTER COLUMN city SET DATA TYPE INT8; -- error
+ERROR:  cannot alter table "wholerow_check_tbl" because constraint cc0 on table wholerow_check_tbl uses its row type
+HINT:  You might need to drop constraint cc0 on table wholerow_check_tbl first
+ALTER TABLE wholerow_check_tbl DROP COLUMN city; -- ok
+\d wholerow_check_tbl
+         Table "public.wholerow_check_tbl"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ state  | integer |           |          | 
+
+DROP TABLE wholerow_check_tbl;
+--
 -- Check inheritance of defaults and constraints
 --
 CREATE TABLE INSERT_CHILD (cx INT default 42,
diff --git a/src/test/regress/expected/foreign_data.out b/src/test/regress/expected/foreign_data.out
index d8e4cb12c3d..4e2acad9dd9 100644
--- a/src/test/regress/expected/foreign_data.out
+++ b/src/test/regress/expected/foreign_data.out
@@ -1427,6 +1427,20 @@ DROP TRIGGER trigtest_before_stmt ON foreign_schema.foreign_table_1;
 DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
 DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
 DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE
+ON foreign_schema.foreign_table_1
+FOR EACH ROW
+WHEN (new IS NOT NULL)
+EXECUTE PROCEDURE dummy_trigger();
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ALTER COLUMN c7 SET DATA TYPE bigint; -- error
+ERROR:  cannot alter table "foreign_table_1" because trigger trigtest_before_stmt on foreign table foreign_schema.foreign_table_1 uses its row type
+HINT:  You might need to drop trigger trigtest_before_stmt on foreign table foreign_schema.foreign_table_1 first
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DROP COLUMN c7; -- error
+ERROR:  cannot drop column c7 of foreign table foreign_schema.foreign_table_1 because other objects depend on it
+DETAIL:  trigger trigtest_before_stmt on foreign table foreign_schema.foreign_table_1 depends on column c7 of foreign table foreign_schema.foreign_table_1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DROP COLUMN c7 CASCADE; -- ok
+NOTICE:  drop cascades to trigger trigtest_before_stmt on foreign table foreign_schema.foreign_table_1
 DROP FUNCTION dummy_trigger();
 -- Table inheritance
 CREATE TABLE fd_pt1 (
diff --git a/src/test/regress/expected/indexing.out b/src/test/regress/expected/indexing.out
index 4d350fbc658..4c4815344f2 100644
--- a/src/test/regress/expected/indexing.out
+++ b/src/test/regress/expected/indexing.out
@@ -759,6 +759,38 @@ alter table idxpart2 drop column c;
  b      | integer |           |          | 
 
 drop table idxpart, idxpart2;
+-- Drop column should also drop all indexes that contains whole-row references
+-- Change column data type should fail if whole-row referenced indexes exists
+create table idxpart (a int, b int, c int) partition by range (a);
+create table idxpart1 partition of idxpart for values from (2000) to (3000);
+create index idxpart_idx1 on idxpart1((idxpart1 is not null));
+alter table idxpart alter column b set data type bigint; -- error
+ERROR:  cannot alter table "idxpart1" because index idxpart_idx1 uses its row type
+HINT:  You might need to drop index idxpart_idx1 first
+drop index idxpart_idx1;
+create index idxpart_idx2 on idxpart1(a) where idxpart1 is not null;
+alter table idxpart alter column c set data type bigint; -- error
+ERROR:  cannot alter table "idxpart1" because index idxpart_idx2 uses its row type
+HINT:  You might need to drop index idxpart_idx2 first
+alter table idxpart drop column c; -- ok
+\d idxpart
+        Partitioned table "public.idxpart"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+Partition key: RANGE (a)
+Number of partitions: 1 (Use \d+ to list them.)
+
+\d idxpart1
+              Table "public.idxpart1"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+Partition of: idxpart FOR VALUES FROM (2000) TO (3000)
+
+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/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 3a5e82c35bd..c0f578cb813 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2731,6 +2731,32 @@ SELECT * FROM document;
   14 |  11 |      1 | regress_rls_bob   | new novel                        | 
 (16 rows)
 
+-- check drop column (no CASCADE) or alter column data type will fail because of
+-- whole-row referenced security policy exists.
+DROP TABLE  part_document;
+ALTER TABLE document ADD COLUMN dummy INT4;
+CREATE POLICY p7 ON document AS PERMISSIVE
+    USING (cid IS NOT NULL AND
+    (WITH cte AS (SELECT TRUE FROM uaccount
+        WHERE EXISTS (SELECT document FROM uaccount WHERE uaccount IS NULL))
+    SELECT * FROM cte));
+ALTER TABLE uaccount ALTER COLUMN seclv SET DATA TYPE BIGINT; -- error
+ERROR:  cannot alter table "uaccount" because policy p7 on table document uses its row type
+HINT:  You might need to drop policy p7 on table document first
+ALTER TABLE document ALTER COLUMN dummy SET DATA TYPE BIGINT; -- error
+ERROR:  cannot alter table "document" because policy p7 on table document uses its row type
+HINT:  You might need to drop policy p7 on table document first
+ALTER TABLE document DROP COLUMN dummy; -- error
+ERROR:  cannot drop column dummy of table document because other objects depend on it
+DETAIL:  policy p7 on table document depends on column dummy of table document
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER TABLE uaccount DROP COLUMN seclv; -- error
+ERROR:  cannot drop column seclv of table uaccount because other objects depend on it
+DETAIL:  policy p7 on table document depends on column seclv of table uaccount
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER TABLE uaccount DROP COLUMN seclv CASCADE; -- ok, policy p7 will be dropped
+NOTICE:  drop cascades to policy p7 on table document
+ALTER TABLE document DROP COLUMN dummy CASCADE; -- ok
 --
 -- ROLE/GROUP
 --
@@ -5199,12 +5225,11 @@ drop table rls_t, test_t;
 --
 RESET SESSION AUTHORIZATION;
 DROP SCHEMA regress_rls_schema CASCADE;
-NOTICE:  drop cascades to 30 other objects
+NOTICE:  drop cascades to 29 other objects
 DETAIL:  drop cascades to function f_leak(text)
 drop cascades to table uaccount
 drop cascades to table category
 drop cascades to table document
-drop cascades to table part_document
 drop cascades to table dependent
 drop cascades to table rec1
 drop cascades to table rec2
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 8fcb33ac81a..b9ddb9ca7ba 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -227,6 +227,34 @@ ERROR:  trigger "no_such_trigger" for table "main_table" does not exist
 COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS 'right';
 COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS NULL;
 --
+-- test triggers with WHEN clause contain wholerow reference
+--
+CREATE TABLE test_tbl1 (a int, b int) PARTITION BY RANGE (a);
+CREATE TABLE test_tbl1p1 PARTITION OF test_tbl1 FOR VALUES FROM (0) TO (1000);
+CREATE TRIGGER test_tbl1p1_trig
+  BEFORE INSERT OR UPDATE ON test_tbl1p1 FOR EACH ROW
+  WHEN (new = ROW (1, 1))
+  EXECUTE PROCEDURE trigger_func ('test_tbl1p1');
+ALTER TABLE test_tbl1 ALTER COLUMN b SET DATA TYPE bigint; -- error
+ERROR:  cannot alter table "test_tbl1p1" because trigger test_tbl1p1_trig on table test_tbl1p1 uses its row type
+HINT:  You might need to drop trigger test_tbl1p1_trig on table test_tbl1p1 first
+ALTER TABLE test_tbl1 DROP COLUMN b; -- error
+ERROR:  cannot drop desired object(s) because other objects depend on them
+DETAIL:  trigger test_tbl1p1_trig on table test_tbl1p1 depends on column b of table test_tbl1p1
+HINT:  Use DROP ... CASCADE to drop the dependent objects too.
+ALTER TABLE test_tbl1 DROP COLUMN b CASCADE; -- ok
+NOTICE:  drop cascades to trigger test_tbl1p1_trig on table test_tbl1p1
+\d+ test_tbl1
+                           Partitioned table "public.test_tbl1"
+ Column |  Type   | Collation | Nullable | Default | Storage | Stats target | Description 
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a      | integer |           |          |         | plain   |              | 
+Partition key: RANGE (a)
+Partitions:
+    test_tbl1p1 FOR VALUES FROM (0) TO (1000)
+
+DROP TABLE test_tbl1;
+--
 -- test triggers with WHEN clause
 --
 CREATE TRIGGER modified_a BEFORE UPDATE OF a ON main_table
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 9705962eb9f..3d25c85b4b1 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -165,6 +165,23 @@ 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 should also drop all check constraints that contains whole-row references
+--
+-- Change column data type should fail since whole-row referenced check constraint still exists
+--
+-- No need to worry about partitioned tables, since the whole-row check constraint
+-- can not span multi relations
+CREATE TABLE wholerow_check_tbl (
+    city int,
+    state int,
+    CONSTRAINT cc0 CHECK (wholerow_check_tbl is null) NOT ENFORCED,
+    CONSTRAINT cc1 CHECK (wholerow_check_tbl is not null) NOT ENFORCED);
+ALTER TABLE wholerow_check_tbl ALTER COLUMN city SET DATA TYPE INT8; -- error
+ALTER TABLE wholerow_check_tbl DROP COLUMN city; -- ok
+\d wholerow_check_tbl
+DROP TABLE wholerow_check_tbl;
+
 --
 -- Check inheritance of defaults and constraints
 --
diff --git a/src/test/regress/sql/foreign_data.sql b/src/test/regress/sql/foreign_data.sql
index ed78052b1e2..5f2eb0e3279 100644
--- a/src/test/regress/sql/foreign_data.sql
+++ b/src/test/regress/sql/foreign_data.sql
@@ -649,6 +649,16 @@ DROP TRIGGER trigtest_before_row ON foreign_schema.foreign_table_1;
 DROP TRIGGER trigtest_after_stmt ON foreign_schema.foreign_table_1;
 DROP TRIGGER trigtest_after_row ON foreign_schema.foreign_table_1;
 
+CREATE TRIGGER trigtest_before_stmt BEFORE INSERT OR UPDATE
+ON foreign_schema.foreign_table_1
+FOR EACH ROW
+WHEN (new IS NOT NULL)
+EXECUTE PROCEDURE dummy_trigger();
+
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 ALTER COLUMN c7 SET DATA TYPE bigint; -- error
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DROP COLUMN c7; -- error
+ALTER FOREIGN TABLE foreign_schema.foreign_table_1 DROP COLUMN c7 CASCADE; -- ok
+
 DROP FUNCTION dummy_trigger();
 
 -- Table inheritance
diff --git a/src/test/regress/sql/indexing.sql b/src/test/regress/sql/indexing.sql
index 561403cc7f7..e15d8a9b541 100644
--- a/src/test/regress/sql/indexing.sql
+++ b/src/test/regress/sql/indexing.sql
@@ -354,6 +354,23 @@ alter table idxpart2 drop column c;
 \d idxpart2
 drop table idxpart, idxpart2;
 
+-- Drop column should also drop all indexes that contains whole-row references
+-- Change column data type should fail if whole-row referenced indexes exists
+create table idxpart (a int, b int, c int) partition by range (a);
+create table idxpart1 partition of idxpart for values from (2000) to (3000);
+
+create index idxpart_idx1 on idxpart1((idxpart1 is not null));
+alter table idxpart alter column b set data type bigint; -- error
+drop index idxpart_idx1;
+
+create index idxpart_idx2 on idxpart1(a) where idxpart1 is not null;
+alter table idxpart alter column c set data type bigint; -- error
+alter table idxpart drop column c; -- ok
+
+\d idxpart
+\d idxpart1
+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/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 6b3566271df..4c49cceeb56 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -1219,6 +1219,23 @@ DROP POLICY p1 ON document;
 -- Just check everything went per plan
 SELECT * FROM document;
 
+-- check drop column (no CASCADE) or alter column data type will fail because of
+-- whole-row referenced security policy exists.
+DROP TABLE  part_document;
+ALTER TABLE document ADD COLUMN dummy INT4;
+CREATE POLICY p7 ON document AS PERMISSIVE
+    USING (cid IS NOT NULL AND
+    (WITH cte AS (SELECT TRUE FROM uaccount
+        WHERE EXISTS (SELECT document FROM uaccount WHERE uaccount IS NULL))
+    SELECT * FROM cte));
+ALTER TABLE uaccount ALTER COLUMN seclv SET DATA TYPE BIGINT; -- error
+ALTER TABLE document ALTER COLUMN dummy SET DATA TYPE BIGINT; -- error
+
+ALTER TABLE document DROP COLUMN dummy; -- error
+ALTER TABLE uaccount DROP COLUMN seclv; -- error
+ALTER TABLE uaccount DROP COLUMN seclv CASCADE; -- ok, policy p7 will be dropped
+ALTER TABLE document DROP COLUMN dummy CASCADE; -- ok
+
 --
 -- ROLE/GROUP
 --
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 2285e90110e..1f7f5e64c50 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -158,6 +158,23 @@ COMMENT ON TRIGGER no_such_trigger ON main_table IS 'wrong';
 COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS 'right';
 COMMENT ON TRIGGER before_ins_stmt_trig ON main_table IS NULL;
 
+--
+-- test triggers with WHEN clause contain wholerow reference
+--
+CREATE TABLE test_tbl1 (a int, b int) PARTITION BY RANGE (a);
+CREATE TABLE test_tbl1p1 PARTITION OF test_tbl1 FOR VALUES FROM (0) TO (1000);
+
+CREATE TRIGGER test_tbl1p1_trig
+  BEFORE INSERT OR UPDATE ON test_tbl1p1 FOR EACH ROW
+  WHEN (new = ROW (1, 1))
+  EXECUTE PROCEDURE trigger_func ('test_tbl1p1');
+
+ALTER TABLE test_tbl1 ALTER COLUMN b SET DATA TYPE bigint; -- error
+ALTER TABLE test_tbl1 DROP COLUMN b; -- error
+ALTER TABLE test_tbl1 DROP COLUMN b CASCADE; -- ok
+\d+ test_tbl1
+DROP TABLE test_tbl1;
+
 --
 -- test triggers with WHEN clause
 --
-- 
2.34.1

