From 7ddc34a1c8dc1914895742905d957c055868e5d7 Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Wed, 24 Jun 2026 16:31:42 -0400 Subject: [PATCH 1/2] Fixes #595: SET CONSTRAINTS before poly column removal on revert remove_polymorphic_object_columns() dropped the content_type and object_id columns with ALTER TABLE but did not flush deferred FK trigger events first. On branch revert, the preceding row deletion queues deferred FK trigger events (from the DEFERRABLE INITIALLY DEFERRED through-table FK); the subsequent ALTER TABLE is then rejected by PostgreSQL with "pending trigger events". Mirror the fix already applied to _schema_remove_field for scalar fields: issue SET CONSTRAINTS ALL IMMEDIATE before the first ALTER TABLE. Co-Authored-By: Claude Sonnet 4.6 --- netbox_custom_objects/field_types.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/netbox_custom_objects/field_types.py b/netbox_custom_objects/field_types.py index 0de2bf65..a0983e55 100644 --- a/netbox_custom_objects/field_types.py +++ b/netbox_custom_objects/field_types.py @@ -939,6 +939,10 @@ def remove_polymorphic_object_columns(self, field_instance, model, schema_editor ct_field_name = f"{field_instance.name}_content_type" oid_field_name = f"{field_instance.name}_object_id" + # Flush deferred FK trigger events before ALTER TABLE; PostgreSQL rejects + # column removal with "pending trigger events" when a row deletion (from + # the revert path) has queued events on a DEFERRABLE FK column. + schema_editor.execute('SET CONSTRAINTS ALL IMMEDIATE') try: oid_field = model._meta.get_field(oid_field_name) schema_editor.remove_field(model, oid_field) From 17dcde4980b619a597e52f3da2dacf61b5657b6d Mon Sep 17 00:00:00 2001 From: Brian Tiemann Date: Thu, 2 Jul 2026 15:56:59 -0400 Subject: [PATCH 2/2] test(#595): add regression test for pending-trigger ALTER TABLE bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ReferencedObjectDeletionTest.test_remove_poly_obj_columns_succeeds_with_pending_deferred_triggers to exercise the fix from commit 7ddc34a. The test reproduces the branching revert path: 1. Delete through-table rows (simulates Collector cascade). 2. Delete the COT instance via raw SQL → PostgreSQL queues a deferred trigger event on the referenced table (custom_objects_X). 3. Call remove_polymorphic_object_columns() — without the fix this fails with "cannot ALTER TABLE … because it has pending trigger events"; with the fix, SET CONSTRAINTS ALL IMMEDIATE fires and clears the pending event before the ALTER TABLE runs. Co-Authored-By: Claude Sonnet 4.6 --- .../tests/test_polymorphic_fields.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/netbox_custom_objects/tests/test_polymorphic_fields.py b/netbox_custom_objects/tests/test_polymorphic_fields.py index 295edf46..31a0e455 100644 --- a/netbox_custom_objects/tests/test_polymorphic_fields.py +++ b/netbox_custom_objects/tests/test_polymorphic_fields.py @@ -1151,6 +1151,65 @@ def test_deleting_custom_object_type_drops_db_table_and_deregisters_model(self): through_model_name.lower(), django_apps.all_models.get(APP_LABEL, {}) ) + def test_remove_poly_obj_columns_succeeds_with_pending_deferred_triggers(self): + """ + remove_polymorphic_object_columns() must not raise + "cannot ALTER TABLE because it has pending trigger events" when the + source row was deleted inside the same transaction (issue #595 regression). + + The through-table created by the MULTIOBJECT field has a source_id FK: + custom_objects__poly_multi.source_id → custom_objects_.id + DEFERRABLE INITIALLY DEFERRED (Django's PostgreSQL backend default) + + When a row in custom_objects_ is deleted inside a transaction, + PostgreSQL queues a deferred trigger event associated with the REFERENCED + table (custom_objects_), not the referencing through-table. A + subsequent ALTER TABLE on that same table then fails with: + "cannot ALTER TABLE … because it has pending trigger events" + unless SET CONSTRAINTS ALL IMMEDIATE is issued first to flush the queue. + + The fix in remove_polymorphic_object_columns() issues SET CONSTRAINTS ALL + IMMEDIATE before the first ALTER TABLE, firing the deferred check against + the through-table. If the through-table rows were already deleted (as the + branching revert path does before removing the COT instance), the check + finds no FK violation and clears the pending event, allowing the ALTER + TABLE to proceed. + """ + from django.db import connection, transaction as db_transaction + from netbox_custom_objects.field_types import FIELD_TYPE_CLASS + + # Create an instance with the MULTIOBJECT populated so the through-table + # has a row referencing the source. + obj = self.model.objects.create(name="revert-repro") + obj.poly_multi.add(self.site) + + main_table = self.model._meta.db_table + through_table = self.m2m_field.through_table_name + + with db_transaction.atomic(): + with connection.cursor() as cursor: + # Step 1: delete through-table rows first (mirrors the branching + # revert path, which cascades child rows before removing the COT + # instance). + cursor.execute( + f'DELETE FROM "{through_table}" WHERE source_id = %s', [obj.pk] + ) + # Step 2: delete the COT instance via raw SQL. Django's FK + # constraints are DEFERRABLE INITIALLY DEFERRED, so PostgreSQL + # queues a deferred trigger on custom_objects_X (the referenced + # table) instead of checking the constraint immediately. + cursor.execute(f'DELETE FROM "{main_table}" WHERE id = %s', [obj.pk]) + # Step 3: remove the polymorphic OBJECT field columns. Without the + # fix, the ALTER TABLE inside remove_polymorphic_object_columns() + # fails with "cannot ALTER TABLE … because it has pending trigger + # events". The fix calls SET CONSTRAINTS ALL IMMEDIATE first, which + # fires the deferred check (no FK violation since step 1 already + # deleted the through-table row) and clears the pending event. + with connection.schema_editor() as editor: + field_type = FIELD_TYPE_CLASS[self.gfk_field.type]() + field_type.remove_polymorphic_object_columns(self.gfk_field, self.model, editor) + # Reaching here without a database exception confirms the fix is effective. + # --------------------------------------------------------------------------- # Cycle-detection: multi-hop polymorphic cycles