diff --git a/doc/src/sgml/ref/create_trigger.sgml b/doc/src/sgml/ref/create_trigger.sgml
index bde3a63f90f..303881fcfbb 100644
--- a/doc/src/sgml/ref/create_trigger.sgml
+++ b/doc/src/sgml/ref/create_trigger.sgml
@@ -526,6 +526,7 @@ UPDATE OF column_name1 [, 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 INSTEAD OF.
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 037d457c3d4..794ff30fac7 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -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.
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index e9da4ef983e..c1482e185bd 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -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();
--
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 80ffbb4b028..e228d0a8a5b 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -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();