From 10760e35806f8ac5608e246eb8d0b4baa5779f4f Mon Sep 17 00:00:00 2001
From: jian he <jian.universality@gmail.com>
Date: Mon, 25 May 2026 14:19:45 +0800
Subject: [PATCH v9 2/3] fix DDL wholerow referenced triggers

ALTER TABLE DROP COLUMN should fail if any trigger's WHEN clause contains a
whole-row reference. To do this, we record a dependency between the
relation and the dependent trigger in RememberWholeRowDependentForRebuilding.
Later, performMultipleDeletions handles the deletion logic.

ALTER COLUMN SET DATA TYPE fundamentally changes the table's row type.  Rows
with different column data types cannot be compared (see record_eq). Therefore,
we must error out otherwise, any trigger WHEN clause contains whole-row
references may constantly error.

discussion: https://postgr.es/m/CACJufxGA6KVQy7DbHGLVw9s9KKmpGyZt5ME6C7kEfjDpr2wZCw@mail.gmail.com
commitfest: https://commitfest.postgresql.org/patch/6055
---
 src/backend/commands/tablecmds.c           | 38 ++++++++++++++++++++++
 src/test/regress/expected/foreign_data.out | 14 ++++++++
 src/test/regress/expected/triggers.out     | 28 ++++++++++++++++
 src/test/regress/sql/foreign_data.sql      | 10 ++++++
 src/test/regress/sql/triggers.sql          | 17 ++++++++++
 5 files changed, 107 insertions(+)

diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 4aa582511bc..bd8e29de1b1 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -24156,4 +24156,42 @@ RememberWholeRowDependentForRebuilding(AlteredTableInfo *tab, AlterTableType sub
 	}
 	systable_endscan(indscan);
 	table_close(pg_index, AccessShareLock);
+
+	/* Now checking trigger whole-row references */
+	if (rel->trigdesc != NULL)
+	{
+		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 trig_obj;
+
+				ObjectAddressSet(trig_obj, TriggerRelationId, trig->tgoid);
+
+				/*
+				 * The dependency between the trigger and its relation is
+				 * DEPENDENCY_NORMAL
+				 */
+				RememberWholeRowDependent(tab, subtype, rel, attnum,
+										  &trig_obj, DEPENDENCY_NORMAL);
+			}
+		}
+	}
 }
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/triggers.out b/src/test/regress/expected/triggers.out
index 8fcb33ac81a..e10a43e5f3f 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/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/triggers.sql b/src/test/regress/sql/triggers.sql
index 2285e90110e..1823c2cfb8d 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

