hi.

demo:
CREATE TABLE main_table (a int, b int);
CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS '
BEGIN
    RAISE NOTICE ''trigger_func(%) called: action = %, when = %, level = %'',
                  TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
    RETURN NULL;
END;';
CREATE TRIGGER modified_a BEFORE UPDATE OF a ON main_table
FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE trigger_func('modified_a');

ALTER TABLE main_table ALTER COLUMN a SET DATA TYPE INT8;
ERROR:  cannot alter type of a column used in a trigger definition
DETAIL:  trigger modified_a on table main_table depends on column "a"

With the attached patch, the previous error will no longer occur.
Foreign key–related internal triggers are not directly dependent on the relation
itself; instead, they depend directly on the constraint.
Therefore, we don't need to worry about internal triggers in this context.

v1-0001: "refactor CreateTrigger and CreateTriggerFiringOn".
used also in 
https://postgr.es/m/cacjufxhjar2fjbeb6ghg_-n5dxx5jvnjkslouxoyt4teaaw...@mail.gmail.com
v1-0002, the actual implementation.
From 41337dcde4b5a76eb224aff12e006fc430b9e5e1 Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
Date: Thu, 25 Sep 2025 16:41:36 +0800
Subject: [PATCH v1 1/2] refactor CreateTrigger and CreateTriggerFiringOn

discussion: https://postgr.es/m/
---
 src/backend/catalog/index.c        |   3 +-
 src/backend/commands/tablecmds.c   |  14 ++-
 src/backend/commands/trigger.c     | 180 ++++++++---------------------
 src/backend/parser/gram.y          |   2 +
 src/backend/parser/parse_utilcmd.c | 144 +++++++++++++++++++++++
 src/backend/tcop/utility.c         |   2 +-
 src/include/commands/trigger.h     |   4 +-
 src/include/nodes/parsenodes.h     |   1 +
 src/include/parser/parse_utilcmd.h |   2 +
 9 files changed, 212 insertions(+), 140 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 5d9db167e59..8d09d61cae2 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2039,10 +2039,11 @@ index_constraint_create(Relation heapRelation,
 		trigger->deferrable = true;
 		trigger->initdeferred = initdeferred;
 		trigger->constrrel = NULL;
+		trigger->transformed = true;
 
 		(void) CreateTrigger(trigger, NULL, RelationGetRelid(heapRelation),
 							 InvalidOid, conOid, indexRelationId, InvalidOid,
-							 InvalidOid, NULL, true, false);
+							 InvalidOid, true, false);
 	}
 
 	/*
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index fc89352b661..bf8120c12f2 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -13802,10 +13802,11 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 	fk_trigger->deferrable = fkconstraint->deferrable;
 	fk_trigger->initdeferred = fkconstraint->initdeferred;
 	fk_trigger->constrrel = NULL;
+	fk_trigger->transformed = true;
 
 	trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
 								constraintOid, indexOid, InvalidOid,
-								parentTrigOid, NULL, true, false);
+								parentTrigOid, true, false);
 
 	/* Make changes-so-far visible */
 	CommandCounterIncrement();
@@ -13847,6 +13848,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
 	fk_trigger->whenClause = NULL;
 	fk_trigger->transitionRels = NIL;
 	fk_trigger->constrrel = NULL;
+	fk_trigger->transformed = true;
 
 	switch (fkconstraint->fk_del_action)
 	{
@@ -13883,7 +13885,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
 
 	trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
 								constraintOid, indexOid, InvalidOid,
-								parentDelTrigger, NULL, true, false);
+								parentDelTrigger, true, false);
 	if (deleteTrigOid)
 		*deleteTrigOid = trigAddress.objectId;
 
@@ -13907,6 +13909,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
 	fk_trigger->whenClause = NULL;
 	fk_trigger->transitionRels = NIL;
 	fk_trigger->constrrel = NULL;
+	fk_trigger->transformed = true;
 
 	switch (fkconstraint->fk_upd_action)
 	{
@@ -13943,7 +13946,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
 
 	trigAddress = CreateTrigger(fk_trigger, NULL, refRelOid, myRelOid,
 								constraintOid, indexOid, InvalidOid,
-								parentUpdTrigger, NULL, true, false);
+								parentUpdTrigger, true, false);
 	if (updateTrigOid)
 		*updateTrigOid = trigAddress.objectId;
 }
@@ -20823,15 +20826,16 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
 		trigStmt->timing = trigForm->tgtype & TRIGGER_TYPE_TIMING_MASK;
 		trigStmt->events = trigForm->tgtype & TRIGGER_TYPE_EVENT_MASK;
 		trigStmt->columns = cols;
-		trigStmt->whenClause = NULL;	/* passed separately */
+		trigStmt->whenClause = qual;
 		trigStmt->transitionRels = NIL; /* not supported at present */
 		trigStmt->deferrable = trigForm->tgdeferrable;
 		trigStmt->initdeferred = trigForm->tginitdeferred;
 		trigStmt->constrrel = NULL; /* passed separately */
+		trigStmt->transformed = true; /* whenClause alerady transformed */
 
 		CreateTriggerFiringOn(trigStmt, NULL, RelationGetRelid(partition),
 							  trigForm->tgconstrrelid, InvalidOid, InvalidOid,
-							  trigForm->tgfoid, trigForm->oid, qual,
+							  trigForm->tgfoid, trigForm->oid,
 							  false, true, trigForm->tgenabled);
 
 		MemoryContextSwitchTo(oldcxt);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 579ac8d76ae..f1431d99a56 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -40,6 +40,7 @@
 #include "parser/parse_collate.h"
 #include "parser/parse_func.h"
 #include "parser/parse_relation.h"
+#include "parser/parse_utilcmd.h"
 #include "partitioning/partdesc.h"
 #include "pgstat.h"
 #include "rewrite/rewriteHandler.h"
@@ -139,9 +140,6 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
  * create the trigger on partitions, 2) when creating child foreign key
  * triggers; see CreateFKCheckTrigger() and createForeignKeyActionTriggers().
  *
- * If whenClause is passed, it is an already-transformed expression for
- * WHEN.  In this case, we ignore any that may come in stmt->whenClause.
- *
  * If isInternal is true then this is an internally-generated trigger.
  * This argument sets the tgisinternal field of the pg_trigger entry, and
  * if true causes us to modify the given trigger name to ensure uniqueness.
@@ -159,13 +157,13 @@ static HeapTuple check_modified_virtual_generated(TupleDesc tupdesc, HeapTuple t
 ObjectAddress
 CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 			  Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
-			  Oid funcoid, Oid parentTriggerOid, Node *whenClause,
+			  Oid funcoid, Oid parentTriggerOid,
 			  bool isInternal, bool in_partition)
 {
 	return
 		CreateTriggerFiringOn(stmt, queryString, relOid, refRelOid,
 							  constraintOid, indexOid, funcoid,
-							  parentTriggerOid, whenClause, isInternal,
+							  parentTriggerOid, isInternal,
 							  in_partition, TRIGGER_FIRES_ON_ORIGIN);
 }
 
@@ -177,15 +175,15 @@ ObjectAddress
 CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 					  Oid relOid, Oid refRelOid, Oid constraintOid,
 					  Oid indexOid, Oid funcoid, Oid parentTriggerOid,
-					  Node *whenClause, bool isInternal, bool in_partition,
+					  bool isInternal, bool in_partition,
 					  char trigger_fires_when)
 {
 	int16		tgtype;
 	int			ncolumns;
 	int16	   *columns;
 	int2vector *tgattr;
-	List	   *whenRtable;
-	char	   *qual;
+	List	   *whenRtable = NIL;
+	char	   *qual = NULL;
 	Datum		values[Natts_pg_trigger];
 	bool		nulls[Natts_pg_trigger];
 	Relation	rel;
@@ -207,6 +205,7 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 	Oid			existing_constraint_oid = InvalidOid;
 	bool		existing_isInternal = false;
 	bool		existing_isClone = false;
+	Node 		*whenClause = NULL;
 
 	if (OidIsValid(relOid))
 		rel = table_open(relOid, ShareRowExclusiveLock);
@@ -557,133 +556,21 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 	/*
 	 * Parse the WHEN clause, if any and we weren't passed an already
 	 * transformed one.
-	 *
-	 * Note that as a side effect, we fill whenRtable when parsing.  If we got
-	 * an already parsed clause, this does not occur, which is what we want --
-	 * no point in adding redundant dependencies below.
 	 */
-	if (!whenClause && stmt->whenClause)
+	if (stmt->whenClause)
 	{
-		ParseState *pstate;
-		ParseNamespaceItem *nsitem;
-		List	   *varList;
-		ListCell   *lc;
-
-		/* Set up a pstate to parse with */
-		pstate = make_parsestate(NULL);
-		pstate->p_sourcetext = queryString;
-
-		/*
-		 * Set up nsitems for OLD and NEW references.
-		 *
-		 * 'OLD' must always have varno equal to 1 and 'NEW' equal to 2.
-		 */
-		nsitem = addRangeTableEntryForRelation(pstate, rel,
-											   AccessShareLock,
-											   makeAlias("old", NIL),
-											   false, false);
-		addNSItemToQuery(pstate, nsitem, false, true, true);
-		nsitem = addRangeTableEntryForRelation(pstate, rel,
-											   AccessShareLock,
-											   makeAlias("new", NIL),
-											   false, false);
-		addNSItemToQuery(pstate, nsitem, false, true, true);
-
-		/* Transform expression.  Copy to be sure we don't modify original */
-		whenClause = transformWhereClause(pstate,
-										  copyObject(stmt->whenClause),
-										  EXPR_KIND_TRIGGER_WHEN,
-										  "WHEN");
-		/* we have to fix its collations too */
-		assign_expr_collations(pstate, whenClause);
-
-		/*
-		 * Check for disallowed references to OLD/NEW.
-		 *
-		 * NB: pull_var_clause is okay here only because we don't allow
-		 * subselects in WHEN clauses; it would fail to examine the contents
-		 * of subselects.
-		 */
-		varList = pull_var_clause(whenClause, 0);
-		foreach(lc, varList)
+		if (!stmt->transformed)
 		{
-			Var		   *var = (Var *) lfirst(lc);
+			stmt = transformCreateTriggerStmt(RelationGetRelid(rel), stmt,
+											  queryString);
 
-			switch (var->varno)
-			{
-				case PRS2_OLD_VARNO:
-					if (!TRIGGER_FOR_ROW(tgtype))
-						ereport(ERROR,
-								(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-								 errmsg("statement trigger's WHEN condition cannot reference column values"),
-								 parser_errposition(pstate, var->location)));
-					if (TRIGGER_FOR_INSERT(tgtype))
-						ereport(ERROR,
-								(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-								 errmsg("INSERT trigger's WHEN condition cannot reference OLD values"),
-								 parser_errposition(pstate, var->location)));
-					/* system columns are okay here */
-					break;
-				case PRS2_NEW_VARNO:
-					if (!TRIGGER_FOR_ROW(tgtype))
-						ereport(ERROR,
-								(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-								 errmsg("statement trigger's WHEN condition cannot reference column values"),
-								 parser_errposition(pstate, var->location)));
-					if (TRIGGER_FOR_DELETE(tgtype))
-						ereport(ERROR,
-								(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-								 errmsg("DELETE trigger's WHEN condition cannot reference NEW values"),
-								 parser_errposition(pstate, var->location)));
-					if (var->varattno < 0 && TRIGGER_FOR_BEFORE(tgtype))
-						ereport(ERROR,
-								(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-								 errmsg("BEFORE trigger's WHEN condition cannot reference NEW system columns"),
-								 parser_errposition(pstate, var->location)));
-					if (TRIGGER_FOR_BEFORE(tgtype) &&
-						var->varattno == 0 &&
-						RelationGetDescr(rel)->constr &&
-						(RelationGetDescr(rel)->constr->has_generated_stored ||
-						 RelationGetDescr(rel)->constr->has_generated_virtual))
-						ereport(ERROR,
-								(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-								 errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
-								 errdetail("A whole-row reference is used and the table contains generated columns."),
-								 parser_errposition(pstate, var->location)));
-					if (TRIGGER_FOR_BEFORE(tgtype) &&
-						var->varattno > 0 &&
-						TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attgenerated)
-						ereport(ERROR,
-								(errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
-								 errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
-								 errdetail("Column \"%s\" is a generated column.",
-										   NameStr(TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attname)),
-								 parser_errposition(pstate, var->location)));
-					break;
-				default:
-					/* can't happen without add_missing_from, so just elog */
-					elog(ERROR, "trigger WHEN condition cannot contain references to other relations");
-					break;
-			}
+			whenClause = stmt->whenClause;
+			Assert(whenClause != NULL);
 		}
+		else
+			whenClause = stmt->whenClause;
 
-		/* we'll need the rtable for recordDependencyOnExpr */
-		whenRtable = pstate->p_rtable;
-
-		qual = nodeToString(whenClause);
-
-		free_parsestate(pstate);
-	}
-	else if (!whenClause)
-	{
-		whenClause = NULL;
-		whenRtable = NIL;
-		qual = NULL;
-	}
-	else
-	{
 		qual = nodeToString(whenClause);
-		whenRtable = NIL;
 	}
 
 	/*
@@ -1129,10 +1016,40 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 	 * If it has a WHEN clause, add dependencies on objects mentioned in the
 	 * expression (eg, functions, as well as any columns used).
 	 */
-	if (whenRtable != NIL)
+	if (whenClause != NULL)
+	{
+		ParseState *pstate;
+		ParseNamespaceItem *nsitem;
+
+		/* Set up a pstate to parse with */
+		pstate = make_parsestate(NULL);
+		pstate->p_sourcetext = queryString;
+
+		/*
+		 * Set up nsitems for OLD and NEW references.
+		 *
+		 * 'OLD' must always have varno equal to 1 and 'NEW' equal to 2.
+		 */
+		nsitem = addRangeTableEntryForRelation(pstate, rel,
+											   AccessShareLock,
+											   makeAlias("old", NIL),
+											   false, false);
+		addNSItemToQuery(pstate, nsitem, false, true, true);
+
+		nsitem = addRangeTableEntryForRelation(pstate, rel,
+											   AccessShareLock,
+											   makeAlias("new", NIL),
+											   false, false);
+		addNSItemToQuery(pstate, nsitem, false, true, true);
+
+		/* we'll need the rtable for recordDependencyOnExpr */
+		whenRtable = pstate->p_rtable;
 		recordDependencyOnExpr(&myself, whenClause, whenRtable,
 							   DEPENDENCY_NORMAL);
 
+		free_parsestate(pstate);
+	}
+
 	/* Post creation hook for new trigger */
 	InvokeObjectPostCreateHookArg(TriggerRelationId, trigoid, 0,
 								  isInternal);
@@ -1175,7 +1092,6 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 			 */
 			childStmt = copyObject(stmt);
 			childStmt->funcname = NIL;
-			childStmt->whenClause = NULL;
 
 			/* If there is a WHEN clause, create a modified copy of it */
 			qual = copyObject(whenClause);
@@ -1185,11 +1101,13 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 			qual = (Node *)
 				map_partition_varattnos((List *) qual, PRS2_NEW_VARNO,
 										childTbl, rel);
+			childStmt->whenClause = qual;
+			childStmt->transformed = true;
 
 			CreateTriggerFiringOn(childStmt, queryString,
 								  partdesc->oids[i], refRelOid,
 								  InvalidOid, InvalidOid,
-								  funcoid, trigoid, qual,
+								  funcoid, trigoid,
 								  isInternal, true, trigger_fires_when);
 
 			table_close(childTbl, NoLock);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 9fd48acb1f8..05ff0c5973f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -6070,6 +6070,7 @@ CreateTrigStmt:
 					n->deferrable = false;
 					n->initdeferred = false;
 					n->constrrel = NULL;
+					n->transformed = false;
 					$$ = (Node *) n;
 				}
 		  | CREATE opt_or_replace CONSTRAINT TRIGGER name AFTER TriggerEvents ON
@@ -6120,6 +6121,7 @@ CreateTrigStmt:
 								   &n->deferrable, &n->initdeferred, &dummy,
 								   NULL, NULL, yyscanner);
 					n->constrrel = $10;
+					n->transformed = false;
 					$$ = (Node *) n;
 				}
 		;
diff --git a/src/backend/parser/parse_utilcmd.c b/src/backend/parser/parse_utilcmd.c
index e96b38a59d5..fd6d5c922ec 100644
--- a/src/backend/parser/parse_utilcmd.c
+++ b/src/backend/parser/parse_utilcmd.c
@@ -38,6 +38,7 @@
 #include "catalog/pg_opclass.h"
 #include "catalog/pg_operator.h"
 #include "catalog/pg_statistic_ext.h"
+#include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
 #include "commands/comment.h"
 #include "commands/defrem.h"
@@ -3133,6 +3134,149 @@ transformIndexStmt(Oid relid, IndexStmt *stmt, const char *queryString)
 	return stmt;
 }
 
+/*
+ * transformCreateTriggerStmt - parse analysis for CREATE TRIGGER
+ *
+ * Note: This is for parse analysis CreateTrigStmt->whenClause only, other
+ * CreateTrigStmt error checking happen in CreateTriggerFiringOn.
+ *
+ * To avoid race conditions, it's important that this function relies only on
+ * the passed-in relid (and not on stmt->relation) to determine the target
+ * relation.
+ */
+CreateTrigStmt *
+transformCreateTriggerStmt(Oid relid, CreateTrigStmt *stmt, const char *queryString)
+{
+	int16		tgtype;
+	ParseState *pstate;
+	ParseNamespaceItem *nsitem;
+	List	   *varList;
+	Relation	rel;
+
+	/* Nothing to do if statement already transformed. */
+	if (stmt->transformed)
+		return stmt;
+
+	/* Compute tgtype */
+	TRIGGER_CLEAR_TYPE(tgtype);
+	if (stmt->row)
+		TRIGGER_SETT_ROW(tgtype);
+	tgtype |= stmt->timing;
+	tgtype |= stmt->events;
+
+	/* Set up a pstate to parse with */
+	pstate = make_parsestate(NULL);
+	pstate->p_sourcetext = queryString;
+
+	/*
+	 * Put the parent table into the rtable so that the expressions can refer
+	 * to its fields without qualification.  Caller is responsible for locking
+	 * relation, but we still need to open it.
+	 */
+	rel = relation_open(relid, NoLock);
+
+	/*
+	 * Set up nsitems for OLD and NEW references.
+	 *
+	 * 'OLD' must always have varno equal to 1 and 'NEW' equal to 2.
+	*/
+	nsitem = addRangeTableEntryForRelation(pstate, rel,
+										   AccessShareLock,
+										   makeAlias("old", NIL),
+										   false, false);
+	addNSItemToQuery(pstate, nsitem, false, true, true);
+	nsitem = addRangeTableEntryForRelation(pstate, rel,
+										   AccessShareLock,
+										   makeAlias("new", NIL),
+										   false, false);
+	addNSItemToQuery(pstate, nsitem, false, true, true);
+
+	stmt->whenClause = transformWhereClause(pstate,
+											stmt->whenClause,
+											EXPR_KIND_TRIGGER_WHEN,
+											"WHEN");
+	/* we have to fix its collations too */
+	assign_expr_collations(pstate, stmt->whenClause);
+
+	/*
+	 * Check for disallowed references to OLD/NEW.
+	 *
+	 * NB: pull_var_clause is okay here only because we don't allow
+	 * subselects in WHEN clauses; it would fail to examine the contents
+	 * of subselects.
+	*/
+	varList = pull_var_clause(stmt->whenClause, 0);
+	foreach_node(Var, var, varList)
+	{
+		switch (var->varno)
+		{
+			case PRS2_OLD_VARNO:
+				if (!TRIGGER_FOR_ROW(tgtype))
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("statement trigger's WHEN condition cannot reference column values"),
+							parser_errposition(pstate, var->location));
+				if (TRIGGER_FOR_INSERT(tgtype))
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("INSERT trigger's WHEN condition cannot reference OLD values"),
+							parser_errposition(pstate, var->location));
+				/* system columns are okay here */
+				break;
+			case PRS2_NEW_VARNO:
+				if (!TRIGGER_FOR_ROW(tgtype))
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("statement trigger's WHEN condition cannot reference column values"),
+							parser_errposition(pstate, var->location));
+				if (TRIGGER_FOR_DELETE(tgtype))
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("DELETE trigger's WHEN condition cannot reference NEW values"),
+							parser_errposition(pstate, var->location));
+				if (var->varattno < 0 && TRIGGER_FOR_BEFORE(tgtype))
+					ereport(ERROR,
+							errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							errmsg("BEFORE trigger's WHEN condition cannot reference NEW system columns"),
+							parser_errposition(pstate, var->location));
+				if (TRIGGER_FOR_BEFORE(tgtype) &&
+					var->varattno == 0 &&
+					RelationGetDescr(rel)->constr &&
+					(RelationGetDescr(rel)->constr->has_generated_stored ||
+						RelationGetDescr(rel)->constr->has_generated_virtual))
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
+							errdetail("A whole-row reference is used and the table contains generated columns."),
+							parser_errposition(pstate, var->location));
+				if (TRIGGER_FOR_BEFORE(tgtype) &&
+					var->varattno > 0 &&
+					TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attgenerated)
+					ereport(ERROR,
+							errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+							errmsg("BEFORE trigger's WHEN condition cannot reference NEW generated columns"),
+							errdetail("Column \"%s\" is a generated column.",
+									  NameStr(TupleDescAttr(RelationGetDescr(rel), var->varattno - 1)->attname)),
+							parser_errposition(pstate, var->location));
+				break;
+			default:
+				/* can't happen without add_missing_from, so just elog */
+				elog(ERROR, "trigger WHEN condition cannot contain references to other relations");
+				break;
+		}
+	}
+
+	free_parsestate(pstate);
+
+	/* Close relation */
+	table_close(rel, NoLock);
+
+	/* Mark statement as successfully transformed */
+	stmt->transformed = true;
+
+	return stmt;
+}
+
 /*
  * transformStatsStmt - parse analysis for CREATE STATISTICS
  *
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 918db53dd5e..73d47b5ebf2 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -1695,7 +1695,7 @@ ProcessUtilitySlow(ParseState *pstate,
 				address = CreateTrigger((CreateTrigStmt *) parsetree,
 										queryString, InvalidOid, InvalidOid,
 										InvalidOid, InvalidOid, InvalidOid,
-										InvalidOid, NULL, false, false);
+										InvalidOid, false, false);
 				break;
 
 			case T_CreatePLangStmt:
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index cfd7daa20ed..3f4951bca61 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -153,12 +153,12 @@ extern PGDLLIMPORT int SessionReplicationRole;
 
 extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString,
 								   Oid relOid, Oid refRelOid, Oid constraintOid, Oid indexOid,
-								   Oid funcoid, Oid parentTriggerOid, Node *whenClause,
+								   Oid funcoid, Oid parentTriggerOid,
 								   bool isInternal, bool in_partition);
 extern ObjectAddress CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 										   Oid relOid, Oid refRelOid, Oid constraintOid,
 										   Oid indexOid, Oid funcoid, Oid parentTriggerOid,
-										   Node *whenClause, bool isInternal, bool in_partition,
+										   bool isInternal, bool in_partition,
 										   char trigger_fires_when);
 
 extern void TriggerSetParentTrigger(Relation trigRel,
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 4ed14fc5b78..fdb0b8a29dd 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -3122,6 +3122,7 @@ typedef struct CreateTrigStmt
 	bool		deferrable;		/* [NOT] DEFERRABLE */
 	bool		initdeferred;	/* INITIALLY {DEFERRED|IMMEDIATE} */
 	RangeVar   *constrrel;		/* opposite relation, if RI trigger */
+	bool		transformed;	/* true when transformCreateTriggerStmt is finished */
 } CreateTrigStmt;
 
 /* ----------------------
diff --git a/src/include/parser/parse_utilcmd.h b/src/include/parser/parse_utilcmd.h
index 4965fac4495..5482719d997 100644
--- a/src/include/parser/parse_utilcmd.h
+++ b/src/include/parser/parse_utilcmd.h
@@ -28,6 +28,8 @@ extern IndexStmt *transformIndexStmt(Oid relid, IndexStmt *stmt,
 									 const char *queryString);
 extern CreateStatsStmt *transformStatsStmt(Oid relid, CreateStatsStmt *stmt,
 										   const char *queryString);
+extern CreateTrigStmt *transformCreateTriggerStmt(Oid relid, CreateTrigStmt *stmt,
+												  const char *queryString);
 extern void transformRuleStmt(RuleStmt *stmt, const char *queryString,
 							  List **actions, Node **whereClause);
 extern List *transformCreateSchemaStmtElements(List *schemaElts,
-- 
2.34.1

From 443352430ea0daca1c285a130144bfb59999256e Mon Sep 17 00:00:00 2001
From: jian he <[email protected]>
Date: Mon, 29 Sep 2025 19:07:49 +0800
Subject: [PATCH v1 2/2] let ALTER COLUMN SET DATA TYPE cope with trigger
 dependency
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

demo:
CREATE TABLE main_table (a int, b int);
CREATE FUNCTION trigger_func() RETURNS trigger LANGUAGE plpgsql AS '
BEGIN
	RAISE NOTICE ''trigger_func(%) called: action = %, when = %, level = %'',
	              TG_ARGV[0], TG_OP, TG_WHEN, TG_LEVEL;
	RETURN NULL;
END;';
CREATE TRIGGER modified_a BEFORE UPDATE OF a ON main_table
FOR EACH ROW WHEN (OLD.a <> NEW.a) EXECUTE PROCEDURE trigger_func('modified_a');

ALTER TABLE main_table ALTER COLUMN a SET DATA TYPE INT8;
ERROR:  cannot alter type of a column used in a trigger definition
DETAIL:  trigger modified_a on table main_table depends on column "a"

With the attached patch, the previous error will no longer occur.
Foreign key–related internal triggers are not directly dependent on the relation
itself; instead, they depend directly on the constraint. Therefore, internal
triggers are not a concern in this context.

discussion: https://postgr.es/m/
---
 src/backend/catalog/index.c                   |   1 +
 src/backend/commands/tablecmds.c              | 148 ++++++++++++++++--
 src/backend/commands/trigger.c                |  61 ++++++++
 src/backend/parser/gram.y                     |   2 +
 src/backend/utils/adt/ruleutils.c             |  10 ++
 src/include/commands/trigger.h                |   1 +
 src/include/nodes/parsenodes.h                |   2 +
 src/include/utils/ruleutils.h                 |   1 +
 .../test_ddl_deparse/test_ddl_deparse.c       |   3 +
 src/test/regress/expected/triggers.out        |  62 ++++++++
 src/test/regress/sql/triggers.sql             |  14 ++
 11 files changed, 288 insertions(+), 17 deletions(-)

diff --git a/src/backend/catalog/index.c b/src/backend/catalog/index.c
index 8d09d61cae2..712014e6f95 100644
--- a/src/backend/catalog/index.c
+++ b/src/backend/catalog/index.c
@@ -2039,6 +2039,7 @@ index_constraint_create(Relation heapRelation,
 		trigger->deferrable = true;
 		trigger->initdeferred = initdeferred;
 		trigger->constrrel = NULL;
+		trigger->trigcomment = NULL;
 		trigger->transformed = true;
 
 		(void) CreateTrigger(trigger, NULL, RelationGetRelid(heapRelation),
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index bf8120c12f2..dc262e257e6 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -208,6 +208,8 @@ typedef struct AlteredTableInfo
 	char	   *clusterOnIndex; /* index to use for CLUSTER */
 	List	   *changedStatisticsOids;	/* OIDs of statistics to rebuild */
 	List	   *changedStatisticsDefs;	/* string definitions of same */
+	List	   *changedTriggerOids;		/* OIDs of trigger to rebuild */
+	List	   *changedTriggerDefs;		/* string definitions of same */
 } AlteredTableInfo;
 
 /* Struct describing one new constraint to check in Phase 3 scan */
@@ -546,6 +548,8 @@ static ObjectAddress ATExecAddIndex(AlteredTableInfo *tab, Relation rel,
 									IndexStmt *stmt, bool is_rebuild, LOCKMODE lockmode);
 static ObjectAddress ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
 										 CreateStatsStmt *stmt, bool is_rebuild, LOCKMODE lockmode);
+static ObjectAddress ATExecAddTrigger(AlteredTableInfo *tab, Relation rel, CreateTrigStmt *stmt,
+									  bool is_rebuild, LOCKMODE lockmode);
 static ObjectAddress ATExecAddConstraint(List **wqueue,
 										 AlteredTableInfo *tab, Relation rel,
 										 Constraint *newConstraint, bool recurse, bool is_readd,
@@ -651,6 +655,7 @@ static void RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableT
 static void RememberConstraintForRebuilding(Oid conoid, AlteredTableInfo *tab);
 static void RememberIndexForRebuilding(Oid indoid, AlteredTableInfo *tab);
 static void RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab);
+static void RememberTriggerForRebuilding(Oid trigoid, AlteredTableInfo *tab);
 static void ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab,
 								   LOCKMODE lockmode);
 static void ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId,
@@ -5449,6 +5454,10 @@ ATExecCmd(List **wqueue, AlteredTableInfo *tab,
 			address = ATExecAddStatistics(tab, rel, (CreateStatsStmt *) cmd->def,
 										  true, lockmode);
 			break;
+		case AT_ReAddTrigger:	/* ADD TRIGGER */
+			address = ATExecAddTrigger(tab, rel, castNode(CreateTrigStmt, cmd->def),
+									   true, lockmode);
+			break;
 		case AT_AddConstraint:	/* ADD CONSTRAINT */
 			/* Transform the command only during initial examination */
 			if (cur_pass == AT_PASS_ADD_CONSTR)
@@ -6716,6 +6725,8 @@ alter_table_type_to_string(AlterTableType cmdtype)
 			return "ALTER COLUMN ... DROP IDENTITY";
 		case AT_ReAddStatistics:
 			return NULL;		/* not real grammar */
+		case AT_ReAddTrigger:
+			return NULL;		/* not real grammar */
 	}
 
 	return NULL;
@@ -9663,6 +9674,33 @@ ATExecAddStatistics(AlteredTableInfo *tab, Relation rel,
 	return address;
 }
 
+/*
+ * ALTER TABLE ADD TRIGGER
+ *
+ * This is no such command in the grammar, but we use this internally to add
+ * AT_ReAddTrigger subcommands to rebuild trigger after a table
+ * column type change.
+ */
+static ObjectAddress
+ATExecAddTrigger(AlteredTableInfo *tab, Relation rel,
+				 CreateTrigStmt *stmt, bool is_rebuild, LOCKMODE lockmode)
+{
+	ObjectAddress address;
+
+	Assert(IsA(stmt, CreateTrigStmt));
+
+	/* The CreateTrigStmt has already been through transformCreateTriggerStmt */
+	Assert(stmt->transformed);
+
+	address = CreateTrigger((CreateTrigStmt *) stmt,
+							NULL,
+							InvalidOid, InvalidOid, InvalidOid,
+							InvalidOid, InvalidOid, InvalidOid,
+							false, false);
+	return address;
+}
+
+
 /*
  * ALTER TABLE ADD CONSTRAINT USING INDEX
  *
@@ -13802,6 +13840,7 @@ CreateFKCheckTrigger(Oid myRelOid, Oid refRelOid, Constraint *fkconstraint,
 	fk_trigger->deferrable = fkconstraint->deferrable;
 	fk_trigger->initdeferred = fkconstraint->initdeferred;
 	fk_trigger->constrrel = NULL;
+	fk_trigger->trigcomment = NULL;
 	fk_trigger->transformed = true;
 
 	trigAddress = CreateTrigger(fk_trigger, NULL, myRelOid, refRelOid,
@@ -13848,6 +13887,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
 	fk_trigger->whenClause = NULL;
 	fk_trigger->transitionRels = NIL;
 	fk_trigger->constrrel = NULL;
+	fk_trigger->trigcomment = NULL;
 	fk_trigger->transformed = true;
 
 	switch (fkconstraint->fk_del_action)
@@ -13909,6 +13949,7 @@ createForeignKeyActionTriggers(Oid myRelOid, Oid refRelOid, Constraint *fkconstr
 	fk_trigger->whenClause = NULL;
 	fk_trigger->transitionRels = NIL;
 	fk_trigger->constrrel = NULL;
+	fk_trigger->trigcomment = NULL;
 	fk_trigger->transformed = true;
 
 	switch (fkconstraint->fk_upd_action)
@@ -15121,21 +15162,13 @@ RememberAllDependentForRebuilding(AlteredTableInfo *tab, AlterTableType subtype,
 			case TriggerRelationId:
 
 				/*
-				 * A trigger can depend on a column because the column is
-				 * specified as an update target, or because the column is
-				 * used in the trigger's WHEN condition.  The first case would
-				 * not require any extra work, but the second case would
-				 * require updating the WHEN expression, which has the same
-				 * issues as above.  Since we can't easily tell which case
-				 * applies, we punt for both.  FIXME someday.
-				 */
+				 * Internally-generated trigger for a constraint will have
+				 * internal dependency of the constraint. It won't have direct
+				 * dependency on the relation(s). So no need to worry about
+				 * internal trigger here.
+				*/
 				if (subtype == AT_AlterColumnType)
-					ereport(ERROR,
-							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-							 errmsg("cannot alter type of a column used in a trigger definition"),
-							 errdetail("%s depends on column \"%s\"",
-									   getObjectDescription(&foundObject, false),
-									   colName)));
+					RememberTriggerForRebuilding(foundObject.objectId, tab);
 				break;
 
 			case PolicyRelationId:
@@ -15396,6 +15429,33 @@ RememberStatisticsForRebuilding(Oid stxoid, AlteredTableInfo *tab)
 	}
 }
 
+/*
+ * Subroutine for ATExecAlterColumnType: remember that a trigger object
+ * needs to be rebuilt (which we might already know).
+ */
+static void
+RememberTriggerForRebuilding(Oid trigoid, AlteredTableInfo *tab)
+{
+	/*
+	 * This de-duplication check is critical for two independent reasons: we
+	 * mustn't try to recreate the same trigger object twice, and if the
+	 * trigger object depends on more than one column whose type is to be
+	 * altered, we must capture its definition string before applying any of
+	 * the type changes. ruleutils.c will get confused if we ask again later.
+	 */
+	if (!list_member_oid(tab->changedTriggerOids, trigoid))
+	{
+		/* OK, capture the trigger object's existing definition string */
+		char	   *defstring = pg_get_triggerobjdef_string(trigoid);
+
+		tab->changedTriggerOids = lappend_oid(tab->changedTriggerOids,
+											  trigoid);
+		tab->changedTriggerDefs = lappend(tab->changedTriggerDefs,
+										  defstring);
+	}
+}
+
+
 /*
  * Cleanup after we've finished all the ALTER TYPE or SET EXPRESSION
  * operations for a particular relation.  We have to drop and recreate all the
@@ -15540,6 +15600,40 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
 		add_exact_object_address(&obj, objects);
 	}
 
+	/* add dependencies for new triggers */
+	forboth(oid_item, tab->changedTriggerOids,
+			def_item, tab->changedTriggerDefs)
+	{
+		List	*relids;
+		Oid		oldId = lfirst_oid(oid_item);
+
+		relids = TriggerGetRelations(oldId);
+		Assert(relids != NIL);
+
+		/*
+		 * As above, make sure we have lock on the trigger object's table
+		 * if it's not the same table.  However, we take
+		 * ShareRowExclusiveLock here, aligning with the lock level used in
+		 * CreateTriggerFiringOn.
+		 *
+		 * CAUTION: this should be done after all cases that grab
+		 * AccessExclusiveLock, else we risk causing deadlock due to needing
+		 * to promote our table lock.
+		 */
+		foreach_oid(relid, relids)
+		{
+			if (relid != tab->relid)
+				LockRelationOid(relid, ShareRowExclusiveLock);
+		}
+
+		ATPostAlterTypeParse(oldId, linitial_oid(relids), InvalidOid,
+							 (char *) lfirst(def_item),
+							 wqueue, lockmode, tab->rewrite);
+
+		ObjectAddressSet(obj, TriggerRelationId, oldId);
+		add_exact_object_address(&obj, objects);
+	}
+
 	/*
 	 * Queue up command to restore replica identity index marking
 	 */
@@ -15588,9 +15682,9 @@ ATPostAlterTypeCleanup(List **wqueue, AlteredTableInfo *tab, LOCKMODE lockmode)
 }
 
 /*
- * Parse the previously-saved definition string for a constraint, index or
- * statistics object against the newly-established column data type(s), and
- * queue up the resulting command parsetrees for execution.
+ * Parse the previously-saved definition string for a constraint, index,
+ * statistics object or trigger against the newly-established column data
+ * type(s), and queue up the resulting command parsetrees for execution.
  *
  * This might fail if, for example, you have a WHERE clause that uses an
  * operator that's not available for the new column type.
@@ -15641,6 +15735,11 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd,
 									 transformStatsStmt(oldRelId,
 														(CreateStatsStmt *) stmt,
 														cmd));
+		else if (IsA(stmt, CreateTrigStmt))
+			querytree_list = lappend(querytree_list,
+									 transformCreateTriggerStmt(oldRelId,
+																castNode(CreateTrigStmt, stmt),
+																cmd));
 		else
 			querytree_list = lappend(querytree_list, stmt);
 	}
@@ -15791,6 +15890,20 @@ ATPostAlterTypeParse(Oid oldId, Oid oldRelId, Oid refRelId, char *cmd,
 			tab->subcmds[AT_PASS_MISC] =
 				lappend(tab->subcmds[AT_PASS_MISC], newcmd);
 		}
+		else if (IsA(stm, CreateTrigStmt))
+		{
+			CreateTrigStmt *stmt = (CreateTrigStmt *) stm;
+			AlterTableCmd *newcmd;
+
+			/* keep the trigger object's comment */
+			stmt->trigcomment = GetComment(oldId, TriggerRelationId, 0);
+
+			newcmd = makeNode(AlterTableCmd);
+			newcmd->subtype = AT_ReAddTrigger;
+			newcmd->def = (Node *) stmt;
+			tab->subcmds[AT_PASS_MISC] =
+				lappend(tab->subcmds[AT_PASS_MISC], newcmd);
+		}
 		else
 			elog(ERROR, "unexpected statement type: %d",
 				 (int) nodeTag(stm));
@@ -20831,6 +20944,7 @@ CloneRowTriggersToPartition(Relation parent, Relation partition)
 		trigStmt->deferrable = trigForm->tgdeferrable;
 		trigStmt->initdeferred = trigForm->tginitdeferred;
 		trigStmt->constrrel = NULL; /* passed separately */
+		trigStmt->trigcomment = NULL;
 		trigStmt->transformed = true; /* whenClause alerady transformed */
 
 		CreateTriggerFiringOn(trigStmt, NULL, RelationGetRelid(partition),
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index f1431d99a56..4f01dad5853 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -30,6 +30,7 @@
 #include "catalog/pg_proc.h"
 #include "catalog/pg_trigger.h"
 #include "catalog/pg_type.h"
+#include "commands/comment.h"
 #include "commands/trigger.h"
 #include "executor/executor.h"
 #include "miscadmin.h"
@@ -1122,6 +1123,11 @@ CreateTriggerFiringOn(CreateTrigStmt *stmt, const char *queryString,
 	/* Keep lock on target rel until end of xact */
 	table_close(rel, NoLock);
 
+	/* Add any requested comment */
+	if (stmt->trigcomment != NULL)
+		CreateComments(trigoid, TriggerRelationId, 0,
+					   stmt->trigcomment);
+
 	return myself;
 }
 
@@ -1331,6 +1337,61 @@ get_trigger_oid(Oid relid, const char *trigname, bool missing_ok)
 	return oid;
 }
 
+
+ /*
+  * TriggerGetRelations
+  *
+  * Collect all relations this trigger depends on.  The constraint trigger may
+  * reference another relation, we include it as well.
+ */
+List *
+TriggerGetRelations(Oid trigId)
+{
+	HeapTuple	ht_trig;
+	Form_pg_trigger trigrec;
+	Relation	tgrel;
+	ScanKeyData skey[1];
+	SysScanDesc tgscan;
+	List		*result = NIL;
+
+	/*
+	 * find the pg_trigger tuple by the Oid of the trigger
+	 */
+	tgrel = table_open(TriggerRelationId, AccessShareLock);
+
+	ScanKeyInit(&skey[0],
+				Anum_pg_trigger_oid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(trigId));
+
+	tgscan = systable_beginscan(tgrel, TriggerOidIndexId, true,
+								NULL, 1, skey);
+
+	ht_trig = systable_getnext(tgscan);
+
+	if (!HeapTupleIsValid(ht_trig))
+	{
+		systable_endscan(tgscan);
+		table_close(tgrel, AccessShareLock);
+		return result;
+	}
+
+	trigrec = (Form_pg_trigger) GETSTRUCT(ht_trig);
+	Assert(trigrec->oid = trigId);
+
+	result = lappend_oid(result, trigrec->tgrelid);
+
+	if (OidIsValid(trigrec->tgconstrrelid))
+		result = lappend_oid(result, trigrec->tgconstrrelid);
+
+	/* Clean up */
+	systable_endscan(tgscan);
+
+	table_close(tgrel, AccessShareLock);
+
+	return result;
+}
+
 /*
  * Perform permissions and integrity checks before acquiring a relation lock.
  */
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 05ff0c5973f..4112c34dace 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -6070,6 +6070,7 @@ CreateTrigStmt:
 					n->deferrable = false;
 					n->initdeferred = false;
 					n->constrrel = NULL;
+					n->trigcomment = NULL;
 					n->transformed = false;
 					$$ = (Node *) n;
 				}
@@ -6121,6 +6122,7 @@ CreateTrigStmt:
 								   &n->deferrable, &n->initdeferred, &dummy,
 								   NULL, NULL, yyscanner);
 					n->constrrel = $10;
+					n->trigcomment = NULL;
 					n->transformed = false;
 					$$ = (Node *) n;
 				}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 0408a95941d..792266e434c 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -881,6 +881,16 @@ pg_get_triggerdef(PG_FUNCTION_ARGS)
 	PG_RETURN_TEXT_P(string_to_text(res));
 }
 
+/*
+ * Internal version for use by ALTER TABLE.
+ * Returns a palloc'd C string; no pretty-printing.
+ */
+char *
+pg_get_triggerobjdef_string(Oid trigid)
+{
+	return pg_get_triggerdef_worker(trigid, false);
+}
+
 Datum
 pg_get_triggerdef_ext(PG_FUNCTION_ARGS)
 {
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 3f4951bca61..a02de432495 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -167,6 +167,7 @@ extern void TriggerSetParentTrigger(Relation trigRel,
 									Oid childTableId);
 extern void RemoveTriggerById(Oid trigOid);
 extern Oid	get_trigger_oid(Oid relid, const char *trigname, bool missing_ok);
+extern List *TriggerGetRelations(Oid trigId);
 
 extern ObjectAddress renametrig(RenameStmt *stmt);
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index fdb0b8a29dd..4114488ca82 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -2477,6 +2477,7 @@ typedef enum AlterTableType
 	AT_SetIdentity,				/* SET identity column options */
 	AT_DropIdentity,			/* DROP IDENTITY */
 	AT_ReAddStatistics,			/* internal to commands/tablecmds.c */
+	AT_ReAddTrigger,			/* internal to commands/tablecmds.c */
 } AlterTableType;
 
 typedef struct AlterTableCmd	/* one subcommand of an ALTER TABLE */
@@ -3122,6 +3123,7 @@ typedef struct CreateTrigStmt
 	bool		deferrable;		/* [NOT] DEFERRABLE */
 	bool		initdeferred;	/* INITIALLY {DEFERRED|IMMEDIATE} */
 	RangeVar   *constrrel;		/* opposite relation, if RI trigger */
+	char	   *trigcomment;	/* comment to apply to trigger, or NULL */
 	bool		transformed;	/* true when transformCreateTriggerStmt is finished */
 } CreateTrigStmt;
 
diff --git a/src/include/utils/ruleutils.h b/src/include/utils/ruleutils.h
index 7ba7d887914..1bb89b57493 100644
--- a/src/include/utils/ruleutils.h
+++ b/src/include/utils/ruleutils.h
@@ -53,5 +53,6 @@ extern char *generate_opclass_name(Oid opclass);
 extern char *get_range_partbound_string(List *bound_datums);
 
 extern char *pg_get_statisticsobjdef_string(Oid statextid);
+extern char *pg_get_triggerobjdef_string(Oid trigid);
 
 #endif							/* RULEUTILS_H */
diff --git a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
index 193669f2bc1..5009681fbb9 100644
--- a/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
+++ b/src/test/modules/test_ddl_deparse/test_ddl_deparse.c
@@ -308,6 +308,9 @@ get_altertable_subcmdinfo(PG_FUNCTION_ARGS)
 			case AT_ReAddStatistics:
 				strtype = "(re) ADD STATS";
 				break;
+			case AT_ReAddTrigger:
+				strtype = "(re) ADD TRIGGER";
+				break;
 		}
 
 		if (subcmd->recurse)
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 1eb8fba0953..ecb2f36074e 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -316,6 +316,38 @@ SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'main_table'
  CREATE TRIGGER modified_any BEFORE UPDATE OF a ON main_table FOR EACH ROW WHEN (old.* IS DISTINCT FROM new.*) EXECUTE FUNCTION trigger_func('modified_any')
 (1 row)
 
+COMMENT ON TRIGGER modified_a ON main_table IS 'modified_a trigger';
+ALTER TABLE main_table ALTER COLUMN a SET DATA TYPE TEXT; --error
+ERROR:  operator does not exist: text = integer
+DETAIL:  No operator of that name accepts the given argument types.
+HINT:  You might need to add explicit type casts.
+ALTER TABLE main_table ALTER COLUMN a SET DATA TYPE INT8;
+\d main_table
+             Table "public.main_table"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | bigint  |           |          | 
+ b      | integer |           |          | 
+Triggers:
+    after_ins_stmt_trig AFTER INSERT ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_ins_stmt')
+    after_upd_row_trig AFTER UPDATE ON main_table FOR EACH ROW EXECUTE FUNCTION trigger_func('after_upd_row')
+    after_upd_stmt_trig AFTER UPDATE ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('after_upd_stmt')
+    before_ins_stmt_trig BEFORE INSERT ON main_table FOR EACH STATEMENT EXECUTE FUNCTION trigger_func('before_ins_stmt')
+    delete_a AFTER DELETE ON main_table FOR EACH ROW WHEN (old.a = 123) EXECUTE FUNCTION trigger_func('delete_a')
+    delete_when AFTER DELETE ON main_table FOR EACH STATEMENT WHEN (true) EXECUTE FUNCTION trigger_func('delete_when')
+    insert_a AFTER INSERT ON main_table FOR EACH ROW WHEN (new.a = 123) EXECUTE FUNCTION trigger_func('insert_a')
+    insert_when BEFORE INSERT ON main_table FOR EACH STATEMENT WHEN (true) EXECUTE FUNCTION trigger_func('insert_when')
+    modified_a BEFORE UPDATE OF a ON main_table FOR EACH ROW WHEN (old.a <> new.a) EXECUTE FUNCTION trigger_func('modified_a')
+    modified_any BEFORE UPDATE OF a ON main_table FOR EACH ROW WHEN (old.* IS DISTINCT FROM new.*) EXECUTE FUNCTION trigger_func('modified_any')
+
+\dd modified_a
+                Object descriptions
+ Schema |    Name    | Object  |    Description     
+--------+------------+---------+--------------------
+ public | modified_a | trigger | modified_a trigger
+(1 row)
+
+ALTER TABLE main_table ALTER COLUMN a SET DATA TYPE INT4; --change it back
 -- Test RENAME TRIGGER
 ALTER TRIGGER modified_a ON main_table RENAME TO modified_modified_a;
 SELECT count(*) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'modified_a';
@@ -343,6 +375,7 @@ create trigger oid_unchanged_trig after update on table_with_oids
 	for each row
 	when (new.tableoid = old.tableoid AND new.tableoid <> 0)
 	execute procedure trigger_func('after_upd_oid_unchanged');
+alter table table_with_oids alter column a set data type bigint;
 update table_with_oids set a = a + 1;
 NOTICE:  trigger_func(after_upd_oid_unchanged) called: action = UPDATE, when = AFTER, level = ROW
 drop table table_with_oids;
@@ -2165,6 +2198,20 @@ insert into parted_irreg_ancestor values ('aasvogel', 3);
 NOTICE:  aasvogel <- woof!
 NOTICE:  trigger parted_trig on parted1_irreg AFTER INSERT for ROW: (a,b)=(3,aasvogel)
 NOTICE:  trigger parted_trig_odd on parted1_irreg AFTER INSERT for ROW: (a,b)=(3,aasvogel)
+alter table parted_irreg_ancestor alter column a set data type bigint;
+\d parted_irreg
+     Partitioned table "public.parted_irreg"
+ Column |  Type  | Collation | Nullable | Default 
+--------+--------+-----------+----------+---------
+ a      | bigint |           |          | 
+ b      | text   |           |          | 
+Partition of: parted_irreg_ancestor FOR VALUES FROM ('aaaa') TO ('zzzz')
+Partition key: RANGE (b)
+Triggers:
+    parted_trig AFTER INSERT ON parted_irreg FOR EACH ROW EXECUTE FUNCTION trigger_notice_ab()
+    parted_trig_odd AFTER INSERT ON parted_irreg FOR EACH ROW WHEN (bark(new.b) AND (new.a % 2::bigint) = 1) EXECUTE FUNCTION trigger_notice_ab()
+Number of partitions: 1 (Use \d+ to list them.)
+
 drop table parted_irreg_ancestor;
 -- Before triggers and partitions
 create table parted (a int, b int, c text) partition by list (a);
@@ -2318,6 +2365,21 @@ create constraint trigger parted_trig_two after insert on parted_constr
   deferrable initially deferred enforced
   for each row when (bark(new.b) AND new.a % 2 = 1)
   execute procedure trigger_notice_ab();
+alter table parted_constr_ancestor alter column a set data type bigint;
+\d parted_constr
+     Partitioned table "public.parted_constr"
+ Column |  Type  | Collation | Nullable | Default 
+--------+--------+-----------+----------+---------
+ a      | bigint |           |          | 
+ b      | text   |           |          | 
+Partition of: parted_constr_ancestor FOR VALUES FROM ('aaaa') TO ('zzzz')
+Partition key: RANGE (b)
+Triggers:
+    parted_trig AFTER INSERT ON parted_constr DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE FUNCTION trigger_notice_ab(), ON TABLE parted_constr_ancestor
+    parted_trig_two AFTER INSERT ON parted_constr DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN (bark(new.b) AND (new.a % 2::bigint) = 1) EXECUTE FUNCTION trigger_notice_ab()
+Number of partitions: 1 (Use \d+ to list them.)
+
+alter table parted_constr_ancestor alter column a set data type int;
 -- The immediate constraint is fired immediately; the WHEN clause of the
 -- deferred constraint is also called immediately.  The deferred constraint
 -- is fired at commit time.
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 5f7f75d7ba5..6513eb83d27 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -192,6 +192,13 @@ SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'main_table'
 SELECT pg_get_triggerdef(oid, false) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'modified_a';
 SELECT pg_get_triggerdef(oid, true) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'modified_any';
 
+COMMENT ON TRIGGER modified_a ON main_table IS 'modified_a trigger';
+ALTER TABLE main_table ALTER COLUMN a SET DATA TYPE TEXT; --error
+ALTER TABLE main_table ALTER COLUMN a SET DATA TYPE INT8;
+\d main_table
+\dd modified_a
+ALTER TABLE main_table ALTER COLUMN a SET DATA TYPE INT4; --change it back
+
 -- Test RENAME TRIGGER
 ALTER TRIGGER modified_a ON main_table RENAME TO modified_modified_a;
 SELECT count(*) FROM pg_trigger WHERE tgrelid = 'main_table'::regclass AND tgname = 'modified_a';
@@ -211,6 +218,7 @@ create trigger oid_unchanged_trig after update on table_with_oids
 	for each row
 	when (new.tableoid = old.tableoid AND new.tableoid <> 0)
 	execute procedure trigger_func('after_upd_oid_unchanged');
+alter table table_with_oids alter column a set data type bigint;
 update table_with_oids set a = a + 1;
 drop table table_with_oids;
 
@@ -1495,6 +1503,8 @@ create trigger parted_trig_odd after insert on parted_irreg for each row
 insert into parted_irreg values (1, 'aardvark'), (2, 'aanimals');
 insert into parted1_irreg values ('aardwolf', 2);
 insert into parted_irreg_ancestor values ('aasvogel', 3);
+alter table parted_irreg_ancestor alter column a set data type bigint;
+\d parted_irreg
 drop table parted_irreg_ancestor;
 
 -- Before triggers and partitions
@@ -1608,6 +1618,10 @@ create constraint trigger parted_trig_two after insert on parted_constr
   for each row when (bark(new.b) AND new.a % 2 = 1)
   execute procedure trigger_notice_ab();
 
+alter table parted_constr_ancestor alter column a set data type bigint;
+\d parted_constr
+alter table parted_constr_ancestor alter column a set data type int;
+
 -- The immediate constraint is fired immediately; the WHEN clause of the
 -- deferred constraint is also called immediately.  The deferred constraint
 -- is fired at commit time.
-- 
2.34.1

Reply via email to