Rename of triggers for partitioned tables
Hello,
I got too annoyed at building queries for gexec all the time. So wrote a patch to fix the issue that the rename of partitioned trigger doesn't affect children.
In case there isn't a dependent trigger with the same name on the child the function errors out. If the user touched the trigger of the child, it's not a sensitive idea to mess with it.
I'm not happy with the interface change in trigger.h. Yet I don't like the idea of creating another layer of indirection either.
Even though it's a small patch, there might be other things I'm missing, so I'd appreciate feedback.
Regards
Arne
Attachments:
recursive_tgrename.patchtext/x-patch; name=recursive_tgrename.patchDownload
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index b11ebf0f61..e92dae78f7 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -365,7 +365,7 @@ ExecRenameStmt(RenameStmt *stmt)
stmt->newname);
case OBJECT_TRIGGER:
- return renametrig(stmt);
+ return renametrig(stmt, 0, 0);
case OBJECT_POLICY:
return rename_policy(stmt);
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index c336b238aa..039386958a 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -1357,12 +1357,7 @@ get_trigger_oid(Oid relid, const char *trigname, bool missing_ok)
return oid;
}
-/*
- * Perform permissions and integrity checks before acquiring a relation lock.
- */
-static void
-RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
- void *arg)
+static void callbackForRenameTrigger(char* relname, Oid relid)
{
HeapTuple tuple;
Form_pg_class form;
@@ -1379,22 +1374,32 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a table, view, or foreign table",
- rv->relname)));
+ relname)));
/* you must own the table to rename one of its triggers */
if (!pg_class_ownercheck(relid, GetUserId()))
- aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), rv->relname);
+ aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), relname);
if (!allowSystemTableMods && IsSystemClass(relid, form))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: \"%s\" is a system catalog",
- rv->relname)));
+ relname)));
ReleaseSysCache(tuple);
}
/*
- * renametrig - changes the name of a trigger on a relation
+ * Perform permissions and integrity checks before acquiring a relation lock.
+ */
+static void
+RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
+ void *arg)
+{
+ callbackForRenameTrigger(rv->relname, relid);
+}
+
+/*
+ * renameonlytrig - changes the name of a trigger on a relation
*
* trigger name is changed in trigger catalog.
* No record of the previous name is kept.
@@ -1407,41 +1412,26 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
* update row in catalog
*/
ObjectAddress
-renametrig(RenameStmt *stmt)
+renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent)
{
Oid tgoid;
Relation targetrel;
- Relation tgrel;
HeapTuple tuple;
SysScanDesc tgscan;
ScanKeyData key[2];
- Oid relid;
ObjectAddress address;
- /*
- * Look up name, check permissions, and acquire lock (which we will NOT
- * release until end of transaction).
- */
- relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
- 0,
- RangeVarCallbackForRenameTrigger,
- NULL);
/* Have lock already, so just need to build relcache entry. */
targetrel = relation_open(relid, NoLock);
+
/*
* Scan pg_trigger twice for existing triggers on relation. We do this in
* order to ensure a trigger does not exist with newname (The unique index
* on tgrelid/tgname would complain anyway) and to ensure a trigger does
* exist with oldname.
*
- * NOTE that this is cool only because we have AccessExclusiveLock on the
- * relation, so the trigger set won't be changing underneath us.
- */
- tgrel = table_open(TriggerRelationId, RowExclusiveLock);
-
- /*
* First pass -- look for name conflict
*/
ScanKeyInit(&key[0],
@@ -1472,6 +1462,7 @@ renametrig(RenameStmt *stmt)
Anum_pg_trigger_tgname,
BTEqualStrategyNumber, F_NAMEEQ,
PointerGetDatum(stmt->subname));
+
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
NULL, 2, key);
if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
@@ -1484,7 +1475,12 @@ renametrig(RenameStmt *stmt)
tuple = heap_copytuple(tuple); /* need a modifiable copy */
trigform = (Form_pg_trigger) GETSTRUCT(tuple);
tgoid = trigform->oid;
-
+ if (tgparent != 0 && tgparent != trigform->tgparentid) {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("trigger \"%s\" for table \"%s\" is not dependent on the trigger on \"%s\"",
+ stmt->subname, RelationGetRelationName(targetrel), stmt->relation->relname)));
+ }
namestrcpy(&trigform->tgname,
stmt->newname);
@@ -1522,6 +1518,73 @@ renametrig(RenameStmt *stmt)
return address;
}
+/*
+ * renametrig - changes the name of a trigger on a possibly partitioned relation recurisvely
+ *
+ * alter name of parent trigger using renameonlytrig
+ * recursive over children and change their names in the same manner
+ *
+ */
+ObjectAddress
+renametrig(RenameStmt *stmt, Oid tgoid, Oid relid)
+{
+
+ Relation tgrel;
+ Oid newtgoid;
+ ObjectAddress tgaddress;
+ /*
+ * NOTE that this is cool only because we have AccessExclusiveLock on the
+ * relation, so the trigger set won't be changing underneath us.
+ */
+ tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+
+ /*
+ * Look up name, check permissions, and acquire lock (which we will NOT
+ * release until end of transaction).
+ * If relid is set the last call of renamtrig already locked this relation.
+ */
+ if (relid == 0) {
+ relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
+ 0,
+ RangeVarCallbackForRenameTrigger,
+ NULL);
+ } else {
+ callbackForRenameTrigger(NULL, relid);
+ LockRelationOid(relid, AccessExclusiveLock);
+ }
+ tgaddress = renameonlytrig(stmt, tgrel, relid, tgoid);
+ newtgoid = tgaddress.objectId;
+
+ /*
+ * In case we have a partioned table we traverse them and rename every dependend subtrigger
+ * of the same name or error out if it doesn't exist.
+ */
+ if (has_subclass(relid))
+ {
+ ListCell *child;
+ List *children;
+
+ /* Make sure it stays a child. */
+ children = find_inheritance_children(relid, AccessExclusiveLock);
+
+ /*
+ * find_all_inheritors does the recursive search of the inheritance
+ * hierarchy, so all we have to do is process all of the relids in the
+ * list that it returns.
+ */
+ foreach(child, children)
+ {
+ Oid childrelid = lfirst_oid(child);
+
+ if (childrelid == relid)
+ continue;
+ renametrig(stmt, newtgoid, childrelid);
+ }
+ }
+ return tgaddress;
+}
+
+
/*
* EnableDisableTrigger()
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 244540d90b..8e8eb98ec0 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -158,7 +158,9 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
-extern ObjectAddress renametrig(RenameStmt *stmt);
+extern ObjectAddress renametrig(RenameStmt *stmt, Oid tgoid, Oid relid);
+
+extern ObjectAddress renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent);
extern void EnableDisableTrigger(Relation rel, const char *tgname,
char fires_when, bool skip_system, LOCKMODE lockmode);
I'm too unhappy with the interface change, so please view attached patch instead.
Regards
Arne
________________________________
From: Arne Roland <A.Roland@index.de>
Sent: Friday, November 27, 2020 12:28:31 AM
To: Pg Hackers
Subject: Rename of triggers for partitioned tables
Hello,
I got too annoyed at building queries for gexec all the time. So wrote a patch to fix the issue that the rename of partitioned trigger doesn't affect children.
In case there isn't a dependent trigger with the same name on the child the function errors out. If the user touched the trigger of the child, it's not a sensitive idea to mess with it.
I'm not happy with the interface change in trigger.h. Yet I don't like the idea of creating another layer of indirection either.
Even though it's a small patch, there might be other things I'm missing, so I'd appreciate feedback.
Regards
Arne
Attachments:
recursive_tgrename.1.patchtext/x-patch; name=recursive_tgrename.1.patchDownload
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index c336b238aa..f4647d38c5 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -1357,12 +1357,7 @@ get_trigger_oid(Oid relid, const char *trigname, bool missing_ok)
return oid;
}
-/*
- * Perform permissions and integrity checks before acquiring a relation lock.
- */
-static void
-RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
- void *arg)
+static void callbackForRenameTrigger(char* relname, Oid relid)
{
HeapTuple tuple;
Form_pg_class form;
@@ -1379,25 +1374,36 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a table, view, or foreign table",
- rv->relname)));
+ relname)));
/* you must own the table to rename one of its triggers */
if (!pg_class_ownercheck(relid, GetUserId()))
- aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), rv->relname);
+ aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), relname);
if (!allowSystemTableMods && IsSystemClass(relid, form))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: \"%s\" is a system catalog",
- rv->relname)));
+ relname)));
ReleaseSysCache(tuple);
}
/*
- * renametrig - changes the name of a trigger on a relation
+ * Perform permissions and integrity checks before acquiring a relation lock.
+ */
+static void
+RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
+ void *arg)
+{
+ callbackForRenameTrigger(rv->relname, relid);
+}
+
+/*
+ * renameonlytrig - changes the name of a trigger on a relation
*
* trigger name is changed in trigger catalog.
* No record of the previous name is kept.
+ * This function assumes that the row is already locked
*
* get proper relrelation from relation catalog (if not arg)
* scan trigger catalog
@@ -1407,41 +1413,26 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
* update row in catalog
*/
ObjectAddress
-renametrig(RenameStmt *stmt)
+renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent)
{
Oid tgoid;
Relation targetrel;
- Relation tgrel;
HeapTuple tuple;
SysScanDesc tgscan;
ScanKeyData key[2];
- Oid relid;
ObjectAddress address;
- /*
- * Look up name, check permissions, and acquire lock (which we will NOT
- * release until end of transaction).
- */
- relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
- 0,
- RangeVarCallbackForRenameTrigger,
- NULL);
/* Have lock already, so just need to build relcache entry. */
targetrel = relation_open(relid, NoLock);
+
/*
* Scan pg_trigger twice for existing triggers on relation. We do this in
* order to ensure a trigger does not exist with newname (The unique index
* on tgrelid/tgname would complain anyway) and to ensure a trigger does
* exist with oldname.
*
- * NOTE that this is cool only because we have AccessExclusiveLock on the
- * relation, so the trigger set won't be changing underneath us.
- */
- tgrel = table_open(TriggerRelationId, RowExclusiveLock);
-
- /*
* First pass -- look for name conflict
*/
ScanKeyInit(&key[0],
@@ -1472,6 +1463,7 @@ renametrig(RenameStmt *stmt)
Anum_pg_trigger_tgname,
BTEqualStrategyNumber, F_NAMEEQ,
PointerGetDatum(stmt->subname));
+
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
NULL, 2, key);
if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
@@ -1484,7 +1476,12 @@ renametrig(RenameStmt *stmt)
tuple = heap_copytuple(tuple); /* need a modifiable copy */
trigform = (Form_pg_trigger) GETSTRUCT(tuple);
tgoid = trigform->oid;
-
+ if (tgparent != 0 && tgparent != trigform->tgparentid) {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("trigger \"%s\" for table \"%s\" is not dependent on the trigger on \"%s\"",
+ stmt->subname, RelationGetRelationName(targetrel), stmt->relation->relname)));
+ }
namestrcpy(&trigform->tgname,
stmt->newname);
@@ -1522,6 +1519,74 @@ renametrig(RenameStmt *stmt)
return address;
}
+/*
+ * renametrig - changes the name of a trigger on a possibly partitioned relation recurisvely
+ *
+ * alter name of parent trigger using renameonlytrig
+ * recursive over children and change their names in the same manner
+ *
+ */
+ObjectAddress
+renametrigHelper (RenameStmt *stmt, Oid tgoid, Oid relid)
+{
+
+ Relation tgrel;
+ Oid newtgoid;
+ ObjectAddress tgaddress;
+ /*
+ * NOTE that this is cool only because we have AccessExclusiveLock on the
+ * relation, so the trigger set won't be changing underneath us.
+ */
+ tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+
+ /*
+ * The last call of renametrig or renametrigHelper already locked this relation.
+ */
+ tgaddress = renameonlytrig(stmt, tgrel, relid, tgoid);
+ newtgoid = tgaddress.objectId;
+
+ /*
+ * In case we have a partioned table we traverse them and rename every dependend subtrigger
+ * of the same name or error out if it doesn't exist.
+ */
+ if (has_subclass(relid))
+ {
+ ListCell *child;
+ List *children;
+
+ /* Make sure it stays a child. */
+ children = find_inheritance_children(relid, AccessExclusiveLock);
+
+ /*
+ * find_all_inheritors does the recursive search of the inheritance
+ * hierarchy, so all we have to do is process all of the relids in the
+ * list that it returns.
+ */
+ foreach(child, children)
+ {
+ Oid childrelid = lfirst_oid(child);
+
+ if (childrelid == relid)
+ continue;
+ renametrigHelper(stmt, newtgoid, childrelid);
+ }
+ }
+ return tgaddress;
+}
+
+ObjectAddress
+renametrig(RenameStmt *stmt)
+{
+ /*
+ * Look up name, check permissions, and acquire lock (which we will NOT
+ * release until end of transaction).
+ */
+ return renametrigHelper(stmt, 0,
+ RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
+ 0,
+ RangeVarCallbackForRenameTrigger,
+ NULL));
+}
/*
* EnableDisableTrigger()
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 244540d90b..c13d7bf931 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -158,6 +158,10 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
+extern ObjectAddress renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent);
+
+extern ObjectAddress renametrigHelper(RenameStmt *stmt, Oid tgoid, Oid relid);
+
extern ObjectAddress renametrig(RenameStmt *stmt);
extern void EnableDisableTrigger(Relation rel, const char *tgname,
I'm sorry for the spam. Here a patch with updated comments, I missed one before.
Regards
Arne
________________________________
From: Arne Roland <A.Roland@index.de>
Sent: Friday, November 27, 2020 1:05:02 AM
To: Pg Hackers
Subject: Re: Rename of triggers for partitioned tables
I'm too unhappy with the interface change, so please view attached patch instead.
Regards
Arne
________________________________
From: Arne Roland <A.Roland@index.de>
Sent: Friday, November 27, 2020 12:28:31 AM
To: Pg Hackers
Subject: Rename of triggers for partitioned tables
Hello,
I got too annoyed at building queries for gexec all the time. So wrote a patch to fix the issue that the rename of partitioned trigger doesn't affect children.
In case there isn't a dependent trigger with the same name on the child the function errors out. If the user touched the trigger of the child, it's not a sensitive idea to mess with it.
I'm not happy with the interface change in trigger.h. Yet I don't like the idea of creating another layer of indirection either.
Even though it's a small patch, there might be other things I'm missing, so I'd appreciate feedback.
Regards
Arne
Attachments:
recursive_tgrename.2.patchtext/x-patch; name=recursive_tgrename.2.patchDownload
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index c336b238aa..f4647d38c5 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -1357,12 +1357,7 @@ get_trigger_oid(Oid relid, const char *trigname, bool missing_ok)
return oid;
}
-/*
- * Perform permissions and integrity checks before acquiring a relation lock.
- */
-static void
-RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
- void *arg)
+static void callbackForRenameTrigger(char* relname, Oid relid)
{
HeapTuple tuple;
Form_pg_class form;
@@ -1379,25 +1374,36 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a table, view, or foreign table",
- rv->relname)));
+ relname)));
/* you must own the table to rename one of its triggers */
if (!pg_class_ownercheck(relid, GetUserId()))
- aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), rv->relname);
+ aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), relname);
if (!allowSystemTableMods && IsSystemClass(relid, form))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: \"%s\" is a system catalog",
- rv->relname)));
+ relname)));
ReleaseSysCache(tuple);
}
/*
- * renametrig - changes the name of a trigger on a relation
+ * Perform permissions and integrity checks before acquiring a relation lock.
+ */
+static void
+RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
+ void *arg)
+{
+ callbackForRenameTrigger(rv->relname, relid);
+}
+
+/*
+ * renameonlytrig - changes the name of a trigger on a relation
*
* trigger name is changed in trigger catalog.
* No record of the previous name is kept.
+ * This function assumes that the row is already locked
*
* get proper relrelation from relation catalog (if not arg)
* scan trigger catalog
@@ -1407,41 +1413,26 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
* update row in catalog
*/
ObjectAddress
-renametrig(RenameStmt *stmt)
+renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent)
{
Oid tgoid;
Relation targetrel;
- Relation tgrel;
HeapTuple tuple;
SysScanDesc tgscan;
ScanKeyData key[2];
- Oid relid;
ObjectAddress address;
- /*
- * Look up name, check permissions, and acquire lock (which we will NOT
- * release until end of transaction).
- */
- relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
- 0,
- RangeVarCallbackForRenameTrigger,
- NULL);
/* Have lock already, so just need to build relcache entry. */
targetrel = relation_open(relid, NoLock);
+
/*
* Scan pg_trigger twice for existing triggers on relation. We do this in
* order to ensure a trigger does not exist with newname (The unique index
* on tgrelid/tgname would complain anyway) and to ensure a trigger does
* exist with oldname.
*
- * NOTE that this is cool only because we have AccessExclusiveLock on the
- * relation, so the trigger set won't be changing underneath us.
- */
- tgrel = table_open(TriggerRelationId, RowExclusiveLock);
-
- /*
* First pass -- look for name conflict
*/
ScanKeyInit(&key[0],
@@ -1472,6 +1463,7 @@ renametrig(RenameStmt *stmt)
Anum_pg_trigger_tgname,
BTEqualStrategyNumber, F_NAMEEQ,
PointerGetDatum(stmt->subname));
+
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
NULL, 2, key);
if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
@@ -1484,7 +1476,12 @@ renametrig(RenameStmt *stmt)
tuple = heap_copytuple(tuple); /* need a modifiable copy */
trigform = (Form_pg_trigger) GETSTRUCT(tuple);
tgoid = trigform->oid;
-
+ if (tgparent != 0 && tgparent != trigform->tgparentid) {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("trigger \"%s\" for table \"%s\" is not dependent on the trigger on \"%s\"",
+ stmt->subname, RelationGetRelationName(targetrel), stmt->relation->relname)));
+ }
namestrcpy(&trigform->tgname,
stmt->newname);
@@ -1522,6 +1519,74 @@ renametrig(RenameStmt *stmt)
return address;
}
+/*
+ * renametrig - changes the name of a trigger on a possibly partitioned relation recurisvely
+ *
+ * alter name of parent trigger using renameonlytrig
+ * recursive over children and change their names in the same manner
+ *
+ */
+ObjectAddress
+renametrigHelper (RenameStmt *stmt, Oid tgoid, Oid relid)
+{
+
+ Relation tgrel;
+ Oid newtgoid;
+ ObjectAddress tgaddress;
+ /*
+ * NOTE that this is cool only because we have AccessExclusiveLock on the
+ * relation, so the trigger set won't be changing underneath us.
+ */
+ tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+
+ /*
+ * The last call of renametrig or renametrigHelper already locked this relation.
+ */
+ tgaddress = renameonlytrig(stmt, tgrel, relid, tgoid);
+ newtgoid = tgaddress.objectId;
+
+ /*
+ * In case we have a partioned table, we traverse them and rename every dependend
+ * trigger of the same name or error out, if it doesn't exist.
+ */
+ if (has_subclass(relid))
+ {
+ ListCell *child;
+ List *children;
+
+ /* Make sure it stays a child. */
+ children = find_inheritance_children(relid, AccessExclusiveLock);
+
+ /*
+ * find_inheritance_children doesn't work recursively.
+ * we check every layer individually to ensure that the trigger
+ * of the child is matching in case it's a partitioned table itself.
+ */
+ foreach(child, children)
+ {
+ Oid childrelid = lfirst_oid(child);
+
+ if (childrelid == relid)
+ continue;
+ renametrigHelper(stmt, newtgoid, childrelid);
+ }
+ }
+ return tgaddress;
+}
+
+ObjectAddress
+renametrig(RenameStmt *stmt)
+{
+ /*
+ * Look up name, check permissions, and acquire lock (which we will NOT
+ * release until end of transaction).
+ */
+ return renametrigHelper(stmt, 0,
+ RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
+ 0,
+ RangeVarCallbackForRenameTrigger,
+ NULL));
+}
/*
* EnableDisableTrigger()
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 244540d90b..c13d7bf931 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -158,6 +158,10 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
+extern ObjectAddress renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent);
+
+extern ObjectAddress renametrigHelper(RenameStmt *stmt, Oid tgoid, Oid relid);
+
extern ObjectAddress renametrig(RenameStmt *stmt);
extern void EnableDisableTrigger(Relation rel, const char *tgname,
Hi Arne,
On Fri, Nov 27, 2020 at 9:20 AM Arne Roland <A.Roland@index.de> wrote:
I'm sorry for the spam. Here a patch with updated comments, I missed one before.
You sent in your patch, recursive_tgrename.2.patch to pgsql-hackers on
Nov 27, but you did not post it to the next CommitFest[1]https://commitfest.postgresql.org/31/. If this
was intentional, then you need to take no action. However, if you
want your patch to be reviewed as part of the upcoming CommitFest,
then you need to add it yourself before 2021-01-01 AoE[2]https://en.wikipedia.org/wiki/Anywhere_on_Earth. Thanks for
your contributions.
Regards,
[1]: https://commitfest.postgresql.org/31/
[2]: https://en.wikipedia.org/wiki/Anywhere_on_Earth
Regards,
--
Masahiko Sawada
EnterpriseDB: https://www.enterprisedb.com/
On 2020-Nov-27, Arne Roland wrote:
I got too annoyed at building queries for gexec all the time. So wrote
a patch to fix the issue that the rename of partitioned trigger
doesn't affect children.
As you say, triggers on children don't necessarily have to have the same
name as on parent; this already happens when the trigger is renamed in
the child table but not on parent. In that situation the search on the
child will fail, which will cause the whole thing to fail I think.
We now have the column pg_trigger.tgparentid, and I think it would be
better (more reliable) to search for the trigger in the child by the
tgparentid column instead, rather than by name.
Also, I think it would be good to have
ALTER TRIGGER .. ON ONLY parent RENAME TO ..
to avoid recursing to children. This seems mostly pointless, but since
we've allowed changing the name of the trigger in children thus far,
then we've implicitly made it supported to have triggers that are named
differently. (And it's not entirely academic, since the trigger name
determines firing order.)
Alternatively to this last point, we could decide to disallow renaming
of triggers on children (i.e. if trigger has tgparentid set, then
renaming is disallowed). I don't have a problem with that, but it would
have to be an explicit decision to take.
I think you did not register the patch in commitfest, so I did that for
you: https://commitfest.postgresql.org/32/2943/
Thanks!
--
�lvaro Herrera Valdivia, Chile
"�C�mo puedes confiar en algo que pagas y que no ves,
y no confiar en algo que te dan y te lo muestran?" (Germ�n Poo)
On 1/15/21 5:26 PM, Alvaro Herrera wrote:
On 2020-Nov-27, Arne Roland wrote:
I got too annoyed at building queries for gexec all the time. So wrote
a patch to fix the issue that the rename of partitioned trigger
doesn't affect children.As you say, triggers on children don't necessarily have to have the same
name as on parent; this already happens when the trigger is renamed in
the child table but not on parent. In that situation the search on the
child will fail, which will cause the whole thing to fail I think.We now have the column pg_trigger.tgparentid, and I think it would be
better (more reliable) to search for the trigger in the child by the
tgparentid column instead, rather than by name.Also, I think it would be good to have
ALTER TRIGGER .. ON ONLY parent RENAME TO ..
to avoid recursing to children. This seems mostly pointless, but since
we've allowed changing the name of the trigger in children thus far,
then we've implicitly made it supported to have triggers that are named
differently. (And it's not entirely academic, since the trigger name
determines firing order.)Alternatively to this last point, we could decide to disallow renaming
of triggers on children (i.e. if trigger has tgparentid set, then
renaming is disallowed). I don't have a problem with that, but it would
have to be an explicit decision to take.
Arne, thoughts on Álvaro's comments?
Marking this patch as Waiting for Author.
Regards,
--
-David
david@pgmasters.net
David Steele wrote:
Arne, thoughts on Álvaro's comments?
Marking this patch as Waiting for Author.
Thank you for the reminder. I was to absorbed by other tasks. I should be more present in the upcoming weeks.
I think you did not register the patch in commitfest, so I did that for
you: https://commitfest.postgresql.org/32/2943/
Thank you! I should have done that.
As you say, triggers on children don't necessarily have to have the same
name as on parent; this already happens when the trigger is renamed in
the child table but not on parent. In that situation the search on the
child will fail, which will cause the whole thing to fail I think.We now have the column pg_trigger.tgparentid, and I think it would be
better (more reliable) to search for the trigger in the child by the
tgparentid column instead, rather than by name.
The last patch already checks tgparentid and errors out if either of those do not match.
The reasoning behind that was, that I don't see a clear reasonable way to handle this scenario.
If the user choose to rename the child trigger, I am at a loss what to do.
I thought to pass it back to the user to solve the situation instead of simply ignoring the users earlier rename.
Silently renaming sounded a bit presumptuous to me, even though I don't care that much either way.
Do you think silently renaming is better than yielding an error?
Also, I think it would be good to have
ALTER TRIGGER .. ON ONLY parent RENAME TO ..
to avoid recursing to children. This seems mostly pointless, but since
we've allowed changing the name of the trigger in children thus far,
then we've implicitly made it supported to have triggers that are named
differently. (And it's not entirely academic, since the trigger name
determines firing order.)
If it would be entirely academic, I wouldn't have noticed the whole thing in the first place.
The on only variant sounds like a good idea to me. Even though hardly worth any efford, it seems simple enough. I will work on that.
Alternatively to this last point, we could decide to disallow renaming
of triggers on children (i.e. if trigger has tgparentid set, then
renaming is disallowed). I don't have a problem with that, but it would
have to be an explicit decision to take.
I rejected that idea, because I was unsure what to do with migrations. If we would be discussing this a few years back, this would have been likely my vote, but I don't see it happening now.
Regards
Arne
Attached a rebased patch with the suggested "ONLY ON" change.
Regards
Arne
________________________________
From: Arne Roland <A.Roland@index.de>
Sent: Monday, March 29, 2021 9:37:53 AM
To: David Steele; Alvaro Herrera
Cc: Pg Hackers
Subject: Re: Rename of triggers for partitioned tables
David Steele wrote:
Arne, thoughts on Álvaro's comments?
Marking this patch as Waiting for Author.
Thank you for the reminder. I was to absorbed by other tasks. I should be more present in the upcoming weeks.
I think you did not register the patch in commitfest, so I did that for
you: https://commitfest.postgresql.org/32/2943/
Thank you! I should have done that.
As you say, triggers on children don't necessarily have to have the same
name as on parent; this already happens when the trigger is renamed in
the child table but not on parent. In that situation the search on the
child will fail, which will cause the whole thing to fail I think.We now have the column pg_trigger.tgparentid, and I think it would be
better (more reliable) to search for the trigger in the child by the
tgparentid column instead, rather than by name.
The last patch already checks tgparentid and errors out if either of those do not match.
The reasoning behind that was, that I don't see a clear reasonable way to handle this scenario.
If the user choose to rename the child trigger, I am at a loss what to do.
I thought to pass it back to the user to solve the situation instead of simply ignoring the users earlier rename.
Silently renaming sounded a bit presumptuous to me, even though I don't care that much either way.
Do you think silently renaming is better than yielding an error?
Also, I think it would be good to have
ALTER TRIGGER .. ON ONLY parent RENAME TO ..
to avoid recursing to children. This seems mostly pointless, but since
we've allowed changing the name of the trigger in children thus far,
then we've implicitly made it supported to have triggers that are named
differently. (And it's not entirely academic, since the trigger name
determines firing order.)
If it would be entirely academic, I wouldn't have noticed the whole thing in the first place.
The on only variant sounds like a good idea to me. Even though hardly worth any efford, it seems simple enough. I will work on that.
Alternatively to this last point, we could decide to disallow renaming
of triggers on children (i.e. if trigger has tgparentid set, then
renaming is disallowed). I don't have a problem with that, but it would
have to be an explicit decision to take.
I rejected that idea, because I was unsure what to do with migrations. If we would be discussing this a few years back, this would have been likely my vote, but I don't see it happening now.
Regards
Arne
Attachments:
recursive_tgrename.4.patchtext/x-patch; name=recursive_tgrename.4.patchDownload
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 7383d5994e..14e3edcc5c 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -1348,12 +1348,7 @@ get_trigger_oid(Oid relid, const char *trigname, bool missing_ok)
return oid;
}
-/*
- * Perform permissions and integrity checks before acquiring a relation lock.
- */
-static void
-RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
- void *arg)
+static void callbackForRenameTrigger(char* relname, Oid relid)
{
HeapTuple tuple;
Form_pg_class form;
@@ -1370,25 +1365,36 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a table, view, or foreign table",
- rv->relname)));
+ relname)));
/* you must own the table to rename one of its triggers */
if (!pg_class_ownercheck(relid, GetUserId()))
- aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), rv->relname);
+ aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), relname);
if (!allowSystemTableMods && IsSystemClass(relid, form))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: \"%s\" is a system catalog",
- rv->relname)));
+ relname)));
ReleaseSysCache(tuple);
}
/*
- * renametrig - changes the name of a trigger on a relation
+ * Perform permissions and integrity checks before acquiring a relation lock.
+ */
+static void
+RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
+ void *arg)
+{
+ callbackForRenameTrigger(rv->relname, relid);
+}
+
+/*
+ * renameonlytrig - changes the name of a trigger on a relation
*
* trigger name is changed in trigger catalog.
* No record of the previous name is kept.
+ * This function assumes that the row is already locked
*
* get proper relrelation from relation catalog (if not arg)
* scan trigger catalog
@@ -1398,41 +1404,26 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
* update row in catalog
*/
ObjectAddress
-renametrig(RenameStmt *stmt)
+renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent)
{
Oid tgoid;
Relation targetrel;
- Relation tgrel;
HeapTuple tuple;
SysScanDesc tgscan;
ScanKeyData key[2];
- Oid relid;
ObjectAddress address;
- /*
- * Look up name, check permissions, and acquire lock (which we will NOT
- * release until end of transaction).
- */
- relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
- 0,
- RangeVarCallbackForRenameTrigger,
- NULL);
/* Have lock already, so just need to build relcache entry. */
targetrel = relation_open(relid, NoLock);
+
/*
* Scan pg_trigger twice for existing triggers on relation. We do this in
* order to ensure a trigger does not exist with newname (The unique index
* on tgrelid/tgname would complain anyway) and to ensure a trigger does
* exist with oldname.
*
- * NOTE that this is cool only because we have AccessExclusiveLock on the
- * relation, so the trigger set won't be changing underneath us.
- */
- tgrel = table_open(TriggerRelationId, RowExclusiveLock);
-
- /*
* First pass -- look for name conflict
*/
ScanKeyInit(&key[0],
@@ -1463,6 +1454,7 @@ renametrig(RenameStmt *stmt)
Anum_pg_trigger_tgname,
BTEqualStrategyNumber, F_NAMEEQ,
PointerGetDatum(stmt->subname));
+
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
NULL, 2, key);
if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
@@ -1475,7 +1467,12 @@ renametrig(RenameStmt *stmt)
tuple = heap_copytuple(tuple); /* need a modifiable copy */
trigform = (Form_pg_trigger) GETSTRUCT(tuple);
tgoid = trigform->oid;
-
+ if (tgparent != 0 && tgparent != trigform->tgparentid) {
+ ereport(ERROR,
+ (errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("trigger \"%s\" for table \"%s\" is not dependent on the trigger on \"%s\"",
+ stmt->subname, RelationGetRelationName(targetrel), stmt->relation->relname)));
+ }
namestrcpy(&trigform->tgname,
stmt->newname);
@@ -1513,6 +1510,74 @@ renametrig(RenameStmt *stmt)
return address;
}
+/*
+ * renametrig - changes the name of a trigger on a possibly partitioned relation recurisvely
+ *
+ * alter name of parent trigger using renameonlytrig
+ * recursive over children and change their names in the same manner
+ *
+ */
+ObjectAddress
+renametrigHelper (RenameStmt *stmt, Oid tgoid, Oid relid)
+{
+
+ Relation tgrel;
+ Oid newtgoid;
+ ObjectAddress tgaddress;
+ /*
+ * NOTE that this is cool only because we have AccessExclusiveLock on the
+ * relation, so the trigger set won't be changing underneath us.
+ */
+ tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+
+ /*
+ * The last call of renametrig or renametrigHelper already locked this relation.
+ */
+ tgaddress = renameonlytrig(stmt, tgrel, relid, tgoid);
+ newtgoid = tgaddress.objectId;
+
+ /*
+ * In case we have a partioned table, we traverse them and rename every dependend
+ * trigger of the same name or error out, if it doesn't exist.
+ */
+ if (stmt->relation->inh && has_subclass(relid))
+ {
+ ListCell *child;
+ List *children;
+
+ /* Make sure it stays a child. */
+ children = find_inheritance_children(relid, AccessExclusiveLock, false);
+
+ /*
+ * find_inheritance_children doesn't work recursively.
+ * we check every layer individually to ensure that the trigger
+ * of the child is matching in case it's a partitioned table itself.
+ */
+ foreach(child, children)
+ {
+ Oid childrelid = lfirst_oid(child);
+
+ if (childrelid == relid)
+ continue;
+ renametrigHelper(stmt, newtgoid, childrelid);
+ }
+ }
+ return tgaddress;
+}
+
+ObjectAddress
+renametrig(RenameStmt *stmt)
+{
+ /*
+ * Look up name, check permissions, and acquire lock (which we will NOT
+ * release until end of transaction).
+ */
+ return renametrigHelper(stmt, 0,
+ RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
+ 0,
+ RangeVarCallbackForRenameTrigger,
+ NULL));
+}
/*
* EnableDisableTrigger()
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7ff36bc842..84e1747402 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -8851,7 +8851,7 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
n->missing_ok = false;
$$ = (Node *)n;
}
- | ALTER TRIGGER name ON qualified_name RENAME TO name
+ | ALTER TRIGGER name ON relation_expr RENAME TO name
{
RenameStmt *n = makeNode(RenameStmt);
n->renameType = OBJECT_TRIGGER;
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9e557cfbce..8dac23d980 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -158,6 +158,10 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
+extern ObjectAddress renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent);
+
+extern ObjectAddress renametrigHelper(RenameStmt *stmt, Oid tgoid, Oid relid);
+
extern ObjectAddress renametrig(RenameStmt *stmt);
extern void EnableDisableTrigger(Relation rel, const char *tgname,
On 2021-Mar-29, Arne Roland wrote:
@@ -1475,7 +1467,12 @@ renametrig(RenameStmt *stmt) tuple = heap_copytuple(tuple); /* need a modifiable copy */ trigform = (Form_pg_trigger) GETSTRUCT(tuple); tgoid = trigform->oid; - + if (tgparent != 0 && tgparent != trigform->tgparentid) { + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("trigger \"%s\" for table \"%s\" is not dependent on the trigger on \"%s\"", + stmt->subname, RelationGetRelationName(targetrel), stmt->relation->relname))); + }
I think this is not what we want to do. What you're doing here as I
understand is traversing the inheritance hierarchy down using the
trigger name, and then fail if the trigger with that name has a
different parent or if no trigger with that name exists in the child.
What we really want to have happen, is to search the list of triggers in
the child, see which one has tgparentid=<the one we're renaming>, and
rename that one -- regardless of what name it had originally.
Also, you added grammar support for the ONLY keyword in the command, but
it doesn't do anything different when given that flag, does it? At
least, I see no change or addition to recursion behavior in ATExecCmd.
I would expect that if I say "ALTER TRIGGER .. ON ONLY tab" then it
renames the trigger on table "tab" but not on its child tables; only if
I remove the ONLY from the command it recurses.
I think it would be good to have a new test for pg_dump that verifies
that this stuff is doing the right thing.
--
�lvaro Herrera 39�49'30"S 73�17'W
"Siempre hay que alimentar a los dioses, aunque la tierra est� seca" (Orual)
Thank you for the quick reply!
Alvaro Herrera wrote:
I think this is not what we want to do. What you're doing here as I
understand is traversing the inheritance hierarchy down using the
trigger name, and then fail if the trigger with that name has a
different parent or if no trigger with that name exists in the child.
The basic concept here was to fail in any circumstance where the trigger on the child isn't as expected.
May it be the name or the parent for that matter.
What we really want to have happen, is to search the list of triggers in
the child, see which one has tgparentid=<the one we're renaming>, and
rename that one -- regardless of what name it had originally.
So if we have a trigger named t42 can be renamed to my_trigger by
ALTER TRIGGER sometrigg ON my_table RENAME TO my_trigger;?
Equivalently there is the question whether we just want to silently ignore
if the corresponding child doesn't have a corresponding trigger.
I am not convinced this is really what we want, but I could agree to raise a notice in these cases.
Also, you added grammar support for the ONLY keyword in the command, but
it doesn't do anything different when given that flag, does it? At
least, I see no change or addition to recursion behavior in ATExecCmd.
I would expect that if I say "ALTER TRIGGER .. ON ONLY tab" then it
renames the trigger on table "tab" but not on its child tables; only if
I remove the ONLY from the command it recurses.
The syntax does work via
+ if (stmt->relation->inh && has_subclass(relid))
in renametrigHelper (src/backend/commands/trigger.c line 1543).
I am not sure which sort of addition in ATExecCmd you expected.
Maybe I can make this more obvious one way or the other. You input would be appreciated.
I think it would be good to have a new test for pg_dump that verifies
that this stuff is doing the right thing.
Good idea, I will add a case involving pg_dump.
Regards
Arne
On 2021-Mar-29, Arne Roland wrote:
Alvaro Herrera wrote:
I think this is not what we want to do. What you're doing here as I
understand is traversing the inheritance hierarchy down using the
trigger name, and then fail if the trigger with that name has a
different parent or if no trigger with that name exists in the child.The basic concept here was to fail in any circumstance where the
trigger on the child isn't as expected.
May it be the name or the parent for that matter.
Consider this. You have table p and partition p1. Add trigger t to p,
and do
ALTER TRIGGER t ON p1 RENAME TO q;
What I'm saying is that if you later do
ALTER TRIGGER t ON ONLY p RENAME TO r;
then the trigger on parent is changed, and the trigger on child stays q.
If you later do
ALTER TRIGGER r ON p RENAME TO s;
then triggers on both tables end up with name s.
What we really want to have happen, is to search the list of triggers in
the child, see which one has tgparentid=<the one we're renaming>, and
rename that one -- regardless of what name it had originally.So if we have a trigger named t42 can be renamed to my_trigger by
ALTER TRIGGER sometrigg ON my_table RENAME TO my_trigger;?
Equivalently there is the question whether we just want to silently ignore
if the corresponding child doesn't have a corresponding trigger.
I think the child cannot not have a corresponding trigger. If you try
to drop the trigger on child, it should say that it cannot be dropped
because the trigger on parent requires it. So I don't think there's any
case where altering name of trigger in parent would raise an user-facing
error. If you can't find the trigger in child, that's a case of catalog
corruption and should be reported with something like
elog(ERROR, "cannot find trigger cloned from trigger %s in partition %s")
Also, you added grammar support for the ONLY keyword in the command, but
it doesn't do anything different when given that flag, does it? At
least, I see no change or addition to recursion behavior in ATExecCmd.
I would expect that if I say "ALTER TRIGGER .. ON ONLY tab" then it
renames the trigger on table "tab" but not on its child tables; only if
I remove the ONLY from the command it recurses.The syntax does work via
+ if (stmt->relation->inh && has_subclass(relid))
in renametrigHelper (src/backend/commands/trigger.c line 1543).
I am not sure which sort of addition in ATExecCmd you expected.
Maybe I can make this more obvious one way or the other. You input would be appreciated.
Hmm, ok, maybe this is sufficient, I didn't actually test it. I think
you do need to add a few test cases to ensure everything is sane. Make
sure to verify what happens if you have multi-level partitioning
(grandparent, parent, child) and you ALTER the one in the middle. Also
things like if parent has two children and one child has multiple
children.
Also, please make sure that it works correctly with INHERITS (legacy
inheritance). We probably don't want to cause recursion in that case.
--
�lvaro Herrera 39�49'30"S 73�17'W
Alvaro Herrera wrote:
I think the child cannot not have a corresponding trigger. If you try
to drop the trigger on child, it should say that it cannot be dropped
because the trigger on parent requires it. So I don't think there's any
case where altering name of trigger in parent would raise an user-facing
error. If you can't find the trigger in child, that's a case of catalog
corruption [...]
Ah, I didn't know that. That changes my reasoning about that. I only knew, that the alter table ... detach partition ... doesn't detach the child trigger from the partitioned trigger, but the the attach partition seem to add one. Maybe we should be able to make sure that there is a one to one correspondence for child triggers and child partitions.
Currently upon detaching we keep the trigger within the partitioned trigger of the parent relation. (So the trigger remains in place and can only be dropped with the parent one.) Only we try to attach the partition again any partitioned triggers are simply removed.
One idea would be to detach partitioned triggers there in a similar manner we detach partitioned indexes. That could give above guarantee. (At least if one would be willing to write a migration for that.) But then we can't tell anymore where the trigger stems from. That hence would break our attach/detach workflow.
I was annoyed by the inability to drop partitioned triggers from detached partitions, but it seems I just got a workaround. Ugh.
This is a bit of a sidetrack discussion, but it is related.
Consider this. You have table p and partition p1. Add trigger t to p,
and do
ALTER TRIGGER t ON p1 RENAME TO q;
What I'm saying is that if you later do
ALTER TRIGGER t ON ONLY p RENAME TO r;
then the trigger on parent is changed, and the trigger on child stays q.
If you later do
ALTER TRIGGER r ON p RENAME TO s;
then triggers on both tables end up with name s.
If we get into the mindset of expecting one trigger on the child, we have the following edge case:
- The trigger is renamed. You seem to favor the silent rename in that case over raising a notice (or even an error). I am not convinced a notice wouldn't the better in that case.
- The trigger is outside of partitioned table. That still means that the trigger might still be in the inheritance tree, but likely isn't. Should we rename it anyways, or skip that? Should be raise a notice about what we are doing here? I think that would be helpful to the end user.
Hmm, ok, maybe this is sufficient, I didn't actually test it. I think
you do need to add a few test cases to ensure everything is sane. Make
sure to verify what happens if you have multi-level partitioning
(grandparent, parent, child) and you ALTER the one in the middle. Also
things like if parent has two children and one child has multiple
children.
The test I had somewhere upwards this thread, already tested that.
I am not sure how to add a test for pg_dump though. Can you point me to the location in pg_regress for pg_dump?
Also, please make sure that it works correctly with INHERITS (legacy
inheritance). We probably don't want to cause recursion in that case.
That works, but I will add a test to make sure it stays that way. Thank you for your input!
Regards
Arne
Hi,
attached a patch with some cleanup and a couple of test cases. The lookup is now done with the parentid and the renaming affects detached partitions as well.
The test cases are not perfectly concise, but only add about 0.4 % to the total runtime of regression tests at my machine. So I didn't bother to much. They only consist of basic sql tests.
Further feedback appreciated! Thank you!
Regards
Arne
________________________________
From: Arne Roland <A.Roland@index.de>
Sent: Thursday, April 1, 2021 4:38:59 PM
To: Alvaro Herrera
Cc: David Steele; Pg Hackers
Subject: Re: Rename of triggers for partitioned tables
Alvaro Herrera wrote:
I think the child cannot not have a corresponding trigger. If you try
to drop the trigger on child, it should say that it cannot be dropped
because the trigger on parent requires it. So I don't think there's any
case where altering name of trigger in parent would raise an user-facing
error. If you can't find the trigger in child, that's a case of catalog
corruption [...]
Ah, I didn't know that. That changes my reasoning about that. I only knew, that the alter table ... detach partition ... doesn't detach the child trigger from the partitioned trigger, but the the attach partition seem to add one. Maybe we should be able to make sure that there is a one to one correspondence for child triggers and child partitions.
Currently upon detaching we keep the trigger within the partitioned trigger of the parent relation. (So the trigger remains in place and can only be dropped with the parent one.) Only we try to attach the partition again any partitioned triggers are simply removed.
One idea would be to detach partitioned triggers there in a similar manner we detach partitioned indexes. That could give above guarantee. (At least if one would be willing to write a migration for that.) But then we can't tell anymore where the trigger stems from. That hence would break our attach/detach workflow.
I was annoyed by the inability to drop partitioned triggers from detached partitions, but it seems I just got a workaround. Ugh.
This is a bit of a sidetrack discussion, but it is related.
Consider this. You have table p and partition p1. Add trigger t to p,
and do
ALTER TRIGGER t ON p1 RENAME TO q;
What I'm saying is that if you later do
ALTER TRIGGER t ON ONLY p RENAME TO r;
then the trigger on parent is changed, and the trigger on child stays q.
If you later do
ALTER TRIGGER r ON p RENAME TO s;
then triggers on both tables end up with name s.
If we get into the mindset of expecting one trigger on the child, we have the following edge case:
- The trigger is renamed. You seem to favor the silent rename in that case over raising a notice (or even an error). I am not convinced a notice wouldn't the better in that case.
- The trigger is outside of partitioned table. That still means that the trigger might still be in the inheritance tree, but likely isn't. Should we rename it anyways, or skip that? Should be raise a notice about what we are doing here? I think that would be helpful to the end user.
Hmm, ok, maybe this is sufficient, I didn't actually test it. I think
you do need to add a few test cases to ensure everything is sane. Make
sure to verify what happens if you have multi-level partitioning
(grandparent, parent, child) and you ALTER the one in the middle. Also
things like if parent has two children and one child has multiple
children.
The test I had somewhere upwards this thread, already tested that.
I am not sure how to add a test for pg_dump though. Can you point me to the location in pg_regress for pg_dump?
Also, please make sure that it works correctly with INHERITS (legacy
inheritance). We probably don't want to cause recursion in that case.
That works, but I will add a test to make sure it stays that way. Thank you for your input!
Regards
Arne
Attachments:
recursive_tgrename.5.patchtext/x-patch; name=recursive_tgrename.5.patchDownload
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 7383d5994e..96cefef7c6 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -1326,7 +1326,6 @@ get_trigger_oid(Oid relid, const char *trigname, bool missing_ok)
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
NULL, 2, skey);
-
tup = systable_getnext(tgscan);
if (!HeapTupleIsValid(tup))
@@ -1348,12 +1347,8 @@ get_trigger_oid(Oid relid, const char *trigname, bool missing_ok)
return oid;
}
-/*
- * Perform permissions and integrity checks before acquiring a relation lock.
- */
static void
-RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
- void *arg)
+callbackForRenameTrigger(char *relname, Oid relid)
{
HeapTuple tuple;
Form_pg_class form;
@@ -1370,25 +1365,36 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a table, view, or foreign table",
- rv->relname)));
+ relname)));
/* you must own the table to rename one of its triggers */
if (!pg_class_ownercheck(relid, GetUserId()))
- aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), rv->relname);
+ aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), relname);
if (!allowSystemTableMods && IsSystemClass(relid, form))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: \"%s\" is a system catalog",
- rv->relname)));
+ relname)));
ReleaseSysCache(tuple);
}
/*
- * renametrig - changes the name of a trigger on a relation
+ * Perform permissions and integrity checks before acquiring a relation lock.
+ */
+static void
+RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
+ void *arg)
+{
+ callbackForRenameTrigger(rv->relname, relid);
+}
+
+/*
+ * renameonlytrig - changes the name of a trigger on a relation
*
* trigger name is changed in trigger catalog.
* No record of the previous name is kept.
+ * This function assumes that the row is already locked
*
* get proper relrelation from relation catalog (if not arg)
* scan trigger catalog
@@ -1398,41 +1404,27 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
* update row in catalog
*/
ObjectAddress
-renametrig(RenameStmt *stmt)
+renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent)
{
Oid tgoid;
Relation targetrel;
- Relation tgrel;
HeapTuple tuple;
SysScanDesc tgscan;
ScanKeyData key[2];
- Oid relid;
ObjectAddress address;
+ int matched;
- /*
- * Look up name, check permissions, and acquire lock (which we will NOT
- * release until end of transaction).
- */
- relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
- 0,
- RangeVarCallbackForRenameTrigger,
- NULL);
/* Have lock already, so just need to build relcache entry. */
targetrel = relation_open(relid, NoLock);
+
/*
* Scan pg_trigger twice for existing triggers on relation. We do this in
* order to ensure a trigger does not exist with newname (The unique index
* on tgrelid/tgname would complain anyway) and to ensure a trigger does
* exist with oldname.
*
- * NOTE that this is cool only because we have AccessExclusiveLock on the
- * relation, so the trigger set won't be changing underneath us.
- */
- tgrel = table_open(TriggerRelationId, RowExclusiveLock);
-
- /*
* First pass -- look for name conflict
*/
ScanKeyInit(&key[0],
@@ -1453,34 +1445,48 @@ renametrig(RenameStmt *stmt)
systable_endscan(tgscan);
/*
- * Second pass -- look for trigger existing with oldname and update
+ * Second pass -- look for trigger existing and update if pgparent is
+ * matching
*/
ScanKeyInit(&key[0],
Anum_pg_trigger_tgrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(relid));
- ScanKeyInit(&key[1],
- Anum_pg_trigger_tgname,
- BTEqualStrategyNumber, F_NAMEEQ,
- PointerGetDatum(stmt->subname));
+
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
- NULL, 2, key);
- if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
+ NULL, 1, key);
+ matched = 0;
+ while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
{
Form_pg_trigger trigform;
+ trigform = (Form_pg_trigger) GETSTRUCT(tuple);
+
+ /*
+ * Skip triggers that not relevant. Relevant is the parent trigger as
+ * well as parts of the current distributed trigger.
+ */
+ if (tgparent == 0 && strcmp(stmt->subname, NameStr(trigform->tgname)) || tgparent && trigform->tgparentid != tgparent)
+ continue;
+
+ if (strcmp(stmt->subname, NameStr(trigform->tgname)))
+ {
+ ereport(NOTICE,
+ (errmsg("trigger \"%s\" for table \"%s\" was not named \"%s\", renaming anyways",
+ NameStr(trigform->tgname), RelationGetRelationName(targetrel), stmt->subname)));
+ }
+
/*
* Update pg_trigger tuple with new tgname.
*/
tuple = heap_copytuple(tuple); /* need a modifiable copy */
trigform = (Form_pg_trigger) GETSTRUCT(tuple);
- tgoid = trigform->oid;
-
namestrcpy(&trigform->tgname,
stmt->newname);
-
CatalogTupleUpdate(tgrel, &tuple->t_self, tuple);
+ tgoid = trigform->oid;
+
InvokeObjectPostAlterHook(TriggerRelationId,
tgoid, 0);
@@ -1490,29 +1496,118 @@ renametrig(RenameStmt *stmt)
* entries. (Ideally this should happen automatically...)
*/
CacheInvalidateRelcache(targetrel);
+ matched++;
}
- else
+
+ systable_endscan(tgscan);
+
+ /*
+ * There always should be exactly one matching trigger on the child
+ * partition. If there isn't fail with an error.
+ */
+ if (!matched)
{
ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("trigger \"%s\" for table \"%s\" does not exist",
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("trigger \"%s\" for child table \"%s\" does not exist",
+ stmt->subname, RelationGetRelationName(targetrel))));
+ }
+ if (matched > 1)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("multiple triggers match \"%s\" for table \"%s\", this might indicate a data corruption",
stmt->subname, RelationGetRelationName(targetrel))));
}
ObjectAddressSet(address, TriggerRelationId, tgoid);
- systable_endscan(tgscan);
-
- table_close(tgrel, RowExclusiveLock);
/*
* Close rel, but keep exclusive lock!
*/
+ relation_close(tgrel, NoLock);
relation_close(targetrel, NoLock);
return address;
}
+/*
+ * renametrig - changes the name of a trigger on a possibly partitioned relation recurisvely
+ *
+ * alter name of parent trigger using renameonlytrig
+ * recursive over children and change their names in the same manner
+ *
+ */
+ObjectAddress
+renametrigHelper(RenameStmt *stmt, Oid tgoid, Oid relid)
+{
+
+ Relation tgrel;
+ ObjectAddress tgaddress;
+ HeapTuple tuple;
+ char relkind;
+
+ /*
+ * NOTE that this is cool only because we have AccessExclusiveLock on the
+ * relations, so neither the trigger set nor the table itself will be
+ * changing underneath us.
+ */
+ tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+
+ /*
+ * The last call of renametrig or renametrigHelper already locked this
+ * relation.
+ */
+ tgaddress = renameonlytrig(stmt, tgrel, relid, tgoid);
+
+ /*
+ * In case we have a partioned table, we traverse them and rename every
+ * dependend trigger, unless ON ONLY was specified.
+ */
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ relkind = ((Form_pg_class) GETSTRUCT(tuple))->relkind;
+ ReleaseSysCache(tuple); /* We are still holding the
+ * AccessExclusiveLock. */
+ if (stmt->relation->inh && relkind == RELKIND_PARTITIONED_TABLE && has_subclass(relid))
+ {
+ ListCell *child;
+ List *children;
+
+ /* Make sure it stays a child, even if it's detached. */
+ children = find_inheritance_children(relid, AccessExclusiveLock, true);
+
+ /*
+ * find_inheritance_children doesn't work recursively. we check every
+ * layer individually to ensure that the trigger of the child is
+ * matching in case it's a partitioned table itself.
+ */
+ foreach(child, children)
+ {
+ Oid childrelid = lfirst_oid(child);
+
+ if (childrelid == relid)
+ continue;
+ renametrigHelper(stmt, tgaddress.objectId, childrelid);
+ }
+ }
+ return tgaddress;
+}
+
+ObjectAddress
+renametrig(RenameStmt *stmt)
+{
+ /*
+ * Look up name, check permissions, and acquire lock (which we will NOT
+ * release until end of transaction).
+ */
+ return renametrigHelper(stmt, 0,
+ RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
+ 0,
+ RangeVarCallbackForRenameTrigger,
+ NULL));
+
+}
/*
* EnableDisableTrigger()
@@ -4350,7 +4445,7 @@ GetAfterTriggersStoreSlot(AfterTriggersTableData *table,
/* Create it if not already done. */
if (!table->storeslot)
{
- MemoryContext oldcxt;
+ MemoryContext oldcxt;
/*
* We only need this slot only until AfterTriggerEndQuery, but making
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 7ff36bc842..84e1747402 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -8851,7 +8851,7 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
n->missing_ok = false;
$$ = (Node *)n;
}
- | ALTER TRIGGER name ON qualified_name RENAME TO name
+ | ALTER TRIGGER name ON relation_expr RENAME TO name
{
RenameStmt *n = makeNode(RenameStmt);
n->renameType = OBJECT_TRIGGER;
diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c
index 7c77c338ce..96269fc2ad 100644
--- a/src/backend/utils/adt/ri_triggers.c
+++ b/src/backend/utils/adt/ri_triggers.c
@@ -394,8 +394,8 @@ RI_FKey_check(TriggerData *trigdata)
* Now check that foreign key exists in PK table
*
* XXX detectNewRows must be true when a partitioned table is on the
- * referenced side. The reason is that our snapshot must be fresh
- * in order for the hack in find_inheritance_children() to work.
+ * referenced side. The reason is that our snapshot must be fresh in
+ * order for the hack in find_inheritance_children() to work.
*/
ri_PerformCheck(riinfo, &qkey, qplan,
fk_rel, pk_rel,
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9e557cfbce..8dac23d980 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -158,6 +158,10 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
+extern ObjectAddress renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent);
+
+extern ObjectAddress renametrigHelper(RenameStmt *stmt, Oid tgoid, Oid relid);
+
extern ObjectAddress renametrig(RenameStmt *stmt);
extern void EnableDisableTrigger(Relation rel, const char *tgname,
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index e8af9a9589..0e2e267a05 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -3349,3 +3349,118 @@ for each statement execute function trigger_function1();
delete from convslot_test_parent;
NOTICE: trigger = bdt_trigger, old_table = (111,tutu), (311,tutu)
drop table convslot_test_child, convslot_test_parent;
+--
+-- build a nested partition scheme for testing
+--
+create table grandparent (id int,
+primary key (id))
+partition by range (id);
+create table middle
+partition of grandparent
+for values from (1)
+to (10)
+partition by range (id);
+create table chi
+partition of middle
+for values from (1)
+to (5);
+create table cho
+partition of middle
+for values from (6)
+to (8);
+create function f ()
+returns trigger as
+$$
+begin
+return new;
+end;
+$$
+language plpgsql;
+create trigger a
+after insert on grandparent
+for each row
+execute procedure f();
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+--------+---------------
+ chi | a | a
+ cho | a | a
+ grandparent | a |
+ middle | a | a
+(4 rows)
+
+alter trigger a on grandparent rename to b;
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a', 'b')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+--------+---------------
+ chi | b | b
+ cho | b | b
+ grandparent | b |
+ middle | b | b
+(4 rows)
+
+alter trigger b on only middle rename to something;
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('something', 'b')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+-----------+---------------
+ chi | b | something
+ cho | b | something
+ grandparent | b |
+ middle | something | b
+(4 rows)
+
+alter trigger something on middle rename to some_trigger;
+NOTICE: trigger "b" for table "chi" was not named "something", renaming anyways
+NOTICE: trigger "b" for table "cho" was not named "something", renaming anyways
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('some_trigger', 'b', 'something')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+--------------+---------------
+ grandparent | b |
+ chi | some_trigger | some_trigger
+ cho | some_trigger | some_trigger
+ middle | some_trigger | b
+(4 rows)
+
+--
+-- Check that classical inheritance is unphased by renaming triggers
+--
+create table inh (id int,
+primary key (id));
+alter table middle detach partition chi;
+alter table chi inherit inh;
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+---------+--------+---------------
+(0 rows)
+
+create trigger unrelated
+before update of id on inh
+for each row
+execute procedure f();
+alter trigger unrelated on inh rename to some_trigger;
+alter trigger some_trigger on inh rename to trigger_name;
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('some_trigger', 'unrelated', 'trigger_name')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+---------+--------------+---------------
+ cho | some_trigger | some_trigger
+ middle | some_trigger | b
+ inh | trigger_name |
+(3 rows)
+
+-- cleanup
+drop table grandparent;
+drop table chi;
+drop table inh;
+drop function f();
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index b50f500045..f06586fcae 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -2535,3 +2535,101 @@ for each statement execute function trigger_function1();
delete from convslot_test_parent;
drop table convslot_test_child, convslot_test_parent;
+
+--
+-- build a nested partition scheme for testing
+--
+
+create table grandparent (id int,
+primary key (id))
+partition by range (id);
+
+create table middle
+partition of grandparent
+for values from (1)
+to (10)
+partition by range (id);
+
+create table chi
+partition of middle
+for values from (1)
+to (5);
+
+create table cho
+partition of middle
+for values from (6)
+to (8);
+
+create function f ()
+returns trigger as
+$$
+begin
+return new;
+end;
+$$
+language plpgsql;
+
+create trigger a
+after insert on grandparent
+for each row
+execute procedure f();
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a')
+order by tgname, tgrelid::regclass::text;
+
+alter trigger a on grandparent rename to b;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a', 'b')
+order by tgname, tgrelid::regclass::text;
+
+alter trigger b on only middle rename to something;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('something', 'b')
+order by tgname, tgrelid::regclass::text;
+
+alter trigger something on middle rename to some_trigger;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('some_trigger', 'b', 'something')
+order by tgname, tgrelid::regclass::text;
+
+--
+-- Check that classical inheritance is unphased by renaming triggers
+--
+
+create table inh (id int,
+primary key (id));
+
+alter table middle detach partition chi;
+
+alter table chi inherit inh;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+
+create trigger unrelated
+before update of id on inh
+for each row
+execute procedure f();
+
+alter trigger unrelated on inh rename to some_trigger;
+
+alter trigger some_trigger on inh rename to trigger_name;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('some_trigger', 'unrelated', 'trigger_name')
+order by tgname, tgrelid::regclass::text;
+
+-- cleanup
+
+drop table grandparent;
+
+drop table chi;
+
+drop table inh;
+
+drop function f();
Hello Álvaro Herrera,
I am sorry to bother, but I am still annoyed by this. So I wonder if you had a look.
Regards
Arne
________________________________
From: Arne Roland <A.Roland@index.de>
Sent: Friday, April 2, 2021 7:55:16 PM
To: Alvaro Herrera
Cc: David Steele; Pg Hackers
Subject: Re: Rename of triggers for partitioned tables
Hi,
attached a patch with some cleanup and a couple of test cases. The lookup is now done with the parentid and the renaming affects detached partitions as well.
The test cases are not perfectly concise, but only add about 0.4 % to the total runtime of regression tests at my machine. So I didn't bother to much. They only consist of basic sql tests.
Further feedback appreciated! Thank you!
Regards
Arne
________________________________
From: Arne Roland <A.Roland@index.de>
Sent: Thursday, April 1, 2021 4:38:59 PM
To: Alvaro Herrera
Cc: David Steele; Pg Hackers
Subject: Re: Rename of triggers for partitioned tables
Alvaro Herrera wrote:
I think the child cannot not have a corresponding trigger. If you try
to drop the trigger on child, it should say that it cannot be dropped
because the trigger on parent requires it. So I don't think there's any
case where altering name of trigger in parent would raise an user-facing
error. If you can't find the trigger in child, that's a case of catalog
corruption [...]
Ah, I didn't know that. That changes my reasoning about that. I only knew, that the alter table ... detach partition ... doesn't detach the child trigger from the partitioned trigger, but the the attach partition seem to add one. Maybe we should be able to make sure that there is a one to one correspondence for child triggers and child partitions.
Currently upon detaching we keep the trigger within the partitioned trigger of the parent relation. (So the trigger remains in place and can only be dropped with the parent one.) Only we try to attach the partition again any partitioned triggers are simply removed.
One idea would be to detach partitioned triggers there in a similar manner we detach partitioned indexes. That could give above guarantee. (At least if one would be willing to write a migration for that.) But then we can't tell anymore where the trigger stems from. That hence would break our attach/detach workflow.
I was annoyed by the inability to drop partitioned triggers from detached partitions, but it seems I just got a workaround. Ugh.
This is a bit of a sidetrack discussion, but it is related.
Consider this. You have table p and partition p1. Add trigger t to p,
and do
ALTER TRIGGER t ON p1 RENAME TO q;
What I'm saying is that if you later do
ALTER TRIGGER t ON ONLY p RENAME TO r;
then the trigger on parent is changed, and the trigger on child stays q.
If you later do
ALTER TRIGGER r ON p RENAME TO s;
then triggers on both tables end up with name s.
If we get into the mindset of expecting one trigger on the child, we have the following edge case:
- The trigger is renamed. You seem to favor the silent rename in that case over raising a notice (or even an error). I am not convinced a notice wouldn't the better in that case.
- The trigger is outside of partitioned table. That still means that the trigger might still be in the inheritance tree, but likely isn't. Should we rename it anyways, or skip that? Should be raise a notice about what we are doing here? I think that would be helpful to the end user.
Hmm, ok, maybe this is sufficient, I didn't actually test it. I think
you do need to add a few test cases to ensure everything is sane. Make
sure to verify what happens if you have multi-level partitioning
(grandparent, parent, child) and you ALTER the one in the middle. Also
things like if parent has two children and one child has multiple
children.
The test I had somewhere upwards this thread, already tested that.
I am not sure how to add a test for pg_dump though. Can you point me to the location in pg_regress for pg_dump?
Also, please make sure that it works correctly with INHERITS (legacy
inheritance). We probably don't want to cause recursion in that case.
That works, but I will add a test to make sure it stays that way. Thank you for your input!
Regards
Arne
On 2021-Jun-26, Arne Roland wrote:
Hello �lvaro Herrera,
I am sorry to bother, but I am still annoyed by this. So I wonder if you had a look.
I'll have a look during the commitfest which starts late next week.
(However, I'm fairly sure that it won't be in released versions ...
it'll only be fixed in the version to be released next year, postgres
15. Things that are clear bug fixes can be applied
in released versions, but this patch probably changes behavior in ways
that are not acceptable in released versions. Sorry about that.)
--
�lvaro Herrera 39�49'30"S 73�17'W
Al principio era UNIX, y UNIX habl� y dijo: "Hello world\n".
No dijo "Hello New Jersey\n", ni "Hello USA\n".
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Sent: Saturday, June 26, 2021 18:36
Subject: Re: Rename of triggers for partitioned tables
I'll have a look during the commitfest which starts late next week.
Thank you!
(However, I'm fairly sure that it won't be in released versions ...
it'll only be fixed in the version to be released next year, postgres
15. Things that are clear bug fixes can be applied
in released versions, but this patch probably changes behavior in ways
that are not acceptable in released versions. Sorry about that.)
I never expected any backports. Albeit I don't know anyone, who would mind, I agree with you on that assessment. This is very annoying, but not really breaking the product.
Sure, I was hoping for pg14 initially. But I will survive another year of gexec.
I am grateful you agreed to have a look!
Regards
Arne
On Sat, Jun 26, 2021 at 10:08 AM Arne Roland <A.Roland@index.de> wrote:
*From:* Alvaro Herrera <alvherre@alvh.no-ip.org>
*Sent:* Saturday, June 26, 2021 18:36
*Subject:* Re: Rename of triggers for partitioned tablesI'll have a look during the commitfest which starts late next week.
Thank you!
(However, I'm fairly sure that it won't be in released versions ...
it'll only be fixed in the version to be released next year, postgres
15. Things that are clear bug fixes can be applied
in released versions, but this patch probably changes behavior in ways
that are not acceptable in released versions. Sorry about that.)I never expected any backports. Albeit I don't know anyone, who would
mind, I agree with you on that assessment. This is very annoying, but not
really breaking the product.Sure, I was hoping for pg14 initially. But I will survive another year of
gexec.
I am grateful you agreed to have a look!Regards
ArneHi, Arne:
It seems the patch no longer applies cleanly on master branch.
Do you mind updating the patch ?
Thanks
1 out of 7 hunks FAILED -- saving rejects to file
src/backend/commands/trigger.c.rej
patching file src/backend/parser/gram.y
Hunk #1 succeeded at 8886 (offset 35 lines).
patching file src/backend/utils/adt/ri_triggers.c
Reversed (or previously applied) patch detected! Assume -R? [n]
Apply anyway? [n]
Skipping patch.
1 out of 1 hunk ignored -- saving rejects to file
src/backend/utils/adt/ri_triggers.c.rej
Hi!
From: Zhihong Yu <zyu@yugabyte.com>
Sent: Saturday, June 26, 2021 20:32
Subject: Re: Rename of triggers for partitioned tables
Hi, Arne:
It seems the patch no longer applies cleanly on master branch.
Do you mind updating the patch ?Thanks
Thank you for having a look! Please let me know if the attached rebased patch works for you.
Regards
Arne
Attachments:
recursive_tgrename.6.patchtext/x-patch; name=recursive_tgrename.6.patchDownload
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 07c73f39de..5731c8adcd 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -1325,7 +1325,6 @@ get_trigger_oid(Oid relid, const char *trigname, bool missing_ok)
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
NULL, 2, skey);
-
tup = systable_getnext(tgscan);
if (!HeapTupleIsValid(tup))
@@ -1347,12 +1346,8 @@ get_trigger_oid(Oid relid, const char *trigname, bool missing_ok)
return oid;
}
-/*
- * Perform permissions and integrity checks before acquiring a relation lock.
- */
static void
-RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
- void *arg)
+callbackForRenameTrigger(char *relname, Oid relid)
{
HeapTuple tuple;
Form_pg_class form;
@@ -1369,25 +1364,36 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
ereport(ERROR,
(errcode(ERRCODE_WRONG_OBJECT_TYPE),
errmsg("\"%s\" is not a table, view, or foreign table",
- rv->relname)));
+ relname)));
/* you must own the table to rename one of its triggers */
if (!pg_class_ownercheck(relid, GetUserId()))
- aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), rv->relname);
+ aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(get_rel_relkind(relid)), relname);
if (!allowSystemTableMods && IsSystemClass(relid, form))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied: \"%s\" is a system catalog",
- rv->relname)));
+ relname)));
ReleaseSysCache(tuple);
}
/*
- * renametrig - changes the name of a trigger on a relation
+ * Perform permissions and integrity checks before acquiring a relation lock.
+ */
+static void
+RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
+ void *arg)
+{
+ callbackForRenameTrigger(rv->relname, relid);
+}
+
+/*
+ * renameonlytrig - changes the name of a trigger on a relation
*
* trigger name is changed in trigger catalog.
* No record of the previous name is kept.
+ * This function assumes that the row is already locked
*
* get proper relrelation from relation catalog (if not arg)
* scan trigger catalog
@@ -1397,41 +1403,27 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
* update row in catalog
*/
ObjectAddress
-renametrig(RenameStmt *stmt)
+renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent)
{
Oid tgoid;
Relation targetrel;
- Relation tgrel;
HeapTuple tuple;
SysScanDesc tgscan;
ScanKeyData key[2];
- Oid relid;
ObjectAddress address;
+ int matched;
- /*
- * Look up name, check permissions, and acquire lock (which we will NOT
- * release until end of transaction).
- */
- relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
- 0,
- RangeVarCallbackForRenameTrigger,
- NULL);
/* Have lock already, so just need to build relcache entry. */
targetrel = relation_open(relid, NoLock);
+
/*
* Scan pg_trigger twice for existing triggers on relation. We do this in
* order to ensure a trigger does not exist with newname (The unique index
* on tgrelid/tgname would complain anyway) and to ensure a trigger does
* exist with oldname.
*
- * NOTE that this is cool only because we have AccessExclusiveLock on the
- * relation, so the trigger set won't be changing underneath us.
- */
- tgrel = table_open(TriggerRelationId, RowExclusiveLock);
-
- /*
* First pass -- look for name conflict
*/
ScanKeyInit(&key[0],
@@ -1452,34 +1444,48 @@ renametrig(RenameStmt *stmt)
systable_endscan(tgscan);
/*
- * Second pass -- look for trigger existing with oldname and update
+ * Second pass -- look for trigger existing and update if pgparent is
+ * matching
*/
ScanKeyInit(&key[0],
Anum_pg_trigger_tgrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(relid));
- ScanKeyInit(&key[1],
- Anum_pg_trigger_tgname,
- BTEqualStrategyNumber, F_NAMEEQ,
- PointerGetDatum(stmt->subname));
+
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
- NULL, 2, key);
- if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
+ NULL, 1, key);
+ matched = 0;
+ while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
{
Form_pg_trigger trigform;
+ trigform = (Form_pg_trigger) GETSTRUCT(tuple);
+
+ /*
+ * Skip triggers that not relevant. Relevant is the parent trigger as
+ * well as parts of the current distributed trigger.
+ */
+ if (tgparent == 0 && strcmp(stmt->subname, NameStr(trigform->tgname)) || tgparent && trigform->tgparentid != tgparent)
+ continue;
+
+ if (strcmp(stmt->subname, NameStr(trigform->tgname)))
+ {
+ ereport(NOTICE,
+ (errmsg("trigger \"%s\" for table \"%s\" was not named \"%s\", renaming anyways",
+ NameStr(trigform->tgname), RelationGetRelationName(targetrel), stmt->subname)));
+ }
+
/*
* Update pg_trigger tuple with new tgname.
*/
tuple = heap_copytuple(tuple); /* need a modifiable copy */
trigform = (Form_pg_trigger) GETSTRUCT(tuple);
- tgoid = trigform->oid;
-
namestrcpy(&trigform->tgname,
stmt->newname);
-
CatalogTupleUpdate(tgrel, &tuple->t_self, tuple);
+ tgoid = trigform->oid;
+
InvokeObjectPostAlterHook(TriggerRelationId,
tgoid, 0);
@@ -1489,29 +1495,118 @@ renametrig(RenameStmt *stmt)
* entries. (Ideally this should happen automatically...)
*/
CacheInvalidateRelcache(targetrel);
+ matched++;
}
- else
+
+ systable_endscan(tgscan);
+
+ /*
+ * There always should be exactly one matching trigger on the child
+ * partition. If there isn't fail with an error.
+ */
+ if (!matched)
{
ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("trigger \"%s\" for table \"%s\" does not exist",
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("trigger \"%s\" for child table \"%s\" does not exist",
+ stmt->subname, RelationGetRelationName(targetrel))));
+ }
+ if (matched > 1)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("multiple triggers match \"%s\" for table \"%s\", this might indicate a data corruption",
stmt->subname, RelationGetRelationName(targetrel))));
}
ObjectAddressSet(address, TriggerRelationId, tgoid);
- systable_endscan(tgscan);
-
- table_close(tgrel, RowExclusiveLock);
/*
* Close rel, but keep exclusive lock!
*/
+ relation_close(tgrel, NoLock);
relation_close(targetrel, NoLock);
return address;
}
+/*
+ * renametrig - changes the name of a trigger on a possibly partitioned relation recurisvely
+ *
+ * alter name of parent trigger using renameonlytrig
+ * recursive over children and change their names in the same manner
+ *
+ */
+ObjectAddress
+renametrigHelper(RenameStmt *stmt, Oid tgoid, Oid relid)
+{
+
+ Relation tgrel;
+ ObjectAddress tgaddress;
+ HeapTuple tuple;
+ char relkind;
+
+ /*
+ * NOTE that this is cool only because we have AccessExclusiveLock on the
+ * relations, so neither the trigger set nor the table itself will be
+ * changing underneath us.
+ */
+ tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+
+ /*
+ * The last call of renametrig or renametrigHelper already locked this
+ * relation.
+ */
+ tgaddress = renameonlytrig(stmt, tgrel, relid, tgoid);
+
+ /*
+ * In case we have a partioned table, we traverse them and rename every
+ * dependend trigger, unless ON ONLY was specified.
+ */
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ relkind = ((Form_pg_class) GETSTRUCT(tuple))->relkind;
+ ReleaseSysCache(tuple); /* We are still holding the
+ * AccessExclusiveLock. */
+ if (stmt->relation->inh && relkind == RELKIND_PARTITIONED_TABLE && has_subclass(relid))
+ {
+ ListCell *child;
+ List *children;
+
+ /* Make sure it stays a child, even if it's detached. */
+ children = find_inheritance_children(relid, AccessExclusiveLock, true);
+
+ /*
+ * find_inheritance_children doesn't work recursively. we check every
+ * layer individually to ensure that the trigger of the child is
+ * matching in case it's a partitioned table itself.
+ */
+ foreach(child, children)
+ {
+ Oid childrelid = lfirst_oid(child);
+
+ if (childrelid == relid)
+ continue;
+ renametrigHelper(stmt, tgaddress.objectId, childrelid);
+ }
+ }
+ return tgaddress;
+}
+
+ObjectAddress
+renametrig(RenameStmt *stmt)
+{
+ /*
+ * Look up name, check permissions, and acquire lock (which we will NOT
+ * release until end of transaction).
+ */
+ return renametrigHelper(stmt, 0,
+ RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
+ 0,
+ RangeVarCallbackForRenameTrigger,
+ NULL));
+
+}
/*
* EnableDisableTrigger()
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index eb24195438..c2f6abf55e 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -8886,7 +8886,7 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
n->missing_ok = false;
$$ = (Node *)n;
}
- | ALTER TRIGGER name ON qualified_name RENAME TO name
+ | ALTER TRIGGER name ON relation_expr RENAME TO name
{
RenameStmt *n = makeNode(RenameStmt);
n->renameType = OBJECT_TRIGGER;
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9e557cfbce..8dac23d980 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -158,6 +158,10 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
+extern ObjectAddress renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent);
+
+extern ObjectAddress renametrigHelper(RenameStmt *stmt, Oid tgoid, Oid relid);
+
extern ObjectAddress renametrig(RenameStmt *stmt);
extern void EnableDisableTrigger(Relation rel, const char *tgname,
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index e8af9a9589..0e2e267a05 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -3349,3 +3349,118 @@ for each statement execute function trigger_function1();
delete from convslot_test_parent;
NOTICE: trigger = bdt_trigger, old_table = (111,tutu), (311,tutu)
drop table convslot_test_child, convslot_test_parent;
+--
+-- build a nested partition scheme for testing
+--
+create table grandparent (id int,
+primary key (id))
+partition by range (id);
+create table middle
+partition of grandparent
+for values from (1)
+to (10)
+partition by range (id);
+create table chi
+partition of middle
+for values from (1)
+to (5);
+create table cho
+partition of middle
+for values from (6)
+to (8);
+create function f ()
+returns trigger as
+$$
+begin
+return new;
+end;
+$$
+language plpgsql;
+create trigger a
+after insert on grandparent
+for each row
+execute procedure f();
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+--------+---------------
+ chi | a | a
+ cho | a | a
+ grandparent | a |
+ middle | a | a
+(4 rows)
+
+alter trigger a on grandparent rename to b;
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a', 'b')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+--------+---------------
+ chi | b | b
+ cho | b | b
+ grandparent | b |
+ middle | b | b
+(4 rows)
+
+alter trigger b on only middle rename to something;
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('something', 'b')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+-----------+---------------
+ chi | b | something
+ cho | b | something
+ grandparent | b |
+ middle | something | b
+(4 rows)
+
+alter trigger something on middle rename to some_trigger;
+NOTICE: trigger "b" for table "chi" was not named "something", renaming anyways
+NOTICE: trigger "b" for table "cho" was not named "something", renaming anyways
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('some_trigger', 'b', 'something')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+--------------+---------------
+ grandparent | b |
+ chi | some_trigger | some_trigger
+ cho | some_trigger | some_trigger
+ middle | some_trigger | b
+(4 rows)
+
+--
+-- Check that classical inheritance is unphased by renaming triggers
+--
+create table inh (id int,
+primary key (id));
+alter table middle detach partition chi;
+alter table chi inherit inh;
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+---------+--------+---------------
+(0 rows)
+
+create trigger unrelated
+before update of id on inh
+for each row
+execute procedure f();
+alter trigger unrelated on inh rename to some_trigger;
+alter trigger some_trigger on inh rename to trigger_name;
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('some_trigger', 'unrelated', 'trigger_name')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+---------+--------------+---------------
+ cho | some_trigger | some_trigger
+ middle | some_trigger | b
+ inh | trigger_name |
+(3 rows)
+
+-- cleanup
+drop table grandparent;
+drop table chi;
+drop table inh;
+drop function f();
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index b50f500045..3218b9b36b 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -2535,3 +2535,108 @@ for each statement execute function trigger_function1();
delete from convslot_test_parent;
drop table convslot_test_child, convslot_test_parent;
+
+--
+-- build a nested partition scheme for testing
+--
+
+create table grandparent (id int,
+primary key (id))
+partition by range (id);
+
+create table middle
+partition of grandparent
+for values from (1)
+to (10)
+partition by range (id);
+
+create table chi
+partition of middle
+for values from (1)
+to (5);
+
+create table cho
+partition of middle
+for values from (6)
+to (8);
+
+create function f ()
+returns trigger as
+$$
+begin
+return new;
+end;
+$$
+language plpgsql;
+
+create trigger a
+after insert on grandparent
+for each row
+execute procedure f();
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a')
+order by tgname, tgrelid::regclass::text;
+
+alter trigger a on grandparent rename to b;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a', 'b')
+order by tgname, tgrelid::regclass::text;
+
+alter trigger b on only middle rename to something;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('something', 'b')
+order by tgname, tgrelid::regclass::text;
+
+alter trigger something on middle rename to a_trigger;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a_trigger', 'b', 'something')
+order by tgname, tgrelid::regclass::text;
+
+--
+-- Check that classical inheritance is unphased by renaming triggers
+--
+
+create table inh (id int,
+primary key (id));
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+
+alter table middle detach partition chi;
+
+alter trigger a_trigger on middle rename to some_trigger;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+
+alter table chi inherit inh;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+
+create trigger unrelated
+before update of id on inh
+for each row
+execute procedure f();
+
+alter trigger unrelated on inh rename to some_trigger;
+
+alter trigger some_trigger on inh rename to trigger_name;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('some_trigger', 'unrelated', 'trigger_name')
+order by tgname, tgrelid::regclass::text;
+
+-- cleanup
+
+drop table grandparent;
+drop table chi;
+drop table inh;
+drop function f();
On Mon, Jun 28, 2021 at 11:45 AM Arne Roland <A.Roland@index.de> wrote:
Show quoted text
Hi,
*From:* Zhihong Yu <zyu@yugabyte.com>
*Sent:* Monday, June 28, 2021 15:28
*Subject:* Re: Rename of triggers for partitioned tables- void *arg) +callbackForRenameTrigger(char *relname, Oid relid)Since this method is only called by RangeVarCallbackForRenameTrigger(),
it seems the method can be folded into RangeVarCallbackForRenameTrigger.
that seems obvious. I have no idea why I didn't do that.
+ * renametrig - changes the name of a trigger on a possibly
partitioned relation recurisvely
...
+renametrigHelper(RenameStmt *stmt, Oid tgoid, Oid relid)Comment doesn't match the name of method. I think it is better to use _
instead of camel case.
The other functions in this file seem to be in mixed case (camel case with
lower first character). I don't have a strong point either way, but the
consistency seems to be worth it to me.-renametrig(RenameStmt *stmt) +renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oidtgparent)
Would renametrig_unlocked be better name for the method ?
I think the name renametrig_unlocked might be confusing. I am not sure my
name is good. But upon calling this function everything is locked already
and stays locked. So unlocked seems a confusing name to me.
renameOnlyOneTriggerWithoutAccountingForChildrenWhereAllLocksAreAquiredAlready
sadly isn't very concise anymore. Do you have another idea?strcmp(stmt->subname, NameStr(trigform->tgname)) can be saved in a
variable since it is used after the if statement.
It's always needed later. I did miss it due to the short circuiting
expression. Thanks!+ tgrel = table_open(TriggerRelationId, RowExclusiveLock); ... + ReleaseSysCache(tuple); /* We are still holding the + * AccessExclusiveLock. */The lock mode in comment doesn't seem to match the lock taken.
I made the comment slightly more verbose. I hope it's clear now.
Thank you very much for the feedback! I attached a new version of the
patch based on your suggestions.Regards
ArneAdding mailing list.
Import Notes
Reply to msg id not found: 9f756493aa624b31aca14b68ba22fe39@index.de
On Mon, Jun 28, 2021 at 3:46 PM Arne Roland <A.Roland@index.de> wrote:
Hi!
From: Zhihong Yu <zyu@yugabyte.com>
Sent: Saturday, June 26, 2021 20:32
Subject: Re: Rename of triggers for partitioned tablesHi, Arne:
It seems the patch no longer applies cleanly on master branch.
Do you mind updating the patch ?Thanks
Thank you for having a look! Please let me know if the attached rebased patch works for you.
The patch does not apply on Head anymore, could you rebase and post a
patch. I'm changing the status to "Waiting for Author".
Regards,
Vignesh
Hello Vignesh,
thank you for your interest!
From: vignesh C <vignesh21@gmail.com>
Sent: Thursday, July 15, 2021 14:08
To: Arne Roland
Cc: Zhihong Yu; Alvaro Herrera; Pg Hackers
Subject: Re: Rename of triggers for partitioned tables
The patch does not apply on Head anymore, could you rebase and post a
patch. I'm changing the status to "Waiting for Author".
Please let me know, whether the attached patch works for you.
Regards
Arne
Attachments:
recursive_tgrename.07.patchtext/x-patch; name=recursive_tgrename.07.patchDownload
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 952c8d582a..e02da5e52f 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -1327,7 +1327,6 @@ get_trigger_oid(Oid relid, const char *trigname, bool missing_ok)
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
NULL, 2, skey);
-
tup = systable_getnext(tgscan);
if (!HeapTupleIsValid(tup))
@@ -1387,10 +1386,11 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
}
/*
- * renametrig - changes the name of a trigger on a relation
+ * renameonlytrig - changes the name of a trigger on a relation
*
* trigger name is changed in trigger catalog.
* No record of the previous name is kept.
+ * This function assumes that the row is already locked.
*
* get proper relrelation from relation catalog (if not arg)
* scan trigger catalog
@@ -1400,41 +1400,28 @@ RangeVarCallbackForRenameTrigger(const RangeVar *rv, Oid relid, Oid oldrelid,
* update row in catalog
*/
ObjectAddress
-renametrig(RenameStmt *stmt)
+renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent)
{
Oid tgoid;
Relation targetrel;
- Relation tgrel;
HeapTuple tuple;
SysScanDesc tgscan;
ScanKeyData key[2];
- Oid relid;
ObjectAddress address;
+ int matched;
+ int stmtTgnameMatches;
- /*
- * Look up name, check permissions, and acquire lock (which we will NOT
- * release until end of transaction).
- */
- relid = RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
- 0,
- RangeVarCallbackForRenameTrigger,
- NULL);
/* Have lock already, so just need to build relcache entry. */
targetrel = relation_open(relid, NoLock);
+
/*
* Scan pg_trigger twice for existing triggers on relation. We do this in
* order to ensure a trigger does not exist with newname (The unique index
* on tgrelid/tgname would complain anyway) and to ensure a trigger does
* exist with oldname.
*
- * NOTE that this is cool only because we have AccessExclusiveLock on the
- * relation, so the trigger set won't be changing underneath us.
- */
- tgrel = table_open(TriggerRelationId, RowExclusiveLock);
-
- /*
* First pass -- look for name conflict
*/
ScanKeyInit(&key[0],
@@ -1455,34 +1442,50 @@ renametrig(RenameStmt *stmt)
systable_endscan(tgscan);
/*
- * Second pass -- look for trigger existing with oldname and update
+ * Second pass -- look for trigger existing and update if pgparent is
+ * matching
*/
ScanKeyInit(&key[0],
Anum_pg_trigger_tgrelid,
BTEqualStrategyNumber, F_OIDEQ,
ObjectIdGetDatum(relid));
- ScanKeyInit(&key[1],
- Anum_pg_trigger_tgname,
- BTEqualStrategyNumber, F_NAMEEQ,
- PointerGetDatum(stmt->subname));
+
tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
- NULL, 2, key);
- if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
+ NULL, 1, key);
+ matched = 0;
+ while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
{
Form_pg_trigger trigform;
+ trigform = (Form_pg_trigger) GETSTRUCT(tuple);
+
+ stmtTgnameMatches = strcmp(stmt->subname, NameStr(trigform->tgname));
+ /*
+ * Skip triggers that not relevant. Relevant is the parent trigger as
+ * well as parts of the current distributed trigger.
+ */
+ if (tgparent == 0 && stmtTgnameMatches
+ || tgparent && trigform->tgparentid != tgparent)
+ continue;
+
+ if (stmtTgnameMatches)
+ {
+ ereport(NOTICE,
+ (errmsg("trigger \"%s\" for table \"%s\" was not named \"%s\", renaming anyways",
+ NameStr(trigform->tgname), RelationGetRelationName(targetrel), stmt->subname)));
+ }
+
/*
* Update pg_trigger tuple with new tgname.
*/
tuple = heap_copytuple(tuple); /* need a modifiable copy */
trigform = (Form_pg_trigger) GETSTRUCT(tuple);
- tgoid = trigform->oid;
-
namestrcpy(&trigform->tgname,
stmt->newname);
-
CatalogTupleUpdate(tgrel, &tuple->t_self, tuple);
+ tgoid = trigform->oid;
+
InvokeObjectPostAlterHook(TriggerRelationId,
tgoid, 0);
@@ -1492,29 +1495,122 @@ renametrig(RenameStmt *stmt)
* entries. (Ideally this should happen automatically...)
*/
CacheInvalidateRelcache(targetrel);
+ matched++;
}
- else
+
+ systable_endscan(tgscan);
+
+ /*
+ * There always should be exactly one matching trigger on the child
+ * partition. If there isn't fail with an error.
+ */
+ if (!matched)
{
ereport(ERROR,
- (errcode(ERRCODE_UNDEFINED_OBJECT),
- errmsg("trigger \"%s\" for table \"%s\" does not exist",
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("trigger \"%s\" for child table \"%s\" does not exist",
+ stmt->subname, RelationGetRelationName(targetrel))));
+ }
+ if (matched > 1)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_CORRUPTED),
+ errmsg("multiple triggers match \"%s\" for table \"%s\", this might indicate a data corruption",
stmt->subname, RelationGetRelationName(targetrel))));
}
ObjectAddressSet(address, TriggerRelationId, tgoid);
- systable_endscan(tgscan);
-
- table_close(tgrel, RowExclusiveLock);
/*
* Close rel, but keep exclusive lock!
*/
+ relation_close(tgrel, NoLock);
relation_close(targetrel, NoLock);
return address;
}
+/*
+ * renametrigHelper - changes the name of a trigger on a possibly partitioned relation recursively
+ *
+ * alter name of parent trigger using renameonlytrig
+ * recursive over children and change their names in the same manner
+ *
+ */
+ObjectAddress
+renametrigHelper(RenameStmt *stmt, Oid tgoid, Oid relid)
+{
+
+ Relation tgrel;
+ ObjectAddress tgaddress;
+ HeapTuple tuple;
+ char relkind;
+
+ /*
+ * NOTE that this is cool only because we have AccessExclusiveLock on the
+ * relations, so neither the trigger set nor the table itself will be
+ * changing underneath us.
+ */
+ tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+
+ /*
+ * The last call of renametrig or renametrigHelper already locked this
+ * relation.
+ */
+ tgaddress = renameonlytrig(stmt, tgrel, relid, tgoid);
+
+ /*
+ * In case we have a partioned table, we traverse them and rename every
+ * dependend trigger, unless ON ONLY was specified.
+ */
+ tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relid));
+ relkind = ((Form_pg_class) GETSTRUCT(tuple))->relkind;
+ ReleaseSysCache(tuple); /* We are still holding the
+ * AccessExclusiveLock on the table of the trigger. */
+ if (stmt->relation->inh && relkind == RELKIND_PARTITIONED_TABLE && has_subclass(relid))
+ {
+ ListCell *child;
+ List *children;
+
+ /*
+ * Make sure it stays a child. We assume that there are no parts of
+ * the trigger in detached partitions, since they should have been
+ * deleted in that case.
+ */
+ children = find_inheritance_children(relid, AccessExclusiveLock);
+
+ /*
+ * find_inheritance_children doesn't work recursively. we check every
+ * layer individually to ensure that the trigger of the child is
+ * matching in case it's a partitioned table itself.
+ */
+ foreach(child, children)
+ {
+ Oid childrelid = lfirst_oid(child);
+
+ if (childrelid == relid)
+ continue;
+ renametrigHelper(stmt, tgaddress.objectId, childrelid);
+ }
+ }
+ return tgaddress;
+}
+
+ObjectAddress
+renametrig(RenameStmt *stmt)
+{
+ /*
+ * Look up name, check permissions, and acquire lock (which we will NOT
+ * release until end of transaction).
+ */
+ return renametrigHelper(stmt, 0,
+ RangeVarGetRelidExtended(stmt->relation, AccessExclusiveLock,
+ 0,
+ RangeVarCallbackForRenameTrigger,
+ NULL));
+
+}
/*
* EnableDisableTrigger()
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 10da5c5c51..26433c529f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -8886,7 +8886,7 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
n->missing_ok = false;
$$ = (Node *)n;
}
- | ALTER TRIGGER name ON qualified_name RENAME TO name
+ | ALTER TRIGGER name ON relation_expr RENAME TO name
{
RenameStmt *n = makeNode(RenameStmt);
n->renameType = OBJECT_TRIGGER;
diff --git a/src/include/commands/trigger.h b/src/include/commands/trigger.h
index 9e557cfbce..8dac23d980 100644
--- a/src/include/commands/trigger.h
+++ b/src/include/commands/trigger.h
@@ -158,6 +158,10 @@ extern ObjectAddress CreateTrigger(CreateTrigStmt *stmt, const char *queryString
extern void RemoveTriggerById(Oid trigOid);
extern Oid get_trigger_oid(Oid relid, const char *name, bool missing_ok);
+extern ObjectAddress renameonlytrig(RenameStmt *stmt, Relation tgrel, Oid relid, Oid tgparent);
+
+extern ObjectAddress renametrigHelper(RenameStmt *stmt, Oid tgoid, Oid relid);
+
extern ObjectAddress renametrig(RenameStmt *stmt);
extern void EnableDisableTrigger(Relation rel, const char *tgname,
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index e8af9a9589..e004794ae2 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -3349,3 +3349,134 @@ for each statement execute function trigger_function1();
delete from convslot_test_parent;
NOTICE: trigger = bdt_trigger, old_table = (111,tutu), (311,tutu)
drop table convslot_test_child, convslot_test_parent;
+--
+-- build a nested partition scheme for testing
+--
+create table grandparent (id int,
+primary key (id))
+partition by range (id);
+create table middle
+partition of grandparent
+for values from (1)
+to (10)
+partition by range (id);
+create table chi
+partition of middle
+for values from (1)
+to (5);
+create table cho
+partition of middle
+for values from (6)
+to (8);
+create function f ()
+returns trigger as
+$$
+begin
+return new;
+end;
+$$
+language plpgsql;
+create trigger a
+after insert on grandparent
+for each row
+execute procedure f();
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+--------+---------------
+ chi | a | a
+ cho | a | a
+ grandparent | a |
+ middle | a | a
+(4 rows)
+
+alter trigger a on grandparent rename to b;
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a', 'b')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+--------+---------------
+ chi | b | b
+ cho | b | b
+ grandparent | b |
+ middle | b | b
+(4 rows)
+
+alter trigger b on only middle rename to something;
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('something', 'b')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+-----------+---------------
+ chi | b | something
+ cho | b | something
+ grandparent | b |
+ middle | something | b
+(4 rows)
+
+alter trigger something on middle rename to a_trigger;
+NOTICE: trigger "b" for table "chi" was not named "something", renaming anyways
+NOTICE: trigger "b" for table "cho" was not named "something", renaming anyways
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a_trigger', 'b', 'something')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+-----------+---------------
+ chi | a_trigger | a_trigger
+ cho | a_trigger | a_trigger
+ middle | a_trigger | b
+ grandparent | b |
+(4 rows)
+
+--
+-- Check that classical inheritance is unphased by renaming triggers
+--
+create table inh (id int,
+primary key (id));
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+---------+-----------+---------------
+ chi | a_trigger | a_trigger
+(1 row)
+
+alter table middle detach partition chi;
+alter trigger a_trigger on middle rename to some_trigger;
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+---------+--------+---------------
+(0 rows)
+
+alter table chi inherit inh;
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+---------+--------+---------------
+(0 rows)
+
+create trigger unrelated
+before update of id on inh
+for each row
+execute procedure f();
+alter trigger unrelated on inh rename to some_trigger;
+alter trigger some_trigger on inh rename to trigger_name;
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('some_trigger', 'unrelated', 'trigger_name')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+---------+--------------+---------------
+ cho | some_trigger | some_trigger
+ middle | some_trigger | b
+ inh | trigger_name |
+(3 rows)
+
+-- cleanup
+drop table grandparent;
+drop table chi;
+drop table inh;
+drop function f();
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index b50f500045..3218b9b36b 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -2535,3 +2535,108 @@ for each statement execute function trigger_function1();
delete from convslot_test_parent;
drop table convslot_test_child, convslot_test_parent;
+
+--
+-- build a nested partition scheme for testing
+--
+
+create table grandparent (id int,
+primary key (id))
+partition by range (id);
+
+create table middle
+partition of grandparent
+for values from (1)
+to (10)
+partition by range (id);
+
+create table chi
+partition of middle
+for values from (1)
+to (5);
+
+create table cho
+partition of middle
+for values from (6)
+to (8);
+
+create function f ()
+returns trigger as
+$$
+begin
+return new;
+end;
+$$
+language plpgsql;
+
+create trigger a
+after insert on grandparent
+for each row
+execute procedure f();
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a')
+order by tgname, tgrelid::regclass::text;
+
+alter trigger a on grandparent rename to b;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a', 'b')
+order by tgname, tgrelid::regclass::text;
+
+alter trigger b on only middle rename to something;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('something', 'b')
+order by tgname, tgrelid::regclass::text;
+
+alter trigger something on middle rename to a_trigger;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a_trigger', 'b', 'something')
+order by tgname, tgrelid::regclass::text;
+
+--
+-- Check that classical inheritance is unphased by renaming triggers
+--
+
+create table inh (id int,
+primary key (id));
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+
+alter table middle detach partition chi;
+
+alter trigger a_trigger on middle rename to some_trigger;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+
+alter table chi inherit inh;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+
+create trigger unrelated
+before update of id on inh
+for each row
+execute procedure f();
+
+alter trigger unrelated on inh rename to some_trigger;
+
+alter trigger some_trigger on inh rename to trigger_name;
+
+select tgrelid::regclass, tgname, (select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('some_trigger', 'unrelated', 'trigger_name')
+order by tgname, tgrelid::regclass::text;
+
+-- cleanup
+
+drop table grandparent;
+drop table chi;
+drop table inh;
+drop function f();
I found this coding too convoluted, so I rewrote it in a different way.
You tests pass with this, but I admit I haven't double-checked them yet;
I'll do that next.
I don't think we need to give a NOTICE when the trigger name does not
match; it doesn't really matter that the trigger was named differently
before the command, does it?
--
Álvaro Herrera 39°49'30"S 73°17'W — https://www.EnterpriseDB.com/
Attachments:
renametrig-8.patchtext/x-diff; charset=utf-8Download
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 6d4b7ee92a..da6320f6a1 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -71,6 +71,10 @@ int SessionReplicationRole = SESSION_REPLICATION_ROLE_ORIGIN;
static int MyTriggerDepth = 0;
/* Local function prototypes */
+static void renametrig_internal(Relation tgrel, Relation targetrel,
+ HeapTuple trigtup, const char *newname);
+static void renametrig_partition(Relation tgrel, Oid partitionId,
+ Oid parentTriggerOid, const char *newname);
static void SetTriggerFlags(TriggerDesc *trigdesc, Trigger *trigger);
static bool GetTupleForTrigger(EState *estate,
EPQState *epqstate,
@@ -1441,6 +1445,14 @@ renametrig(RenameStmt *stmt)
/* Have lock already, so just need to build relcache entry. */
targetrel = relation_open(relid, NoLock);
+ /*
+ * On partitioned tables, this operation recurses to partitions, unless
+ * caller requested not to. Lock all tables upfront, if needed.
+ */
+ if (stmt->relation->inh &&
+ targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ (void) find_all_inheritors(relid, AccessExclusiveLock, NULL);
+
/*
* Scan pg_trigger twice for existing triggers on relation. We do this in
* order to ensure a trigger does not exist with newname (The unique index
@@ -1489,27 +1501,25 @@ renametrig(RenameStmt *stmt)
{
Form_pg_trigger trigform;
- /*
- * Update pg_trigger tuple with new tgname.
- */
- tuple = heap_copytuple(tuple); /* need a modifiable copy */
trigform = (Form_pg_trigger) GETSTRUCT(tuple);
tgoid = trigform->oid;
- namestrcpy(&trigform->tgname,
- stmt->newname);
+ /* Rename the trigger on this relation ... */
+ renametrig_internal(tgrel, targetrel, tuple, stmt->newname);
- CatalogTupleUpdate(tgrel, &tuple->t_self, tuple);
+ /* ... and if it is partitioned, recurse to its partitions */
+ if (stmt->relation->inh &&
+ targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ PartitionDesc partdesc = RelationGetPartitionDesc(targetrel, true);
- InvokeObjectPostAlterHook(TriggerRelationId,
- tgoid, 0);
+ for (int i = 0; i < partdesc->nparts; i++)
+ {
+ Oid partitionId = partdesc->oids[i];
- /*
- * Invalidate relation's relcache entry so that other backends (and
- * this one too!) are sent SI message to make them rebuild relcache
- * entries. (Ideally this should happen automatically...)
- */
- CacheInvalidateRelcache(targetrel);
+ renametrig_partition(tgrel, partitionId, trigform->oid, stmt->newname);
+ }
+ }
}
else
{
@@ -1533,6 +1543,92 @@ renametrig(RenameStmt *stmt)
return address;
}
+/*
+ * Subroutine for renametrig -- perform the actual work of renaming one
+ * trigger on one table.
+ */
+static void
+renametrig_internal(Relation tgrel, Relation targetrel, HeapTuple trigtup,
+ const char *newname)
+{
+ HeapTuple tuple;
+ Form_pg_trigger newtgform;
+
+ /*
+ * Update pg_trigger tuple with new tgname.
+ */
+ tuple = heap_copytuple(trigtup); /* need a modifiable copy */
+ newtgform = (Form_pg_trigger) GETSTRUCT(tuple);
+
+ namestrcpy(&newtgform->tgname, newname);
+
+ CatalogTupleUpdate(tgrel, &tuple->t_self, tuple);
+
+ InvokeObjectPostAlterHook(TriggerRelationId, newtgform->oid, 0);
+
+ /*
+ * Invalidate relation's relcache entry so that other backends (and
+ * this one too!) are sent SI message to make them rebuild relcache
+ * entries. (Ideally this should happen automatically...)
+ */
+ CacheInvalidateRelcache(targetrel);
+}
+
+/*
+ * Subroutine for renametrig -- Helper for recursing to partitions when
+ * renaming triggers on a partitioned table.
+ */
+static void
+renametrig_partition(Relation tgrel, Oid partitionId, Oid parentTriggerOid,
+ const char *newname)
+{
+ SysScanDesc tgscan;
+ ScanKeyData key;
+ HeapTuple tuple;
+ int found = 0 PG_USED_FOR_ASSERTS_ONLY;
+
+ /*
+ * Given a relation and the OID of a trigger on parent relation, find the
+ * corresponding trigger in the child and rename that trigger to the given
+ * name.
+ */
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(partitionId));
+ tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
+ NULL, 1, &key);
+ while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tuple);
+ Relation partitionRel;
+
+ if (tgform->tgparentid != parentTriggerOid)
+ continue; /* not our trigger */
+
+ Assert(found++ <= 0);
+
+ partitionRel = table_open(partitionId, NoLock);
+
+ /* Rename the trigger on this partition */
+ renametrig_internal(tgrel, partitionRel, tuple, newname);
+
+ /* And if this relation is partitioned, recurse to its partitions */
+ if (partitionRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ PartitionDesc partdesc = RelationGetPartitionDesc(partitionRel, true);
+
+ for (int i = 0; i < partdesc->nparts; i++)
+ {
+ Oid partitionId = partdesc->oids[i];
+
+ renametrig_partition(tgrel, partitionId, tgform->oid, newname);
+ }
+ }
+ table_close(partitionRel, NoLock);
+ }
+ systable_endscan(tgscan);
+}
/*
* EnableDisableTrigger()
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 10da5c5c51..26433c529f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -8886,7 +8886,7 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
n->missing_ok = false;
$$ = (Node *)n;
}
- | ALTER TRIGGER name ON qualified_name RENAME TO name
+ | ALTER TRIGGER name ON relation_expr RENAME TO name
{
RenameStmt *n = makeNode(RenameStmt);
n->renameType = OBJECT_TRIGGER;
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 5254447cf8..0380257aee 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -3410,3 +3410,122 @@ for each statement execute function trigger_function1();
delete from convslot_test_parent;
NOTICE: trigger = bdt_trigger, old_table = (111,tutu), (311,tutu)
drop table convslot_test_child, convslot_test_parent;
+--
+-- build a nested partition scheme for testing
+--
+create table grandparent (id int,
+primary key (id))
+partition by range (id);
+create table middle partition of grandparent for values from (1) to (10)
+partition by range (id);
+create table chi partition of middle for values from (1) to (5);
+create table cho partition of middle for values from (6) to (8);
+create function f () returns trigger as
+$$ begin return new; end; $$
+language plpgsql;
+create trigger a after insert on grandparent
+for each row execute procedure f();
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+--------+---------------
+ chi | a | a
+ cho | a | a
+ grandparent | a |
+ middle | a | a
+(4 rows)
+
+alter trigger a on grandparent rename to b;
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a', 'b')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+--------+---------------
+ chi | b | b
+ cho | b | b
+ grandparent | b |
+ middle | b | b
+(4 rows)
+
+alter trigger b on only middle rename to something;
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('something', 'b')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+-----------+---------------
+ chi | b | something
+ cho | b | something
+ grandparent | b |
+ middle | something | b
+(4 rows)
+
+alter trigger something on middle rename to a_trigger;
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a_trigger', 'b', 'something')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+-----------+---------------
+ chi | a_trigger | a_trigger
+ cho | a_trigger | a_trigger
+ middle | a_trigger | b
+ grandparent | b |
+(4 rows)
+
+--
+-- Check that classical inheritance is unphased by renaming triggers
+--
+create table inh (id int,
+primary key (id));
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+---------+-----------+---------------
+ chi | a_trigger | a_trigger
+(1 row)
+
+alter table middle detach partition chi;
+alter trigger a_trigger on middle rename to some_trigger;
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+---------+--------+---------------
+(0 rows)
+
+alter table chi inherit inh;
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+---------+--------+---------------
+(0 rows)
+
+create trigger unrelated before update of id on inh
+for each row execute procedure f();
+alter trigger unrelated on inh rename to some_trigger;
+alter trigger some_trigger on inh rename to trigger_name;
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('some_trigger', 'unrelated', 'trigger_name')
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+---------+--------------+---------------
+ cho | some_trigger | some_trigger
+ middle | some_trigger | b
+ inh | trigger_name |
+(3 rows)
+
+-- cleanup
+drop table grandparent;
+drop table chi;
+drop table inh;
+drop function f();
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 7b73ee20a1..5be330508e 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -2572,3 +2572,96 @@ for each statement execute function trigger_function1();
delete from convslot_test_parent;
drop table convslot_test_child, convslot_test_parent;
+
+--
+-- build a nested partition scheme for testing
+--
+create table grandparent (id int,
+primary key (id))
+partition by range (id);
+
+create table middle partition of grandparent for values from (1) to (10)
+partition by range (id);
+
+create table chi partition of middle for values from (1) to (5);
+create table cho partition of middle for values from (6) to (8);
+
+create function f () returns trigger as
+$$ begin return new; end; $$
+language plpgsql;
+
+create trigger a after insert on grandparent
+for each row execute procedure f();
+
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a')
+order by tgname, tgrelid::regclass::text;
+
+alter trigger a on grandparent rename to b;
+
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a', 'b')
+order by tgname, tgrelid::regclass::text;
+
+alter trigger b on only middle rename to something;
+
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('something', 'b')
+order by tgname, tgrelid::regclass::text;
+
+alter trigger something on middle rename to a_trigger;
+
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('a_trigger', 'b', 'something')
+order by tgname, tgrelid::regclass::text;
+
+--
+-- Check that classical inheritance is unphased by renaming triggers
+--
+
+create table inh (id int,
+primary key (id));
+
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+
+alter table middle detach partition chi;
+
+alter trigger a_trigger on middle rename to some_trigger;
+
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+
+alter table chi inherit inh;
+
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid = 'chi'::regclass
+order by tgname, tgrelid::regclass::text;
+
+create trigger unrelated before update of id on inh
+for each row execute procedure f();
+
+alter trigger unrelated on inh rename to some_trigger;
+
+alter trigger some_trigger on inh rename to trigger_name;
+
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgname in ('some_trigger', 'unrelated', 'trigger_name')
+order by tgname, tgrelid::regclass::text;
+
+-- cleanup
+
+drop table grandparent;
+drop table chi;
+drop table inh;
+drop function f();
Hi!
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Sent: Tuesday, July 20, 2021 02:03
To: Arne Roland
Cc: vignesh C; Zhihong Yu; Pg Hackers
Subject: Re: Rename of triggers for partitioned tables
I found this coding too convoluted, so I rewrote it in a different way.
You tests pass with this, but I admit I haven't double-checked them yet;
I'll do that next.
Is your patch based on master? It doesn't apply at my end.
I don't think we need to give a NOTICE when the trigger name does not
match; it doesn't really matter that the trigger was named differently
before the command, does it?
I'd expect the command
ALTER TRIGGER name ON table_name RENAME TO new_name;
to rename a trigger named "name". We are referring the trigger via it's name after all. If a child is named differently we break with that assumption. I think informing the user about that, is very valuable.
Regards
Arne
We are referring the trigger via it's name after all. If a child is named differently we break with that assumption. I think informing the user about that, is very valuable.
To elaborate on that:
While we to similar things for things like set schema, here it has a functional relevance.
ALTER TRIGGER a5 ON table_name RENAME TO a9;
suggests that the trigger is now fired later from now on. Which obviously might not be the case, if one of the child triggers have had a different name.
I like your naming suggestions. I'm looking forward to test this patch when it applies at my end!
Regards
Arne
On 2021-Jul-20, Arne Roland wrote:
Is your patch based on master? It doesn't apply at my end.
It does ... master being dd498998a3 here,
$ patch -p1 < /tmp/renametrig-8.patch
patching file src/backend/commands/trigger.c
patching file src/backend/parser/gram.y
patching file src/test/regress/expected/triggers.out
patching file src/test/regress/sql/triggers.sql
applies fine.
I don't think we need to give a NOTICE when the trigger name does not
match; it doesn't really matter that the trigger was named differently
before the command, does it?
I'd expect the command
ALTER TRIGGER name ON table_name RENAME TO new_name;
to rename a trigger named "name". We are referring the trigger via it's name after all. If a child is named differently we break with that assumption. I think informing the user about that, is very valuable.
Well, it does rename a trigger named 'name' on the table 'table_name',
as well as all its descendant triggers. I guess I am surprised that
anybody would rename a descendant trigger in the first place. I'm not
wedded to the decision of removing the NOTICE, though ... are there any
other votes for that, anyone?
Thanks
--
Álvaro Herrera 39°49'30"S 73°17'W — https://www.EnterpriseDB.com/
Thank you! I get a compile time error
gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Werror=vla -Wendif-labels -Wmissing-format-attribute -Wimplicit-fallthrough=3 -Wformat-security -fno-strict-aliasing -fwrapv -fexcess-precision=standard -Wno-format-truncation -O2 -I../../../src/include -D_GNU_SOURCE -c -o trigger.o trigger.c
In file included from ../../../src/include/postgres.h:46:0,
from trigger.c:14:
trigger.c: In function ‘renametrig_partition’:
../../../src/include/c.h:118:31: error: expected ‘,’ or ‘;’ before ‘__attribute__’
#define pg_attribute_unused() __attribute__((unused))
^
../../../src/include/c.h:155:34: note: in expansion of macro ‘pg_attribute_unused’
#define PG_USED_FOR_ASSERTS_ONLY pg_attribute_unused()
^~~~~~~~~~~~~~~~~~~
trigger.c:1588:17: note: in expansion of macro ‘PG_USED_FOR_ASSERTS_ONLY’
int found = 0 PG_USED_FOR_ASSERTS_ONLY;
^~~~~~~~~~~~~~~~~~~~~~~~
trigger.c:1588:7: warning: unused variable ‘found’ [-Wunused-variable]
int found = 0 PG_USED_FOR_ASSERTS_ONLY;
^~~~~
make[3]: *** [<builtin>: trigger.o] Error 1
Any ideas on what I might be doing wrong?
Regards
Arne
On 2021-Jul-20, Arne Roland wrote:
Thank you! I get a compile time error
trigger.c:1588:17: note: in expansion of macro ‘PG_USED_FOR_ASSERTS_ONLY’
int found = 0 PG_USED_FOR_ASSERTS_ONLY;
^~~~~~~~~~~~~~~~~~~~~~~~
Oh, I misplaced the annotation of course. It has to be
int found PG_USED_FOR_ASSERTS_ONLY = 0;
--
Álvaro Herrera 39°49'30"S 73°17'W — https://www.EnterpriseDB.com/
On 2021-Jul-19, Alvaro Herrera wrote:
Well, it does rename a trigger named 'name' on the table 'table_name',
as well as all its descendant triggers. I guess I am surprised that
anybody would rename a descendant trigger in the first place. I'm not
wedded to the decision of removing the NOTICE, though ... are there any
other votes for that, anyone?
I put it back, mostly because I don't really care and it's easily
removed if people don't want it. (I used a different wording though,
not necessarily final.)
I also realized that if you rename a trigger and the target name is
already occupied, we'd better throw a nicer error message than failure
by violation of a unique constraint; so I moved the check for the name
to within renametrig_internal(). Added a test for this.
Also added a test to ensure that nothing happens with statement
triggers. This doesn't need any new code, because those triggers don't
have tgparentid set.
--
Álvaro Herrera 39°49'30"S 73°17'W — https://www.EnterpriseDB.com/
Attachments:
v9-0001-Make-ALTER-TRIGGER-recurse-to-partitions.patchtext/x-diff; charset=utf-8Download
From 0a2a5e92437475162161837001c7aef16f57908e Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Tue, 20 Jul 2021 16:00:31 -0400
Subject: [PATCH v9] Make ALTER TRIGGER recurse to partitions
---
src/backend/commands/trigger.c | 197 +++++++++++++++++++------
src/backend/parser/gram.y | 2 +-
src/test/regress/expected/triggers.out | 109 ++++++++++++++
src/test/regress/sql/triggers.sql | 59 ++++++++
4 files changed, 323 insertions(+), 44 deletions(-)
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 6d4b7ee92a..0005cf657d 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -71,6 +71,12 @@ int SessionReplicationRole = SESSION_REPLICATION_ROLE_ORIGIN;
static int MyTriggerDepth = 0;
/* Local function prototypes */
+static void renametrig_internal(Relation tgrel, Relation targetrel,
+ HeapTuple trigtup, const char *newname,
+ const char *expected_name);
+static void renametrig_partition(Relation tgrel, Oid partitionId,
+ Oid parentTriggerOid, const char *newname,
+ const char *expected_name);
static void SetTriggerFlags(TriggerDesc *trigdesc, Trigger *trigger);
static bool GetTupleForTrigger(EState *estate,
EPQState *epqstate,
@@ -1442,38 +1448,17 @@ renametrig(RenameStmt *stmt)
targetrel = relation_open(relid, NoLock);
/*
- * Scan pg_trigger twice for existing triggers on relation. We do this in
- * order to ensure a trigger does not exist with newname (The unique index
- * on tgrelid/tgname would complain anyway) and to ensure a trigger does
- * exist with oldname.
- *
- * NOTE that this is cool only because we have AccessExclusiveLock on the
- * relation, so the trigger set won't be changing underneath us.
+ * On partitioned tables, this operation recurses to partitions, unless
+ * caller requested not to. Lock all tables upfront, if needed.
*/
+ if (stmt->relation->inh &&
+ targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ (void) find_all_inheritors(relid, AccessExclusiveLock, NULL);
+
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
- * First pass -- look for name conflict
- */
- ScanKeyInit(&key[0],
- Anum_pg_trigger_tgrelid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(relid));
- ScanKeyInit(&key[1],
- Anum_pg_trigger_tgname,
- BTEqualStrategyNumber, F_NAMEEQ,
- PointerGetDatum(stmt->newname));
- tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
- NULL, 2, key);
- if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
- ereport(ERROR,
- (errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" already exists",
- stmt->newname, RelationGetRelationName(targetrel))));
- systable_endscan(tgscan);
-
- /*
- * Second pass -- look for trigger existing with oldname and update
+ * Search for the trigger to modify.
*/
ScanKeyInit(&key[0],
Anum_pg_trigger_tgrelid,
@@ -1489,27 +1474,27 @@ renametrig(RenameStmt *stmt)
{
Form_pg_trigger trigform;
- /*
- * Update pg_trigger tuple with new tgname.
- */
- tuple = heap_copytuple(tuple); /* need a modifiable copy */
trigform = (Form_pg_trigger) GETSTRUCT(tuple);
tgoid = trigform->oid;
- namestrcpy(&trigform->tgname,
- stmt->newname);
+ /* Rename the trigger on this relation ... */
+ renametrig_internal(tgrel, targetrel, tuple, stmt->newname,
+ stmt->subname);
- CatalogTupleUpdate(tgrel, &tuple->t_self, tuple);
+ /* ... and if it is partitioned, recurse to its partitions */
+ if (stmt->relation->inh &&
+ targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ PartitionDesc partdesc = RelationGetPartitionDesc(targetrel, true);
- InvokeObjectPostAlterHook(TriggerRelationId,
- tgoid, 0);
+ for (int i = 0; i < partdesc->nparts; i++)
+ {
+ Oid partitionId = partdesc->oids[i];
- /*
- * Invalidate relation's relcache entry so that other backends (and
- * this one too!) are sent SI message to make them rebuild relcache
- * entries. (Ideally this should happen automatically...)
- */
- CacheInvalidateRelcache(targetrel);
+ renametrig_partition(tgrel, partitionId, trigform->oid,
+ stmt->newname, stmt->subname);
+ }
+ }
}
else
{
@@ -1533,6 +1518,132 @@ renametrig(RenameStmt *stmt)
return address;
}
+/*
+ * Subroutine for renametrig -- perform the actual work of renaming one
+ * trigger on one table.
+ *
+ * If the trigger has a name different from the expected one, raise a
+ * NOTICE about it.
+ */
+static void
+renametrig_internal(Relation tgrel, Relation targetrel, HeapTuple trigtup,
+ const char *newname, const char *expected_name)
+{
+ HeapTuple tuple;
+ Form_pg_trigger newtgform;
+ ScanKeyData key[2];
+ SysScanDesc tgscan;
+
+ /*
+ * Before actually trying the rename, search for triggers with the same
+ * name. The update would fail with an ugly message in that case, and it
+ * is better to throw a nicer error.
+ */
+ ScanKeyInit(&key[0],
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(targetrel)));
+ ScanKeyInit(&key[1],
+ Anum_pg_trigger_tgname,
+ BTEqualStrategyNumber, F_NAMEEQ,
+ PointerGetDatum(newname));
+ tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
+ NULL, 2, key);
+ if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_OBJECT),
+ errmsg("trigger \"%s\" for relation \"%s\" already exists",
+ newname, RelationGetRelationName(targetrel))));
+ systable_endscan(tgscan);
+
+ /*
+ * The target name is free; update the existing pg_trigger tuple with it.
+ */
+ tuple = heap_copytuple(trigtup); /* need a modifiable copy */
+ newtgform = (Form_pg_trigger) GETSTRUCT(tuple);
+
+ /*
+ * If the trigger has a name different from what we expected, let the user
+ * know. (We can proceed anyway, since we must have reached here following
+ * a tgparentid link.)
+ */
+ if (strcmp(NameStr(newtgform->tgname), expected_name) != 0)
+ ereport(NOTICE,
+ errmsg("renamed trigger \"%s\" on relation \"%s\"",
+ NameStr(newtgform->tgname),
+ RelationGetRelationName(targetrel)));
+
+ namestrcpy(&newtgform->tgname, newname);
+
+ CatalogTupleUpdate(tgrel, &tuple->t_self, tuple);
+
+ InvokeObjectPostAlterHook(TriggerRelationId, newtgform->oid, 0);
+
+ /*
+ * Invalidate relation's relcache entry so that other backends (and this
+ * one too!) are sent SI message to make them rebuild relcache entries.
+ * (Ideally this should happen automatically...)
+ */
+ CacheInvalidateRelcache(targetrel);
+}
+
+/*
+ * Subroutine for renametrig -- Helper for recursing to partitions when
+ * renaming triggers on a partitioned table.
+ */
+static void
+renametrig_partition(Relation tgrel, Oid partitionId, Oid parentTriggerOid,
+ const char *newname, const char *expected_name)
+{
+ SysScanDesc tgscan;
+ ScanKeyData key;
+ HeapTuple tuple;
+ int found PG_USED_FOR_ASSERTS_ONLY = 0;
+
+ /*
+ * Given a relation and the OID of a trigger on parent relation, find the
+ * corresponding trigger in the child and rename that trigger to the given
+ * name.
+ */
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(partitionId));
+ tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
+ NULL, 1, &key);
+ while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tuple);
+ Relation partitionRel;
+
+ if (tgform->tgparentid != parentTriggerOid)
+ continue; /* not our trigger */
+
+ Assert(found++ <= 0);
+
+ partitionRel = table_open(partitionId, NoLock);
+
+ /* Rename the trigger on this partition */
+ renametrig_internal(tgrel, partitionRel, tuple, newname, expected_name);
+
+ /* And if this relation is partitioned, recurse to its partitions */
+ if (partitionRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ PartitionDesc partdesc = RelationGetPartitionDesc(partitionRel,
+ true);
+
+ for (int i = 0; i < partdesc->nparts; i++)
+ {
+ Oid partitionId = partdesc->oids[i];
+
+ renametrig_partition(tgrel, partitionId, tgform->oid, newname,
+ NameStr(tgform->tgname));
+ }
+ }
+ table_close(partitionRel, NoLock);
+ }
+ systable_endscan(tgscan);
+}
/*
* EnableDisableTrigger()
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 10da5c5c51..26433c529f 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -8886,7 +8886,7 @@ RenameStmt: ALTER AGGREGATE aggregate_with_argtypes RENAME TO name
n->missing_ok = false;
$$ = (Node *)n;
}
- | ALTER TRIGGER name ON qualified_name RENAME TO name
+ | ALTER TRIGGER name ON relation_expr RENAME TO name
{
RenameStmt *n = makeNode(RenameStmt);
n->renameType = OBJECT_TRIGGER;
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 5254447cf8..9e47bc4d45 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -3410,3 +3410,112 @@ for each statement execute function trigger_function1();
delete from convslot_test_parent;
NOTICE: trigger = bdt_trigger, old_table = (111,tutu), (311,tutu)
drop table convslot_test_child, convslot_test_parent;
+-- Test trigger renaming on partitioned tables
+create table grandparent (id int, primary key (id)) partition by range (id);
+create view ourtriggers as
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid in (select relid from pg_partition_tree('grandparent'))
+order by tgname, tgrelid::regclass::text;
+create table middle partition of grandparent for values from (1) to (10)
+partition by range (id);
+create table chi partition of middle for values from (1) to (5);
+create table cho partition of middle for values from (6) to (8);
+create function f () returns trigger as
+$$ begin return new; end; $$
+language plpgsql;
+create trigger a after insert on grandparent
+for each row execute procedure f();
+select * from ourtriggers;
+ tgrelid | tgname | parent_tgname
+-------------+--------+---------------
+ chi | a | a
+ cho | a | a
+ grandparent | a |
+ middle | a | a
+(4 rows)
+
+alter trigger a on grandparent rename to b;
+select * from ourtriggers;
+ tgrelid | tgname | parent_tgname
+-------------+--------+---------------
+ chi | b | b
+ cho | b | b
+ grandparent | b |
+ middle | b | b
+(4 rows)
+
+alter trigger b on only middle rename to something;
+select * from ourtriggers;
+ tgrelid | tgname | parent_tgname
+-------------+-----------+---------------
+ chi | b | something
+ cho | b | something
+ grandparent | b |
+ middle | something | b
+(4 rows)
+
+alter trigger something on middle rename to a_trigger;
+NOTICE: renamed trigger "b" on relation "chi"
+NOTICE: renamed trigger "b" on relation "cho"
+select * from ourtriggers;
+ tgrelid | tgname | parent_tgname
+-------------+-----------+---------------
+ chi | a_trigger | a_trigger
+ cho | a_trigger | a_trigger
+ middle | a_trigger | b
+ grandparent | b |
+(4 rows)
+
+-- Renaming fails if a trigger with the target name exists in a partition
+create trigger final after insert on cho for each row execute procedure f();
+alter trigger b on grandparent rename to final;
+NOTICE: renamed trigger "a_trigger" on relation "middle"
+ERROR: trigger "final" for relation "cho" already exists
+select * from ourtriggers;
+ tgrelid | tgname | parent_tgname
+-------------+-----------+---------------
+ chi | a_trigger | a_trigger
+ cho | a_trigger | a_trigger
+ middle | a_trigger | b
+ grandparent | b |
+ cho | final |
+(5 rows)
+
+-- Renaming does not affect statement triggers
+create trigger p after insert on grandparent for each statement execute function f();
+create trigger p after insert on middle for each statement execute function f();
+alter trigger p on grandparent rename to q;
+select * from ourtriggers;
+ tgrelid | tgname | parent_tgname
+-------------+-----------+---------------
+ chi | a_trigger | a_trigger
+ cho | a_trigger | a_trigger
+ middle | a_trigger | b
+ grandparent | b |
+ cho | final |
+ middle | p |
+ grandparent | q |
+(7 rows)
+
+drop view ourtriggers;
+drop table grandparent;
+-- Trigger renaming does not recurse on legacy inheritance
+create table parent (a int);
+create table child () inherits (parent);
+create trigger parenttrig after insert on parent
+for each row execute procedure f();
+create trigger parenttrig after insert on child
+for each row execute procedure f();
+alter trigger parenttrig on parent rename to anothertrig;
+\d+ child
+ Table "public.child"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | | | plain | |
+Triggers:
+ parenttrig AFTER INSERT ON child FOR EACH ROW EXECUTE FUNCTION f()
+Inherits: parent
+
+drop table parent, child;
+drop function f();
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 7b73ee20a1..41e5172648 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -2572,3 +2572,62 @@ for each statement execute function trigger_function1();
delete from convslot_test_parent;
drop table convslot_test_child, convslot_test_parent;
+
+-- Test trigger renaming on partitioned tables
+create table grandparent (id int, primary key (id)) partition by range (id);
+create view ourtriggers as
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid in (select relid from pg_partition_tree('grandparent'))
+order by tgname, tgrelid::regclass::text;
+
+create table middle partition of grandparent for values from (1) to (10)
+partition by range (id);
+
+create table chi partition of middle for values from (1) to (5);
+create table cho partition of middle for values from (6) to (8);
+
+create function f () returns trigger as
+$$ begin return new; end; $$
+language plpgsql;
+
+create trigger a after insert on grandparent
+for each row execute procedure f();
+
+select * from ourtriggers;
+
+alter trigger a on grandparent rename to b;
+select * from ourtriggers;
+
+alter trigger b on only middle rename to something;
+select * from ourtriggers;
+
+alter trigger something on middle rename to a_trigger;
+select * from ourtriggers;
+
+-- Renaming fails if a trigger with the target name exists in a partition
+create trigger final after insert on cho for each row execute procedure f();
+alter trigger b on grandparent rename to final;
+select * from ourtriggers;
+
+-- Renaming does not affect statement triggers
+create trigger p after insert on grandparent for each statement execute function f();
+create trigger p after insert on middle for each statement execute function f();
+alter trigger p on grandparent rename to q;
+select * from ourtriggers;
+
+drop view ourtriggers;
+drop table grandparent;
+
+-- Trigger renaming does not recurse on legacy inheritance
+create table parent (a int);
+create table child () inherits (parent);
+create trigger parenttrig after insert on parent
+for each row execute procedure f();
+create trigger parenttrig after insert on child
+for each row execute procedure f();
+alter trigger parenttrig on parent rename to anothertrig;
+\d+ child
+
+drop table parent, child;
+drop function f();
--
2.30.2
... now, thinking about how does pg_dump deal with this, I think this
thread is all wrong, or at least mostly wrong. We cannot have triggers
in partitions with different names from the triggers in the parents.
pg_dump would only dump the trigger in the parent and totally ignore the
ones in children if they have different names.
So we still need the recursion bits; but
1. if we detect that we're running in a trigger which has tgparentid
set, we have to raise an error,
2. if we are running on a trigger in a partitioned table, then ONLY must
not be specified.
--
Álvaro Herrera Valdivia, Chile — https://www.EnterpriseDB.com/
"Puedes vivir sólo una vez, pero si lo haces bien, una vez es suficiente"
Hi,
looking at the patch, I realized the renametrig_partition could use an index leading with tgparentid, without the need to traverse the child tables. Since we still need to lock them, there is likely no practical performance gain. But I am surprised there is no unique index on (tgparentid, tgrelid), which sounds like a decent sanity check to have anyways.
________________________________
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Sent: Thursday, July 22, 2021 00:16
To: Arne Roland
Cc: vignesh C; Zhihong Yu; Pg Hackers
Subject: Re: Rename of triggers for partitioned tables
... now, thinking about how does pg_dump deal with this, I think this
thread is all wrong, or at least mostly wrong. We cannot have triggers
in partitions with different names from the triggers in the parents.
pg_dump would only dump the trigger in the parent and totally ignore the
ones in children if they have different names.
That makes things simpler. The reasoning behind the ONLY variant was not to break things that worked in the past. If it never worked in the past, there is no reason to offer backwards compatibility.
1. if we detect that we're running in a trigger which has tgparentid
set, we have to raise an error,
2. if we are running on a trigger in a partitioned table, then ONLY must
not be specified.
I think that would make the whole idea of allowing ONLY obsolete altogether anyways. I'd vote to simply scratch that syntax. I will work on updating your version 9 of this patch accordingly.
I think we should do something about pg_upgrade. The inconsistency is obviously broken. I'd like it to make sure every partitioned trigger is named correctly, simply taking the name of the parent. Simplifying the state our database can have, might help us to avoid future headaches.
Regards
Arne
On 2021-Jul-22, Arne Roland wrote:
Hi,
looking at the patch, I realized the renametrig_partition could use an index leading with tgparentid, without the need to traverse the child tables. Since we still need to lock them, there is likely no practical performance gain. But I am surprised there is no unique index on (tgparentid, tgrelid), which sounds like a decent sanity check to have anyways.
If we have good use for such an index, I don't see why we can't add it.
But I'm not sure that it is justified -- certainly if the only benefit
is to make ALTER TRIGGER RENAME recurse faster on partitioned tables, it
is not justified.
--
Álvaro Herrera PostgreSQL Developer — https://www.EnterpriseDB.com/
"Nunca se desea ardientemente lo que solo se desea por razón" (F. Alexandre)
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Sent: Thursday, July 22, 2021 18:20
To: Arne Roland
Subject: Re: Rename of triggers for partitioned tables
If we have good use for such an index, I don't see why we can't add it.
But I'm not sure that it is justified -- certainly if the only benefit
is to make ALTER TRIGGER RENAME recurse faster on partitioned tables, it
is not justified.
Speed is not really the issue here, most people will have under 20 triggers per table anyways. I think even the simpler code would be worth more than the speed gain. The main value of such an index would be the enforced uniqueness.
I just noticed that apparently the
ALTER TABLE [ IF EXISTS ] [ ONLY ] name [ ENABLE | DISABLE ] TRIGGER ...
syntax albeit looking totally different and it already recurses, it has precisely the same issue with pg_dump. In that case the ONLY syntax isn't optional and the 2. from your above post does inevitably apply.
Since it is sort of the same problem, I think it might be worthwhile to address it as well within this patch. Adding two to four ereports doesn't sound like scope creeping to me, even though it touches completely different code. I'll look into that as well.
Regards
Arne
On 2021-Jul-22, Arne Roland wrote:
Since it is sort of the same problem, I think it might be worthwhile
to address it as well within this patch. Adding two to four ereports
doesn't sound like scope creeping to me, even though it touches
completely different code. I'll look into that as well.
I don't understand what you mean. But here's an updated patch, with the
following changes
1. support for ONLY is removed, since evidently the only thing it is
good for is introduce inconsistencies
2. recursion to partitioned tables always occurs; no more conditionally
on relation->inh. This is sensible because due to point 1 above, inh
can no longer be false.
3. renaming a trigger that's not topmost is forbidden.
--
Álvaro Herrera 39°49'30"S 73°17'W — https://www.EnterpriseDB.com/
Attachments:
v10-0001-Make-ALTER-TRIGGER-RENAME-consistent-for-partiti.patchtext/x-diff; charset=utf-8Download
From bac7f7f824ef3622281a4aed0a8e658eed3fa1a8 Mon Sep 17 00:00:00 2001
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Date: Wed, 21 Jul 2021 13:32:24 -0400
Subject: [PATCH v10] Make ALTER TRIGGER RENAME consistent for partitioned
tables
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Previously renaming clone triggers on partitions was allowed, which is
pretty much useless and causes pg_dump to be inconsistent (because the
different name is not propagated); and also, when triggers were renamed
on partitioned tables, the partitions were not affected. Make the
operation recurse to partitions, and also forbid renaming clone
triggers.
Co-authored-by: Arne Roland <A.Roland@index.de>
Co-authored-by: Ãlvaro Herrera <alvherre@alvh.no-ip.org>
Reviewed-by: Zhihong Yu <zyu@yugabyte.com>
Discussion: https://postgr.es/m/d0fd7040c2fb4de1a111b9d9ccc456b8@index.de
---
doc/src/sgml/ref/alter_trigger.sgml | 15 +-
src/backend/commands/trigger.c | 215 ++++++++++++++++++++-----
src/test/regress/expected/triggers.out | 76 +++++++++
src/test/regress/sql/triggers.sql | 47 ++++++
4 files changed, 307 insertions(+), 46 deletions(-)
diff --git a/doc/src/sgml/ref/alter_trigger.sgml b/doc/src/sgml/ref/alter_trigger.sgml
index 43a7da4f0b..cec50e2245 100644
--- a/doc/src/sgml/ref/alter_trigger.sgml
+++ b/doc/src/sgml/ref/alter_trigger.sgml
@@ -31,9 +31,20 @@ ALTER TRIGGER <replaceable class="parameter">name</replaceable> ON <replaceable
<para>
<command>ALTER TRIGGER</command> changes properties of an existing
- trigger. The <literal>RENAME</literal> clause changes the name of
+ trigger.
+ </para>
+
+ <para>
+ The <literal>RENAME</literal> clause changes the name of
the given trigger without otherwise changing the trigger
- definition. The <literal>DEPENDS ON EXTENSION</literal> clause marks
+ definition.
+ If the table that the trigger is on is a partitioned table,
+ then corresponding clone triggers in the partitions are
+ renamed too.
+ </para>
+
+ <para>
+ The <literal>DEPENDS ON EXTENSION</literal> clause marks
the trigger as dependent on an extension, such that if the extension is
dropped, the trigger will automatically be dropped as well.
</para>
diff --git a/src/backend/commands/trigger.c b/src/backend/commands/trigger.c
index 6d4b7ee92a..e983b361ea 100644
--- a/src/backend/commands/trigger.c
+++ b/src/backend/commands/trigger.c
@@ -71,6 +71,12 @@ int SessionReplicationRole = SESSION_REPLICATION_ROLE_ORIGIN;
static int MyTriggerDepth = 0;
/* Local function prototypes */
+static void renametrig_internal(Relation tgrel, Relation targetrel,
+ HeapTuple trigtup, const char *newname,
+ const char *expected_name);
+static void renametrig_partition(Relation tgrel, Oid partitionId,
+ Oid parentTriggerOid, const char *newname,
+ const char *expected_name);
static void SetTriggerFlags(TriggerDesc *trigdesc, Trigger *trigger);
static bool GetTupleForTrigger(EState *estate,
EPQState *epqstate,
@@ -1442,38 +1448,16 @@ renametrig(RenameStmt *stmt)
targetrel = relation_open(relid, NoLock);
/*
- * Scan pg_trigger twice for existing triggers on relation. We do this in
- * order to ensure a trigger does not exist with newname (The unique index
- * on tgrelid/tgname would complain anyway) and to ensure a trigger does
- * exist with oldname.
- *
- * NOTE that this is cool only because we have AccessExclusiveLock on the
- * relation, so the trigger set won't be changing underneath us.
+ * On partitioned tables, this operation recurses to partitions. Lock all
+ * tables upfront.
*/
+ if (targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ (void) find_all_inheritors(relid, AccessExclusiveLock, NULL);
+
tgrel = table_open(TriggerRelationId, RowExclusiveLock);
/*
- * First pass -- look for name conflict
- */
- ScanKeyInit(&key[0],
- Anum_pg_trigger_tgrelid,
- BTEqualStrategyNumber, F_OIDEQ,
- ObjectIdGetDatum(relid));
- ScanKeyInit(&key[1],
- Anum_pg_trigger_tgname,
- BTEqualStrategyNumber, F_NAMEEQ,
- PointerGetDatum(stmt->newname));
- tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
- NULL, 2, key);
- if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
- ereport(ERROR,
- (errcode(ERRCODE_DUPLICATE_OBJECT),
- errmsg("trigger \"%s\" for relation \"%s\" already exists",
- stmt->newname, RelationGetRelationName(targetrel))));
- systable_endscan(tgscan);
-
- /*
- * Second pass -- look for trigger existing with oldname and update
+ * Search for the trigger to modify.
*/
ScanKeyInit(&key[0],
Anum_pg_trigger_tgrelid,
@@ -1489,27 +1473,39 @@ renametrig(RenameStmt *stmt)
{
Form_pg_trigger trigform;
- /*
- * Update pg_trigger tuple with new tgname.
- */
- tuple = heap_copytuple(tuple); /* need a modifiable copy */
trigform = (Form_pg_trigger) GETSTRUCT(tuple);
tgoid = trigform->oid;
- namestrcpy(&trigform->tgname,
- stmt->newname);
-
- CatalogTupleUpdate(tgrel, &tuple->t_self, tuple);
-
- InvokeObjectPostAlterHook(TriggerRelationId,
- tgoid, 0);
-
/*
- * Invalidate relation's relcache entry so that other backends (and
- * this one too!) are sent SI message to make them rebuild relcache
- * entries. (Ideally this should happen automatically...)
+ * If the trigger descends from a trigger on a parent partitioned
+ * table, reject the rename. We don't allow a trigger in a partition
+ * to differ in name from that of its parent: that would lead to an
+ * inconsistency that pg_dump would reproduce.
*/
- CacheInvalidateRelcache(targetrel);
+ if (OidIsValid(trigform->tgparentid))
+ ereport(ERROR,
+ errmsg("trigger \"%s\" cannot be renamed", stmt->subname),
+ errhint("Rename trigger on table \"%s\" instead.",
+ get_rel_name(get_partition_parent(relid, false))));
+
+
+ /* Rename the trigger on this relation ... */
+ renametrig_internal(tgrel, targetrel, tuple, stmt->newname,
+ stmt->subname);
+
+ /* ... and if it is partitioned, recurse to its partitions */
+ if (targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ PartitionDesc partdesc = RelationGetPartitionDesc(targetrel, true);
+
+ for (int i = 0; i < partdesc->nparts; i++)
+ {
+ Oid partitionId = partdesc->oids[i];
+
+ renametrig_partition(tgrel, partitionId, trigform->oid,
+ stmt->newname, stmt->subname);
+ }
+ }
}
else
{
@@ -1533,6 +1529,137 @@ renametrig(RenameStmt *stmt)
return address;
}
+/*
+ * Subroutine for renametrig -- perform the actual work of renaming one
+ * trigger on one table.
+ *
+ * If the trigger has a name different from the expected one, raise a
+ * NOTICE about it.
+ */
+static void
+renametrig_internal(Relation tgrel, Relation targetrel, HeapTuple trigtup,
+ const char *newname, const char *expected_name)
+{
+ HeapTuple tuple;
+ Form_pg_trigger tgform;
+ ScanKeyData key[2];
+ SysScanDesc tgscan;
+
+ /* If the trigger already has the new name, nothing to do. */
+ tgform = (Form_pg_trigger) GETSTRUCT(trigtup);
+ if (strcmp(NameStr(tgform->tgname), newname) == 0)
+ return;
+
+ /*
+ * Before actually trying the rename, search for triggers with the same
+ * name. The update would fail with an ugly message in that case, and it
+ * is better to throw a nicer error.
+ */
+ ScanKeyInit(&key[0],
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(RelationGetRelid(targetrel)));
+ ScanKeyInit(&key[1],
+ Anum_pg_trigger_tgname,
+ BTEqualStrategyNumber, F_NAMEEQ,
+ PointerGetDatum(newname));
+ tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
+ NULL, 2, key);
+ if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
+ ereport(ERROR,
+ (errcode(ERRCODE_DUPLICATE_OBJECT),
+ errmsg("trigger \"%s\" for relation \"%s\" already exists",
+ newname, RelationGetRelationName(targetrel))));
+ systable_endscan(tgscan);
+
+ /*
+ * The target name is free; update the existing pg_trigger tuple with it.
+ */
+ tuple = heap_copytuple(trigtup); /* need a modifiable copy */
+ tgform = (Form_pg_trigger) GETSTRUCT(tuple);
+
+ /*
+ * If the trigger has a name different from what we expected, let the user
+ * know. (We can proceed anyway, since we must have reached here following
+ * a tgparentid link.)
+ */
+ if (strcmp(NameStr(tgform->tgname), expected_name) != 0)
+ ereport(NOTICE,
+ errmsg("renamed trigger \"%s\" on relation \"%s\"",
+ NameStr(tgform->tgname),
+ RelationGetRelationName(targetrel)));
+
+ namestrcpy(&tgform->tgname, newname);
+
+ CatalogTupleUpdate(tgrel, &tuple->t_self, tuple);
+
+ InvokeObjectPostAlterHook(TriggerRelationId, tgform->oid, 0);
+
+ /*
+ * Invalidate relation's relcache entry so that other backends (and this
+ * one too!) are sent SI message to make them rebuild relcache entries.
+ * (Ideally this should happen automatically...)
+ */
+ CacheInvalidateRelcache(targetrel);
+}
+
+/*
+ * Subroutine for renametrig -- Helper for recursing to partitions when
+ * renaming triggers on a partitioned table.
+ */
+static void
+renametrig_partition(Relation tgrel, Oid partitionId, Oid parentTriggerOid,
+ const char *newname, const char *expected_name)
+{
+ SysScanDesc tgscan;
+ ScanKeyData key;
+ HeapTuple tuple;
+ int found PG_USED_FOR_ASSERTS_ONLY = 0;
+
+ /*
+ * Given a relation and the OID of a trigger on parent relation, find the
+ * corresponding trigger in the child and rename that trigger to the given
+ * name.
+ */
+ ScanKeyInit(&key,
+ Anum_pg_trigger_tgrelid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(partitionId));
+ tgscan = systable_beginscan(tgrel, TriggerRelidNameIndexId, true,
+ NULL, 1, &key);
+ while (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
+ {
+ Form_pg_trigger tgform = (Form_pg_trigger) GETSTRUCT(tuple);
+ Relation partitionRel;
+
+ if (tgform->tgparentid != parentTriggerOid)
+ continue; /* not our trigger */
+
+ Assert(found++ <= 0);
+
+ partitionRel = table_open(partitionId, NoLock);
+
+ /* Rename the trigger on this partition */
+ renametrig_internal(tgrel, partitionRel, tuple, newname, expected_name);
+
+ /* And if this relation is partitioned, recurse to its partitions */
+ if (partitionRel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+ {
+ PartitionDesc partdesc = RelationGetPartitionDesc(partitionRel,
+ true);
+
+ for (int i = 0; i < partdesc->nparts; i++)
+ {
+ Oid partitionId = partdesc->oids[i];
+
+ renametrig_partition(tgrel, partitionId, tgform->oid, newname,
+ NameStr(tgform->tgname));
+ }
+ }
+ table_close(partitionRel, NoLock);
+ }
+ systable_endscan(tgscan);
+}
/*
* EnableDisableTrigger()
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 5254447cf8..6ca9154066 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -3410,3 +3410,79 @@ for each statement execute function trigger_function1();
delete from convslot_test_parent;
NOTICE: trigger = bdt_trigger, old_table = (111,tutu), (311,tutu)
drop table convslot_test_child, convslot_test_parent;
+-- Test trigger renaming on partitioned tables
+create table grandparent (id int, primary key (id)) partition by range (id);
+create table middle partition of grandparent for values from (1) to (10)
+partition by range (id);
+create table chi partition of middle for values from (1) to (5);
+create table cho partition of middle for values from (6) to (10);
+create function f () returns trigger as
+$$ begin return new; end; $$
+language plpgsql;
+create trigger a after insert on grandparent
+for each row execute procedure f();
+alter trigger a on grandparent rename to b;
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid in (select relid from pg_partition_tree('grandparent'))
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+--------+---------------
+ chi | b | b
+ cho | b | b
+ grandparent | b |
+ middle | b | b
+(4 rows)
+
+alter trigger a on only grandparent rename to b; -- ONLY not supported
+ERROR: syntax error at or near "only"
+LINE 1: alter trigger a on only grandparent rename to b;
+ ^
+alter trigger b on middle rename to c; -- can't rename trigger on partition
+ERROR: trigger "b" cannot be renamed
+HINT: Rename trigger on table "grandparent" instead.
+create trigger c after insert on middle
+for each row execute procedure f();
+alter trigger b on grandparent rename to c;
+ERROR: trigger "c" for relation "middle" already exists
+-- Rename cascading does not affect statement triggers
+create trigger p after insert on grandparent for each statement execute function f();
+create trigger p after insert on middle for each statement execute function f();
+alter trigger p on grandparent rename to q;
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid in (select relid from pg_partition_tree('grandparent'))
+order by tgname, tgrelid::regclass::text;
+ tgrelid | tgname | parent_tgname
+-------------+--------+---------------
+ chi | b | b
+ cho | b | b
+ grandparent | b |
+ middle | b | b
+ chi | c | c
+ cho | c | c
+ middle | c |
+ middle | p |
+ grandparent | q |
+(9 rows)
+
+drop table grandparent;
+-- Trigger renaming does not recurse on legacy inheritance
+create table parent (a int);
+create table child () inherits (parent);
+create trigger parenttrig after insert on parent
+for each row execute procedure f();
+create trigger parenttrig after insert on child
+for each row execute procedure f();
+alter trigger parenttrig on parent rename to anothertrig;
+\d+ child
+ Table "public.child"
+ Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
+--------+---------+-----------+----------+---------+---------+--------------+-------------
+ a | integer | | | | plain | |
+Triggers:
+ parenttrig AFTER INSERT ON child FOR EACH ROW EXECUTE FUNCTION f()
+Inherits: parent
+
+drop table parent, child;
+drop function f();
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 7b73ee20a1..fb94eca3ed 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -2572,3 +2572,50 @@ for each statement execute function trigger_function1();
delete from convslot_test_parent;
drop table convslot_test_child, convslot_test_parent;
+
+-- Test trigger renaming on partitioned tables
+create table grandparent (id int, primary key (id)) partition by range (id);
+create table middle partition of grandparent for values from (1) to (10)
+partition by range (id);
+create table chi partition of middle for values from (1) to (5);
+create table cho partition of middle for values from (6) to (10);
+create function f () returns trigger as
+$$ begin return new; end; $$
+language plpgsql;
+create trigger a after insert on grandparent
+for each row execute procedure f();
+
+alter trigger a on grandparent rename to b;
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid in (select relid from pg_partition_tree('grandparent'))
+order by tgname, tgrelid::regclass::text;
+alter trigger a on only grandparent rename to b; -- ONLY not supported
+alter trigger b on middle rename to c; -- can't rename trigger on partition
+create trigger c after insert on middle
+for each row execute procedure f();
+alter trigger b on grandparent rename to c;
+
+-- Rename cascading does not affect statement triggers
+create trigger p after insert on grandparent for each statement execute function f();
+create trigger p after insert on middle for each statement execute function f();
+alter trigger p on grandparent rename to q;
+select tgrelid::regclass, tgname,
+(select tgname from pg_trigger tr where tr.oid = pg_trigger.tgparentid) parent_tgname
+from pg_trigger where tgrelid in (select relid from pg_partition_tree('grandparent'))
+order by tgname, tgrelid::regclass::text;
+
+drop table grandparent;
+
+-- Trigger renaming does not recurse on legacy inheritance
+create table parent (a int);
+create table child () inherits (parent);
+create trigger parenttrig after insert on parent
+for each row execute procedure f();
+create trigger parenttrig after insert on child
+for each row execute procedure f();
+alter trigger parenttrig on parent rename to anothertrig;
+\d+ child
+
+drop table parent, child;
+drop function f();
--
2.30.2
On 2021-Jul-22, Arne Roland wrote:
I just noticed that apparently the
ALTER TABLE [ IF EXISTS ] [ ONLY ] name [ ENABLE | DISABLE ] TRIGGER ...
syntax albeit looking totally different and it already recurses, it
has precisely the same issue with pg_dump. In that case the ONLY
syntax isn't optional and the 2. from your above post does inevitably
apply.Since it is sort of the same problem, I think it might be worthwhile
to address it as well within this patch. Adding two to four ereports
doesn't sound like scope creeping to me, even though it touches
completely different code. I'll look into that as well.
Oh! For some reason I failed to notice that you were talking about
ENABLE / DISABLE, not RENAME anymore. It turns out that I recently
spent some time in this area; see commits below (these are from branch
REL_11_STABLE). Are you talking about something that need to be handled
*beyond* these fixes?
commit ccfc3cbb341abf515d930097c4851d9e2152504f
Author: Alvaro Herrera <alvherre@alvh.no-ip.org> [Álvaro Herrera <alvherre@alvh.no-ip.org>]
AuthorDate: Fri Jul 16 17:29:22 2021 -0400
CommitDate: Fri Jul 16 17:29:22 2021 -0400
Fix pg_dump for disabled triggers on partitioned tables
pg_dump failed to preserve the 'enabled' flag (which can be not only
disabled, but also REPLICA or ALWAYS) for partitions which had it
changed from their respective parents. Attempt to handle that by
including a definition for such triggers in the dump, but replace the
standard CREATE TRIGGER line with an ALTER TRIGGER line.
Backpatch to 11, where these triggers can exist. In branches 11 and 12,
pick up a few test lines from commit b9b408c48724 to verify that
pg_upgrade is okay with these arrangements.
Co-authored-by: Justin Pryzby <pryzby@telsasoft.com>
Co-authored-by: Álvaro Herrera <alvherre@alvh.no-ip.org>
Discussion: /messages/by-id/20200930223450.GA14848@telsasoft.com
commit fed35bd4a65032f714af6bc88c102d0e90422dee
Author: Alvaro Herrera <alvherre@alvh.no-ip.org> [Álvaro Herrera <alvherre@alvh.no-ip.org>]
AuthorDate: Fri Jul 16 13:01:43 2021 -0400
CommitDate: Fri Jul 16 13:01:43 2021 -0400
Preserve firing-on state when cloning row triggers to partitions
When triggers are cloned from partitioned tables to their partitions,
the 'tgenabled' flag (origin/replica/always/disable) was not propagated.
Make it so that the flag on the trigger on partition is initially set to
the same value as on the partitioned table.
Add a test case to verify the behavior.
Backpatch to 11, where this appeared in commit 86f575948c77.
Author: Álvaro Herrera <alvherre@alvh.no-ip.org>
Reported-by: Justin Pryzby <pryzby@telsasoft.com>
Discussion: /messages/by-id/20200930223450.GA14848@telsasoft.com
commit bbb927b4db9b3b449ccd0f76c1296de382a2f0c1
Author: Alvaro Herrera <alvherre@alvh.no-ip.org> [Álvaro Herrera <alvherre@alvh.no-ip.org>]
AuthorDate: Tue Oct 20 19:22:09 2020 -0300
CommitDate: Tue Oct 20 19:22:09 2020 -0300
Fix ALTER TABLE .. ENABLE/DISABLE TRIGGER recursion
More precisely, correctly handle the ONLY flag indicating not to
recurse. This was implemented in 86f575948c77 by recursing in
trigger.c, but that's the wrong place; use ATSimpleRecursion instead,
which behaves properly. However, because legacy inheritance has never
recursed in that situation, make sure to do that only for new-style
partitioning.
I noticed this problem while testing a fix for another bug in the
vicinity.
This has been wrong all along, so backpatch to 11.
Discussion: /messages/by-id/20201016235925.GA29829@alvherre.pgsql
--
Álvaro Herrera Valdivia, Chile — https://www.EnterpriseDB.com/
From: Alvaro Herrera <alvherre@alvh.no-ip.org>
Sent: Thursday, July 22, 2021 19:33
To: Arne Roland
Subject: Re: Rename of triggers for partitioned tables
On 2021-Jul-22, Arne Roland wrote:
Since it is sort of the same problem, I think it might be worthwhile
to address it as well within this patch. Adding two to four ereports
doesn't sound like scope creeping to me, even though it touches
completely different code. I'll look into that as well.
I don't understand what you mean. But here's an updated patch, with the
following changes
alter table middle disable trigger b;
creates the same kind of inconsistency
alter trigger b on middle rename to something;
does.
With other words: enableing/disabling non-topmost triggers should be forbidden as well.
Regards
Arne
On 2021-Jul-22, Arne Roland wrote:
alter table middle disable trigger b;
creates the same kind of inconsistency
alter trigger b on middle rename to something;
does.
With other words: enableing/disabling non-topmost triggers should be forbidden as well.
I'm not so sure about that ... I think enabling/disabling triggers on
individual partitions is a valid use case. Renaming them, not so much.
--
Álvaro Herrera Valdivia, Chile — https://www.EnterpriseDB.com/
"No necesitamos banderas
No reconocemos fronteras" (Jorge González)
I pushed this patch with some minor corrections (mostly improved the
message wording.)
Thanks
--
Álvaro Herrera Valdivia, Chile — https://www.EnterpriseDB.com/
"En las profundidades de nuestro inconsciente hay una obsesiva necesidad
de un universo lógico y coherente. Pero el universo real se halla siempre
un paso más allá de la lógica" (Irulan)
On 2021-Jul-22, Alvaro Herrera wrote:
I pushed this patch with some minor corrections (mostly improved the
message wording.)
Hmm, the sorting fails for Czech or something like that. Will fix ...
--
Álvaro Herrera 39°49'30"S 73°17'W — https://www.EnterpriseDB.com/
Alvaro Herrera <alvherre@alvh.no-ip.org> writes:
I pushed this patch with some minor corrections (mostly improved the
message wording.)
Coverity does not like this bit, and I fully agree:
/srv/coverity/git/pgsql-git/postgresql/src/backend/commands/trigger.c: 1639 in renametrig_partition()
CID 1489387: Incorrect expression (ASSERT_SIDE_EFFECT)
Argument "found++" of Assert() has a side effect. The containing function might work differently in a non-debug build.
1639 Assert(found++ <= 0);
Perhaps there's no actual bug there, but it's still horrible coding.
For one thing, the implication that found could be negative is extremely
confusing to readers. A boolean might be better. However, I wonder why
you bothered with a flag in the first place. The usual convention if
we know there can be only one match is to just not write a loop at all,
with a suitable comment, like this pre-existing example elsewhere in
trigger.c:
/* There should be at most one matching tuple */
if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))
If you're not quite convinced there can be only one match, then it
still shouldn't be an Assert --- a real test-and-elog would be better.
regards, tom lane
On 2021-Jul-25, Tom Lane wrote:
Perhaps there's no actual bug there, but it's still horrible coding.
For one thing, the implication that found could be negative is extremely
confusing to readers. A boolean might be better. However, I wonder why
you bothered with a flag in the first place. The usual convention if
we know there can be only one match is to just not write a loop at all,
with a suitable comment, like this pre-existing example elsewhere in
trigger.c:/* There should be at most one matching tuple */
if (HeapTupleIsValid(tuple = systable_getnext(tgscan)))If you're not quite convinced there can be only one match, then it
still shouldn't be an Assert --- a real test-and-elog would be better.
I agree that coding there was dubious. I've removed the flag and assert.
Arne complained that there should be a unique constraint on (tgrelid,
tgparentid) which would sidestep the need for this to be a loop. I
don't think it's really necessary, and I'm not sure how to create a
system index WHERE tgparentid <> 0.
--
Álvaro Herrera 39°49'30"S 73°17'W — https://www.EnterpriseDB.com/
"Saca el libro que tu religión considere como el indicado para encontrar la
oración que traiga paz a tu alma. Luego rebootea el computador
y ve si funciona" (Carlos Duclós)
Alvaro Herrera <alvherre@alvh.no-ip.org> writes:
Arne complained that there should be a unique constraint on (tgrelid,
tgparentid) which would sidestep the need for this to be a loop. I
don't think it's really necessary, and I'm not sure how to create a
system index WHERE tgparentid <> 0.
Yeah, we don't support partial indexes on catalogs, and this example
doesn't make me feel like we ought to open that can of worms. But
then maybe this function shouldn't assume there's only one match?
regards, tom lane
From: Tom Lane <tgl@sss.pgh.pa.us>
Sent: Monday, July 26, 2021 19:38
To: Alvaro Herrera
Subject: Re: Rename of triggers for partitioned tables
Yeah, we don't support partial indexes on catalogs, and this example
doesn't make me feel like we ought to open that can of worms.
I asked why such an index doesn't exists already. I guess that answers it. I definitely don't want to push for supporting that, especially not knowing what it entails.
But then maybe this function shouldn't assume there's only one match?
I'd consider a duplication here a serious bug. That is pretty much catalog corruption. Having two children within a single child table sounds like it would break a lot of things.
That being said this function is hardly performance critical. I don't know whether throwing an elog indicating what's going wrong here would be worth it.
Regards
Arne