Fix detaching partitions with cloned row triggers
authorAlvaro Herrera <alvherre@alvh.no-ip.org>
Tue, 21 Apr 2020 17:57:00 +0000 (13:57 -0400)
committerAlvaro Herrera <alvherre@alvh.no-ip.org>
Tue, 21 Apr 2020 17:57:00 +0000 (13:57 -0400)
When a partition is detached, any triggers that had been cloned from its
parent were not properly disentangled from its parent triggers.
This resulted in triggers that could not be dropped because they
depended on the trigger in the trigger in the no-longer-parent table:
  ALTER TABLE t DETACH PARTITION t1;
  DROP TRIGGER trig ON t1;
    ERROR:  cannot drop trigger trig on table t1 because trigger trig on table t requires it
    HINT:  You can drop trigger trig on table t instead.

Moreover the table can no longer be re-attached to its parent, because
the trigger name is already taken:
  ALTER TABLE t ATTACH PARTITION t1 FOR VALUES FROM (1)TO(2);
    ERROR:  trigger "trig" for relation "t1" already exists

The former is a bug introduced in commit 86f575948c77.  (The latter is
not necessarily a bug, but it makes the bug more uncomfortable.)

To avoid the complexity that would be needed to tell whether the trigger
has a local definition that has to be merged with the one coming from
the parent table, establish the behavior that the trigger is removed
when the table is detached.

Backpatch to pg11.

Author: Justin Pryzby <pryzby@telsasoft.com>
Reviewed-by: Amit Langote <amitlangote09@gmail.com>
Reviewed-by: Álvaro Herrera <alvherre@alvh.no-ip.org>
Discussion: https://www.postgresql.org/message-id/flat/20200408152412.GZ2228@telsasoft.com

doc/src/sgml/ref/create_trigger.sgml
src/backend/commands/tablecmds.c
src/test/regress/expected/triggers.out
src/test/regress/sql/triggers.sql

index bde3a63f90f15210bbfc089b437b3984992ac2a2..303881fcfbbcdd62f016eb81ad03a82104b75409 100644 (file)
@@ -526,6 +526,7 @@ UPDATE OF <replaceable>column_name1</replaceable> [, <replaceable>column_name2</
    Creating a row-level trigger on a partitioned table will cause identical
    triggers to be created in all its existing partitions; and any partitions
    created or attached later will contain an identical trigger, too.
+   If the partition is detached from its parent, the trigger is removed.
    Triggers on partitioned tables may not be <literal>INSTEAD OF</literal>.
   </para>
 
index 037d457c3d469b873b45ae305080059fb31b8148..794ff30fac74a6cfafa31f756c9849b4dc6032fa 100644 (file)
@@ -547,6 +547,7 @@ static void QueuePartitionConstraintValidation(List **wqueue, Relation scanrel,
                                                                                           List *partConstraint,
                                                                                           bool validate_default);
 static void CloneRowTriggersToPartition(Relation parent, Relation partition);
+static void DropClonedTriggersFromPartition(Oid partitionId);
 static ObjectAddress ATExecDetachPartition(Relation rel, RangeVar *name);
 static ObjectAddress ATExecAttachPartitionIdx(List **wqueue, Relation rel,
                                                                                          RangeVar *name);
@@ -16797,6 +16798,9 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
        }
        table_close(classRel, RowExclusiveLock);
 
+       /* Drop any triggers that were cloned on creation/attach. */
+       DropClonedTriggersFromPartition(RelationGetRelid(partRel));
+
        /*
         * Detach any foreign keys that are inherited.  This includes creating
         * additional action triggers.
@@ -16881,6 +16885,66 @@ ATExecDetachPartition(Relation rel, RangeVar *name)
        return address;
 }
 
+/*
+ * DropClonedTriggersFromPartition
+ *             subroutine for ATExecDetachPartition to remove any triggers that were
+ *             cloned to the partition when it was created-as-partition or attached.
+ *             This undoes what CloneRowTriggersToPartition did.
+ */
+static void
+DropClonedTriggersFromPartition(Oid partitionId)
+{
+       ScanKeyData skey;
+       SysScanDesc     scan;
+       HeapTuple       trigtup;
+       Relation        tgrel;
+       ObjectAddresses *objects;
+
+       objects = new_object_addresses();
+
+       /*
+        * Scan pg_trigger to search for all triggers on this rel.
+        */
+       ScanKeyInit(&skey, Anum_pg_trigger_tgrelid, BTEqualStrategyNumber,
+                               F_OIDEQ, ObjectIdGetDatum(partitionId));
+       tgrel = table_open(TriggerRelationId, RowExclusiveLock);
+       scan = systable_beginscan(tgrel, TriggerRelidNameIndexId,
+                                                         true, NULL, 1, &skey);
+       while (HeapTupleIsValid(trigtup = systable_getnext(scan)))
+       {
+               Form_pg_trigger pg_trigger = (Form_pg_trigger) GETSTRUCT(trigtup);
+               ObjectAddress trig;
+
+               /* Ignore triggers that weren't cloned */
+               if (!OidIsValid(pg_trigger->tgparentid))
+                       continue;
+
+               /*
+                * This is ugly, but necessary: remove the dependency markings on the
+                * trigger so that it can be removed.
+                */
+               deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid,
+                                                                               TriggerRelationId,
+                                                                               DEPENDENCY_PARTITION_PRI);
+               deleteDependencyRecordsForClass(TriggerRelationId, pg_trigger->oid,
+                                                                               RelationRelationId,
+                                                                               DEPENDENCY_PARTITION_SEC);
+
+               /* remember this trigger to remove it below */
+               ObjectAddressSet(trig, TriggerRelationId, pg_trigger->oid);
+               add_exact_object_address(&trig, objects);
+       }
+
+       /* make the dependency removal visible to the deletion below */
+       CommandCounterIncrement();
+       performMultipleDeletions(objects, DROP_RESTRICT, PERFORM_DELETION_INTERNAL);
+
+       /* done */
+       free_object_addresses(objects);
+       systable_endscan(scan);
+       table_close(tgrel, RowExclusiveLock);
+}
+
 /*
  * Before acquiring lock on an index, acquire the same lock on the owning
  * table.
index e9da4ef983e1c0819886cf5182fa85a32c465fcd..c1482e185bd3527a725be639090d128114409486 100644 (file)
@@ -2023,6 +2023,51 @@ select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
 ---------+--------+--------
 (0 rows)
 
+-- check detach behavior
+create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
+\d trigpart3
+             Table "public.trigpart3"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+Partition of: trigpart FOR VALUES FROM (2000) TO (3000)
+Triggers:
+    trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
+
+alter table trigpart detach partition trigpart3;
+drop trigger trg1 on trigpart3; -- fail due to "does not exist"
+ERROR:  trigger "trg1" for table "trigpart3" does not exist
+alter table trigpart detach partition trigpart4;
+drop trigger trg1 on trigpart41; -- fail due to "does not exist"
+ERROR:  trigger "trg1" for table "trigpart41" does not exist
+drop table trigpart4;
+alter table trigpart attach partition trigpart3 for values from (2000) to (3000);
+alter table trigpart detach partition trigpart3;
+alter table trigpart attach partition trigpart3 for values from (2000) to (3000);
+drop table trigpart3;
+select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal from pg_trigger
+  where tgname ~ '^trg1' order by 1;
+  tgrelid  | tgname |     tgfoid      | tgenabled | tgisinternal 
+-----------+--------+-----------------+-----------+--------------
+ trigpart  | trg1   | trigger_nothing | O         | f
+ trigpart1 | trg1   | trigger_nothing | O         | t
+(2 rows)
+
+create table trigpart3 (like trigpart);
+create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
+\d trigpart3
+             Table "public.trigpart3"
+ Column |  Type   | Collation | Nullable | Default 
+--------+---------+-----------+----------+---------
+ a      | integer |           |          | 
+ b      | integer |           |          | 
+Triggers:
+    trg1 AFTER INSERT ON trigpart3 FOR EACH ROW EXECUTE FUNCTION trigger_nothing()
+
+alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000) to (3000); -- fail
+ERROR:  trigger "trg1" for relation "trigpart3" already exists
+drop table trigpart3;
 drop table trigpart;
 drop function trigger_nothing();
 --
index 80ffbb4b02810465839f1f64ba9c30c66e21b33a..e228d0a8a5b6e9f6e3efc67bc4314585a527f4c2 100644 (file)
@@ -1380,6 +1380,27 @@ drop trigger trg1 on trigpart;           -- ok, all gone
 select tgrelid::regclass, tgname, tgfoid::regproc from pg_trigger
   where tgrelid::regclass::text like 'trigpart%' order by tgrelid::regclass::text;
 
+-- check detach behavior
+create trigger trg1 after insert on trigpart for each row execute procedure trigger_nothing();
+\d trigpart3
+alter table trigpart detach partition trigpart3;
+drop trigger trg1 on trigpart3; -- fail due to "does not exist"
+alter table trigpart detach partition trigpart4;
+drop trigger trg1 on trigpart41; -- fail due to "does not exist"
+drop table trigpart4;
+alter table trigpart attach partition trigpart3 for values from (2000) to (3000);
+alter table trigpart detach partition trigpart3;
+alter table trigpart attach partition trigpart3 for values from (2000) to (3000);
+drop table trigpart3;
+
+select tgrelid::regclass::text, tgname, tgfoid::regproc, tgenabled, tgisinternal from pg_trigger
+  where tgname ~ '^trg1' order by 1;
+create table trigpart3 (like trigpart);
+create trigger trg1 after insert on trigpart3 for each row execute procedure trigger_nothing();
+\d trigpart3
+alter table trigpart attach partition trigpart3 FOR VALUES FROM (2000) to (3000); -- fail
+drop table trigpart3;
+
 drop table trigpart;
 drop function trigger_nothing();