On Mon, Sep 15, 2025 at 11:48 AM Chao Li <li.evan.c...@gmail.com> wrote:
>
> 3 - 0001
> ```
> + }
> + }
> + ReleaseSysCache(indexTuple);
> + }
> + CommandCounterIncrement();
> ```
>
> Why CommandCounterIncrement() is needed? In current code, there is a 
> CommandCounterIncrement() after CatalogTupleUpdate(), which is necessary. But 
> for your new code, maybe you considered “recordDependencyOn()” needs 
> CommandCounterIncrement(). I searched over all places when 
> “recordDependencyOn()” is called, I don’t see CommandCounterIncrement() is 
> called.
>
My thought is that CommandCounterIncrement may be needed;
because recordDependencyOn inserts many tuples to pg_depend,
then later performMultipleDeletions will interact with pg_depend.


>
> 5 - 0001
> ···
> + conscan = systable_beginscan(conDesc, ConstraintRelidTypidNameIndexId, true,
> + NULL, 3, skey);
> + if (!HeapTupleIsValid(contuple = systable_getnext(conscan)))
> + elog(ERROR, "constraint \"%s\" of relation \"%s\" does not exist",
> + constr_name, RelationGetRelationName(rel));
> ···
>
> Should we continue after elog()?
>
if "elog(ERROR," happens, then it will abort, so there is no need to
"continue", I think.

Summary of attached v4:
v4-0001: Handles ALTER TABLE DROP COLUMN when whole-row Vars are
referenced in check constraints and indexes.

v4-0002: Handles ALTER TABLE ALTER COLUMN SET DATA TYPE when whole-row
Vars are referenced in check constraints and indexes.

v4-0003: Handle ALTER TABLE ALTER COLUMN SET DATA TYPE and ALTER TABLE DROP
COLUMN when policy objects reference whole-row Vars.  Policy quals and check
quals may contain whole-row Vars and can include sublinks (unplanned
subqueries), pull_varattnos is not enough to locate whole-row Var. Instead,
obtain the whole-row type OID and recursively check each Var in expression node
to see if its vartype matches the whole-row type OID.
From 5d3ad72059977c6c3576c92e1ba25684f80b628d Mon Sep 17 00:00:00 2001
From: jian he <jian.universal...@gmail.com>
Date: Mon, 15 Sep 2025 20:19:29 +0800
Subject: [PATCH v4 3/3] disallow change or drop column when wholerow
 referenced policy exists

demo:
CREATE TABLE rls_tbl (a int, b int, c int);
CREATE POLICY p1 ON rls_tbl USING (rls_tbl >= ROW(1,1,1));

ALTER TABLE rls_tbl DROP COLUMN b; --error
ERROR:  cannot drop column b of table rls_tbl because other objects depend on it
DETAIL:  policy p1 on table rls_tbl depends on column b of table rls_tbl
HINT:  Use DROP ... CASCADE to drop the dependent objects too.

ALTER TABLE rls_tbl ALTER COLUMN b SET DATA TYPE BIGINT; --error
ERROR:  cannot alter table "rls_tbl" because security policy "p1" uses its row type
HINT:  You might need to drop policy "p1" first

ALTER TABLE rls_tbl DROP COLUMN b CASCADE; --ok
NOTICE:  drop cascades to policy p1 on table rls_tbl
ALTER TABLE

discussion: https://postgr.es/m/cacjufxga6kvqy7dbhglvw9s9kkmpgyzt5me6c7kefjdpr2w...@mail.gmail.com
---
 src/backend/commands/tablecmds.c          | 91 ++++++++++++++++++++++-
 src/backend/optimizer/util/var.c          | 60 +++++++++++++++
 src/include/optimizer/optimizer.h         |  1 +
 src/test/regress/expected/rowsecurity.out | 17 +++++
 src/test/regress/sql/rowsecurity.sql      | 12 +++
 src/tools/pgindent/typedefs.list          |  1 +
 6 files changed, 181 insertions(+), 1 deletion(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4949d7204a0..403b8a1adb8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -22096,7 +22096,7 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 }
 
 /*
- * Record dependencies between whole-row objects (indexes, constraints)
+ * Record dependencies between whole-row objects (indexes, constraints or policies)
  * associated with relation and relation's ObjectAddress.
  * error_out means can not install such dependency, we have to error out explicitly.
  */
@@ -22107,9 +22107,15 @@ recordWholeRowDependencyOnOrError(Relation rel, const ObjectAddress *object, boo
 	List	   *indexlist = NIL;
 	ObjectAddress con_obj;
 	ObjectAddress idx_obj;
+	ObjectAddress pol_obj;
 	bool		find_wholerow = false;
 
 	TupleConstr *constr = RelationGetDescr(rel)->constr;
+	Relation	pg_policy;
+	ScanKeyData skey[1];
+	SysScanDesc sscan;
+	HeapTuple	policy_tuple;
+	Oid			reltypid;
 
 	if (constr && constr->num_check > 0)
 	{
@@ -22265,4 +22271,87 @@ recordWholeRowDependencyOnOrError(Relation rel, const ObjectAddress *object, boo
 		}
 		ReleaseSysCache(indexTuple);
 	}
+
+	find_wholerow = false;
+	reltypid = get_rel_type_id(RelationGetRelid(rel));
+
+	pg_policy = table_open(PolicyRelationId, AccessShareLock);
+	ScanKeyInit(&skey[0],
+				Anum_pg_policy_polrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(rel)));
+	sscan = systable_beginscan(pg_policy,
+							   PolicyPolrelidPolnameIndexId, true, NULL, 1,
+							   skey);
+	while (HeapTupleIsValid(policy_tuple = systable_getnext(sscan)))
+	{
+		Datum		datum;
+		bool		isnull;
+		char	   *str_value;
+		Node		*polexpr;
+
+		Form_pg_policy policy = (Form_pg_policy) GETSTRUCT(policy_tuple);
+
+		/* Get policy qual */
+		datum = heap_getattr(policy_tuple, Anum_pg_policy_polqual,
+							 RelationGetDescr(pg_policy), &isnull);
+		if (!isnull)
+		{
+			str_value = TextDatumGetCString(datum);
+			polexpr = (Node *) stringToNode(str_value);
+			pfree(str_value);
+
+			find_wholerow = ExprContainWholeRow(polexpr, reltypid);
+			if (find_wholerow && !error_out)
+			{
+				pol_obj.classId = PolicyRelationId;
+				pol_obj.objectId = policy->oid;
+				pol_obj.objectSubId = 0;
+
+				/* record dependency for policies that references whole-row Var */
+				recordDependencyOn(&pol_obj, object, DEPENDENCY_NORMAL);
+				continue;
+			}
+
+			if (find_wholerow && error_out)
+				ereport(ERROR,
+						errcode(ERRCODE_DATATYPE_MISMATCH),
+						errmsg("cannot alter table \"%s\" because security policy \"%s\" uses its row type",
+								RelationGetRelationName(rel),
+								NameStr(policy->polname)),
+						errhint("You might need to drop policy \"%s\" first",
+								NameStr(policy->polname)));
+		}
+
+		datum = heap_getattr(policy_tuple, Anum_pg_policy_polwithcheck,
+							 RelationGetDescr(pg_policy), &isnull);
+		if (!isnull)
+		{
+			str_value = TextDatumGetCString(datum);
+			polexpr = (Node *) stringToNode(str_value);
+			pfree(str_value);
+
+			find_wholerow = ExprContainWholeRow(polexpr, reltypid);
+			if (find_wholerow && !error_out)
+			{
+				pol_obj.classId = PolicyRelationId;
+				pol_obj.objectId = policy->oid;
+				pol_obj.objectSubId = 0;
+
+				/* record dependency for policies that references whole-row Var */
+				recordDependencyOn(&pol_obj, object, DEPENDENCY_NORMAL);
+			}
+
+			if (find_wholerow && error_out)
+				ereport(ERROR,
+						errcode(ERRCODE_DATATYPE_MISMATCH),
+						errmsg("cannot alter table \"%s\" because security policy \"%s\" uses its row type",
+								RelationGetRelationName(rel),
+								NameStr(policy->polname)),
+						errhint("You might need to drop policy \"%s\" first",
+								NameStr(policy->polname)));
+		}
+	}
+	systable_endscan(sscan);
+	table_close(pg_policy, AccessShareLock);
 }
diff --git a/src/backend/optimizer/util/var.c b/src/backend/optimizer/util/var.c
index 8065237a189..36de0a3bfca 100644
--- a/src/backend/optimizer/util/var.c
+++ b/src/backend/optimizer/util/var.c
@@ -48,6 +48,10 @@ typedef struct
 	List	   *vars;
 	int			sublevels_up;
 } pull_vars_context;
+typedef struct
+{
+	Oid			reltypid;
+} contain_wholerow_context;
 
 typedef struct
 {
@@ -73,6 +77,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 ExprContainWholeRow_walker(Node *node, contain_wholerow_context *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 +332,61 @@ pull_varattnos_walker(Node *node, pull_varattnos_context *context)
 	return expression_tree_walker(node, pull_varattnos_walker, context);
 }
 
+static bool
+ExprContainWholeRow_walker(Node *node, contain_wholerow_context *context)
+{
+	if (node == NULL)
+		return false;
+
+	if (IsA(node, Var))
+	{
+		Var		   *var = (Var *) node;
+
+		if (var->varattno == InvalidAttrNumber &&
+			var->vartype == context->reltypid)
+			return true;
+
+		return false;
+	}
+
+	if (IsA(node, Query))
+	{
+		/* Recurse into RTE subquery or not-yet-planned sublink subquery */
+		bool		result;
+
+		result = query_tree_walker((Query *) node, ExprContainWholeRow_walker,
+								   context, 0);
+		return result;
+	}
+
+	return expression_tree_walker(node, ExprContainWholeRow_walker, context);
+}
+
+
+/*
+ * ExprContainWholeRow -
+ *
+ * Determine whether an expression contains a whole-row Var, recursing as needed.
+ * For simple expressions without sublinks, pull_varattnos is usually sufficient
+ * to detect a whole-row Var. If the node contains sublinks (unplanned subqueries),
+ * the check must instead rely on the whole-row type OID. Therefore, reltypid is
+ * used consistently to determine the presence of a whole-row Var.
+ */
+bool
+ExprContainWholeRow(Node *node, Oid reltypid)
+{
+	contain_wholerow_context context;
+
+	context.reltypid = reltypid;
+
+	Assert(OidIsValid(reltypid));
+
+	return query_or_expression_tree_walker(node,
+										   ExprContainWholeRow_walker,
+										   &context,
+										   0);
+}
+
 
 /*
  * pull_vars_of_level
diff --git a/src/include/optimizer/optimizer.h b/src/include/optimizer/optimizer.h
index 37bc13c2cbd..d31d1c49c75 100644
--- a/src/include/optimizer/optimizer.h
+++ b/src/include/optimizer/optimizer.h
@@ -205,6 +205,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 ExprContainWholeRow(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/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index 8c879509313..b2b5333f8dc 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -2357,6 +2357,23 @@ SELECT * FROM document;
   14 |  11 |      1 | regress_rls_bob   | new novel                        | 
 (16 rows)
 
+--check drop column restrict or alter column data type will fail because of whole-row
+--referenced security policy exists.
+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 document ALTER COLUMN dummy SET DATA TYPE BIGINT; --error
+ERROR:  cannot alter table "document" because security policy "p7" uses its row type
+HINT:  You might need to drop policy "p7" 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 document DROP COLUMN dummy CASCADE; --ok
+NOTICE:  drop cascades to policy p7 on table document
 --
 -- ROLE/GROUP
 --
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index 21ac0ca51ee..0cdd5a82ca3 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -1021,6 +1021,18 @@ DROP POLICY p1 ON document;
 -- Just check everything went per plan
 SELECT * FROM document;
 
+--check drop column restrict or alter column data type will fail because of whole-row
+--referenced security policy exists.
+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 document ALTER COLUMN dummy SET DATA TYPE BIGINT; --error
+ALTER TABLE document DROP COLUMN dummy; --error
+ALTER TABLE document DROP COLUMN dummy CASCADE; --ok
+
 --
 -- ROLE/GROUP
 --
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a13e8162890..81b97cc622e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -3521,6 +3521,7 @@ conn_oauth_scope_func
 conn_sasl_state_func
 contain_aggs_of_level_context
 contain_placeholder_references_context
+contain_wholerow_context
 convert_testexpr_context
 copy_data_dest_cb
 copy_data_source_cb
-- 
2.34.1

From edf09b4d7da937d946f5c6ef6ca6806019d37fe0 Mon Sep 17 00:00:00 2001
From: jian he <jian.universal...@gmail.com>
Date: Mon, 15 Sep 2025 15:32:49 +0800
Subject: [PATCH v4 1/3] ALTER TABLE DROP COLUMN drop wholerow referenced
 object

CREATE TABLE ts (a int, c int, b int constraint cc check((ts = ROW(1,1,1))));
CREATE INDEX tsi3 on ts ((ts is null));
CREATE INDEX tsi4 on ts (b) where ts is not null;

ALTER TABLE ts DROP COLUMN a CASCADE;
will drop above all indexes, constraints on the table ts.

now
\d ts
                 Table "public.ts"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 c      | integer |           |          |
 b      | integer |           |          |

discussion: https://postgr.es/m/cacjufxga6kvqy7dbhglvw9s9kkmpgyzt5me6c7kefjdpr2w...@mail.gmail.com
---
 src/backend/commands/tablecmds.c          | 170 +++++++++++++++++++++-
 src/test/regress/expected/constraints.out |  17 +++
 src/test/regress/expected/indexing.out    |  13 ++
 src/test/regress/sql/constraints.sql      |  11 ++
 src/test/regress/sql/indexing.sql         |   8 +
 5 files changed, 216 insertions(+), 3 deletions(-)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 3be2e051d32..7f5e58da62f 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -740,6 +740,7 @@ static List *GetParentedForeignKeyRefs(Relation partition);
 static void ATDetachCheckNoForeignKeyRefs(Relation partition);
 static char GetAttributeCompression(Oid atttypid, const char *compression);
 static char GetAttributeStorage(Oid atttypid, const char *storagemode);
+static void recordWholeRowDependencyOnOrError(Relation rel, const ObjectAddress *object, bool error_out);
 
 
 /* ----------------------------------------------------------------
@@ -9333,6 +9334,26 @@ ATExecDropColumn(List **wqueue, Relation rel, const char *colName,
 
 	ReleaseSysCache(tuple);
 
+	object.classId = RelationRelationId;
+	object.objectId = RelationGetRelid(rel);
+	object.objectSubId = attnum;
+
+	/*
+	 * ALTER TABLE DROP COLUMN must also remove indexes or constraints that
+	 * contain whole-row Var reference expressions. Since there is no direct
+	 * dependency recorded between whole-row Vars and individual columns, and
+	 * creating such dependencies would cause catalog bloat (see
+	 * find_expr_references_walker).
+	 *
+	 * Here we handle this explicitly. We call 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.
+	*/
+	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
@@ -9426,9 +9447,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)
@@ -22062,3 +22080,149 @@ GetAttributeStorage(Oid atttypid, const char *storagemode)
 
 	return cstorage;
 }
+
+/*
+ * Record dependencies between whole-row objects (indexes, constraints)
+ * associated with relation and relation's ObjectAddress.
+ * error_out means can not install such dependency, we have to error out explicitly.
+ */
+static void
+recordWholeRowDependencyOnOrError(Relation rel, const ObjectAddress *object, bool error_out)
+{
+	Node	   *expr;
+	List	   *indexlist = NIL;
+	ObjectAddress con_obj;
+	ObjectAddress idx_obj;
+	bool		find_wholerow = false;
+
+	TupleConstr *constr = RelationGetDescr(rel)->constr;
+
+	if (constr && constr->num_check > 0)
+	{
+		ConstrCheck *check = constr->check;
+
+		for (int i = 0; i < constr->num_check; i++)
+		{
+			Bitmapset  *expr_attrs = NULL;
+			char	   *constr_name = check[i].ccname;
+
+			expr = stringToNode(check[i].ccbin);
+
+			/* Find all attributes referenced */
+			pull_varattnos(expr, 1, &expr_attrs);
+
+			find_wholerow = bms_is_member(0 - FirstLowInvalidHeapAttributeNumber,
+										  expr_attrs);
+			if (find_wholerow && !error_out)
+			{
+				Relation	conDesc;
+				SysScanDesc conscan;
+				ScanKeyData skey[3];
+				HeapTuple	contuple;
+
+				/* Search for a pg_constraint entry with same name and relation */
+				conDesc = table_open(ConstraintRelationId, AccessShareLock);
+
+				ScanKeyInit(&skey[0],
+							Anum_pg_constraint_conrelid,
+							BTEqualStrategyNumber, F_OIDEQ,
+							ObjectIdGetDatum(RelationGetRelid(rel)));
+				ScanKeyInit(&skey[1],
+							Anum_pg_constraint_contypid,
+							BTEqualStrategyNumber, F_OIDEQ,
+							ObjectIdGetDatum(InvalidOid));
+				ScanKeyInit(&skey[2],
+							Anum_pg_constraint_conname,
+							BTEqualStrategyNumber, F_NAMEEQ,
+							CStringGetDatum(constr_name));
+
+				conscan = systable_beginscan(conDesc, ConstraintRelidTypidNameIndexId, true,
+											 NULL, 3, skey);
+				if (!HeapTupleIsValid(contuple = systable_getnext(conscan)))
+					elog(ERROR, "constraint \"%s\" of relation \"%s\" does not exist",
+						 constr_name, RelationGetRelationName(rel));
+
+				con_obj.classId = ConstraintRelationId;
+				con_obj.objectId = ((Form_pg_constraint) GETSTRUCT(contuple))->oid;
+				con_obj.objectSubId = 0;
+
+				/* record dependency for constraints that references whole-row */
+				recordDependencyOn(&con_obj, object, DEPENDENCY_AUTO);
+
+				systable_endscan(conscan);
+				table_close(conDesc, AccessShareLock);
+			}
+		}
+	}
+
+	find_wholerow = false;
+	indexlist = RelationGetIndexList(rel);
+	foreach_oid(indexoid, indexlist)
+	{
+		HeapTuple	indexTuple;
+		Form_pg_index indexStruct;
+		Node	   *node;
+
+		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_indpred, NULL))
+		{
+			Datum		predDatum;
+			char	   *predString;
+			Bitmapset  *expr_attrs = NULL;
+
+			/* Convert text string to node tree */
+			predDatum = SysCacheGetAttrNotNull(INDEXRELID, indexTuple,
+											   Anum_pg_index_indpred);
+			predString = TextDatumGetCString(predDatum);
+			node = (Node *) stringToNode(predString);
+			pfree(predString);
+
+			pull_varattnos(node, 1, &expr_attrs);
+			find_wholerow = bms_is_member(0 - FirstLowInvalidHeapAttributeNumber,
+										  expr_attrs);
+			if (find_wholerow && !error_out)
+			{
+				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_indexprs, NULL))
+		{
+			Datum		exprDatum;
+			char	   *exprString;
+			Bitmapset  *expr_attrs = NULL;
+
+			/* Convert text string to node tree */
+			exprDatum = SysCacheGetAttrNotNull(INDEXRELID, indexTuple,
+											   Anum_pg_index_indexprs);
+			exprString = TextDatumGetCString(exprDatum);
+			node = (Node *) stringToNode(exprString);
+			pfree(exprString);
+
+			pull_varattnos(node, 1, &expr_attrs);
+
+			find_wholerow = bms_is_member(0 - FirstLowInvalidHeapAttributeNumber,
+										  expr_attrs);
+			if (find_wholerow && !error_out)
+			{
+				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);
+	}
+}
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 3590d3274f0..8930b4da7ce 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -254,6 +254,23 @@ 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, is_capital bool, altitude int,
+    CONSTRAINT cc CHECK (city is not null),
+    CONSTRAINT cc1 CHECK (DROP_COL_CHECK_TBL is not null));
+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    |           |          | 
+ is_capital | boolean |           |          | 
+ altitude   | integer |           |          | 
+
+DROP TABLE DROP_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..cac1cca3a1f 100644
--- a/src/test/regress/expected/indexing.out
+++ b/src/test/regress/expected/indexing.out
@@ -654,6 +654,19 @@ 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 on idxpart((idxpart is not null));
+create index 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 |           |          | 
+
+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 1f6dc8fd69f..676d8736101 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -165,6 +165,17 @@ 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, is_capital bool, altitude int,
+    CONSTRAINT cc CHECK (city is not null),
+    CONSTRAINT cc1 CHECK (DROP_COL_CHECK_TBL is not null));
+ALTER TABLE DROP_COL_CHECK_TBL DROP COLUMN city;
+\d DROP_COL_CHECK_TBL
+DROP TABLE DROP_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..2bad9555112 100644
--- a/src/test/regress/sql/indexing.sql
+++ b/src/test/regress/sql/indexing.sql
@@ -295,6 +295,14 @@ 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 on idxpart((idxpart is not null));
+create index on idxpart(a) where idxpart is not null;
+alter table idxpart drop column c;
+\d idxpart
+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

From 07ac16212729fb71b0f3fa8557688e0788477625 Mon Sep 17 00:00:00 2001
From: jian he <jian.universal...@gmail.com>
Date: Mon, 15 Sep 2025 15:53:59 +0800
Subject: [PATCH v4 2/3] disallow ALTER COLUMN SET DATA TYPE when wholerow
 referenced constraint exists

CREATE TABLE ts (a int, c int, b int  constraint cc check((ts = ROW(1,1,1))));
INSERT INTO ts values (1,1,1);

ALTER TABLE ts ALTER COLUMN b SET DATA TYPE INT8;
ERROR:  cannot alter table "ts" because constraint "cc" uses its row type
HINT:  You might need to drop constraint "cc" first

create index on ts((ts is not null));
alter table ts alter column a set data type int8; --error
ERROR:  cannot alter table "ts" because index "ts_expr_idx" uses its row type
HINT:  You might need to drop index "ts_expr_idx" first

discussion: https://postgr.es/m/cacjufxga6kvqy7dbhglvw9s9kkmpgyzt5me6c7kefjdpr2w...@mail.gmail.com
---
 src/backend/commands/tablecmds.c          | 40 +++++++++++++++++++++++
 src/test/regress/expected/constraints.out | 11 +++++++
 src/test/regress/expected/indexing.out    |  6 ++++
 src/test/regress/sql/constraints.sql      | 10 ++++++
 src/test/regress/sql/indexing.sql         |  2 ++
 5 files changed, 69 insertions(+)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7f5e58da62f..4949d7204a0 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -14541,6 +14541,20 @@ ATPrepAlterColumnType(List **wqueue,
 		find_composite_type_dependencies(rel->rd_rel->reltype, rel, NULL);
 	}
 
+	/*
+	 * If the table has a whole-row referenced CHECK constraint, then changing
+	 * any column data type is not allowed.
+	*/
+	if (targettype != attTup->atttypid || targettypmod != attTup->atttypmod)
+	{
+		ObjectAddress object;
+
+		object.classId = RelationRelationId;
+		object.objectId = RelationGetRelid(rel);
+		object.objectSubId = attnum;
+		recordWholeRowDependencyOnOrError(rel, &object, true);
+	}
+
 	ReleaseSysCache(tuple);
 
 	/*
@@ -22152,6 +22166,14 @@ recordWholeRowDependencyOnOrError(Relation rel, const ObjectAddress *object, boo
 				systable_endscan(conscan);
 				table_close(conDesc, AccessShareLock);
 			}
+			if (find_wholerow && error_out)
+				ereport(ERROR,
+						errcode(ERRCODE_DATATYPE_MISMATCH),
+						errmsg("cannot alter table \"%s\" because constraint \"%s\" uses its row type",
+								RelationGetRelationName(rel),
+								constr_name),
+						errhint("You might need to drop constraint \"%s\" first",
+								constr_name));
 		}
 	}
 
@@ -22195,6 +22217,15 @@ recordWholeRowDependencyOnOrError(Relation rel, const ObjectAddress *object, boo
 				ReleaseSysCache(indexTuple);
 				continue;
 			}
+
+			if (find_wholerow && 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)));
 		}
 
 		if (!heap_attisnull(indexTuple, Anum_pg_index_indexprs, NULL))
@@ -22222,6 +22253,15 @@ recordWholeRowDependencyOnOrError(Relation rel, const ObjectAddress *object, boo
 				/* record dependency for indexes that references whole-row */
 				recordDependencyOn(&idx_obj, object, DEPENDENCY_AUTO);
 			}
+
+			if (find_wholerow && 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)));
 		}
 		ReleaseSysCache(indexTuple);
 	}
diff --git a/src/test/regress/expected/constraints.out b/src/test/regress/expected/constraints.out
index 8930b4da7ce..4527016f319 100644
--- a/src/test/regress/expected/constraints.out
+++ b/src/test/regress/expected/constraints.out
@@ -271,6 +271,17 @@ ALTER TABLE DROP_COL_CHECK_TBL DROP COLUMN city;
 
 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, is_capital bool, altitude int,
+    CONSTRAINT cc1 CHECK (ALTER_COL_CHECK_TBL is not null));
+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 cac1cca3a1f..e544141ad9c 100644
--- a/src/test/regress/expected/indexing.out
+++ b/src/test/regress/expected/indexing.out
@@ -657,7 +657,13 @@ drop table idxpart, idxpart2;
 create table idxpart (a int, b int, c int);
 create index on idxpart(c);
 create index on idxpart((idxpart is not null));
+alter table idxpart alter column a set data type int8; --error
+ERROR:  cannot alter table "idxpart" because index "idxpart_expr_idx" uses its row type
+HINT:  You might need to drop index "idxpart_expr_idx" first
 create index 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_expr_idx" uses its row type
+HINT:  You might need to drop index "idxpart_expr_idx" first
 alter table idxpart drop column c;
 \d idxpart
               Table "public.idxpart"
diff --git a/src/test/regress/sql/constraints.sql b/src/test/regress/sql/constraints.sql
index 676d8736101..bbe8328fbf9 100644
--- a/src/test/regress/sql/constraints.sql
+++ b/src/test/regress/sql/constraints.sql
@@ -176,6 +176,16 @@ 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, is_capital bool, altitude int,
+    CONSTRAINT cc1 CHECK (ALTER_COL_CHECK_TBL is not null));
+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 2bad9555112..0711860443e 100644
--- a/src/test/regress/sql/indexing.sql
+++ b/src/test/regress/sql/indexing.sql
@@ -298,7 +298,9 @@ drop table idxpart, idxpart2;
 create table idxpart (a int, b int, c int);
 create index on idxpart(c);
 create index on idxpart((idxpart is not null));
+alter table idxpart alter column a set data type int8; --error
 create index on idxpart(a) where idxpart is not null;
+alter table idxpart alter column a set data type int8; --error
 alter table idxpart drop column c;
 \d idxpart
 drop table idxpart;
-- 
2.34.1

Reply via email to