Make ALTER TRIGGER RENAME consistent for partitioned tables
authorAlvaro Herrera <alvherre@alvh.no-ip.org>
Thu, 22 Jul 2021 22:33:47 +0000 (18:33 -0400)
committerAlvaro Herrera <alvherre@alvh.no-ip.org>
Thu, 22 Jul 2021 22:33:47 +0000 (18:33 -0400)
Renaming triggers on partitioned tables had two problems: first,
it did not recurse to renaming the triggers on the partitions; and
second, it failed to prohibit renaming clone triggers.  Having triggers
with different names in partitions is pointless, and furthermore pg_dump
would not preserve names for partitions anyway.

Not backpatched -- making the ALTER TRIGGER throw an error in stable
versions might cause problems for existing scripts.

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
src/backend/commands/trigger.c
src/test/regress/expected/triggers.out
src/test/regress/sql/triggers.sql

index 43a7da4f0bcf39e150e0bde35227ba9972f3cb07..ece9cb5acfd1d90306c887eda421b16b5334e241 100644 (file)
@@ -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>
index 6d4b7ee92acda30e8847ec7d71a6b1e2f924fa93..fc0a4b2fa738d9df80c3ee0229e0a570acc46d67 100644 (file)
@@ -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.
         */
-       tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+       if (targetrel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
+               (void) find_all_inheritors(relid, AccessExclusiveLock, NULL);
 
-       /*
-        * 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);
+       tgrel = table_open(TriggerRelationId, RowExclusiveLock);
 
        /*
-        * 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,40 @@ 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);
+               /*
+                * 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 not reproduce.
+                */
+               if (OidIsValid(trigform->tgparentid))
+                       ereport(ERROR,
+                                       errmsg("cannot rename trigger \"%s\" on table \"%s\"",
+                                                  stmt->subname, RelationGetRelationName(targetrel)),
+                                       errhint("Rename trigger on partitioned table \"%s\" instead.",
+                                                       get_rel_name(get_partition_parent(relid, false))));
 
-               CatalogTupleUpdate(tgrel, &tuple->t_self, tuple);
 
-               InvokeObjectPostAlterHook(TriggerRelationId,
-                                                                 tgoid, 0);
+               /* Rename the trigger on this relation ... */
+               renametrig_internal(tgrel, targetrel, tuple, stmt->newname,
+                                                       stmt->subname);
 
-               /*
-                * 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);
+               /* ... 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 +1530,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()
index 5254447cf8ebefb1e0f54c210ff0568d7e8445df..564eb4faa24c3b45c2ebc41e4cdab67d4a7e8b8a 100644 (file)
@@ -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:  cannot rename trigger "b" on table "middle"
+HINT:  Rename trigger on partitioned 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();
index 7b73ee20a1b0de8ea614938d510a0c3b3d10921a..fb94eca3ed2ce1d8f5eb24fc8aeb92b7bbaf4c23 100644 (file)
@@ -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();