Skip to content

Reverting a merged branch fails for COTs with a polymorphic object field #595

Description

@arthanson

Plugin Version

v0.6.0

NetBox Version

v4.5

Python Version

v3.13

Steps to Reproduce

  1. Create branch b1, provision it, and activate it (activate_branch(b1)).
  2. Inside b1, create a COT, e.g. repro.
  3. Add fields to the COT:
    • name — text, primary.
    • ref — object, polymorphic, pointing to dcim.site. (Gives a polymorphic column on the COT's table for revert to drop. Target type is irrelevant.)
  • sites — multiobject, pointing to dcim.site. (Creates the through table whose source_id → custom_objects_ FK is DEFERRABLE INITIALLY DEFERRED — the thing that queues pending trigger events. Target type is irrelevant.)
  1. Create one instance of the COT with ref = a Site and sites = [a Site] (the multiobject must be populated so the row delete hits the deferred FK).
  2. Merge b1 → main. ✅ Succeeds (forward order adds columns before inserting rows).
  3. Revert the merge. ❌ Fails:
    ObjectInUse: cannot ALTER TABLE "custom_objects_" because it has pending trigger events
  4. (surfaced downstream as current transaction is aborted, commands ignored…)

script to repro:

"""Minimal repro: polymorphic-object field + netbox-branching revert bug.

Reverting a merged branch that created a COT with a polymorphic OBJECT field
fails:
  ObjectInUse: cannot ALTER TABLE "custom_objects_<id>" because it has pending
  trigger events   (masked downstream as 'current transaction is aborted')

Cause: remove_polymorphic_object_columns() drops the poly columns with ALTER
TABLE but omits 'SET CONSTRAINTS ALL IMMEDIATE' first (the scalar-field path in
models._schema_remove_field does it). On revert the row delete queues deferred
FK trigger events, so the column drop is rejected.

Run: manage.py polyrevertrepro
"""
import time
import traceback
import uuid

from django.core.management.base import BaseCommand, CommandError
from django.test import RequestFactory
from django.urls import reverse


class Command(BaseCommand):
    help = "Minimal repro of the polymorphic-object field branching revert bug."

    def handle(self, *a, **o):
        from netbox.context_managers import event_tracking
        from netbox_branching.choices import BranchStatusChoices
        from netbox_branching.models import Branch
        from netbox_branching.utilities import activate_branch
        from django.contrib.auth import get_user_model
        from core.models import ObjectType
        from dcim.models import Site
        from extras.choices import CustomFieldTypeChoices as F
        from netbox_custom_objects.models import CustomObjectType, CustomObjectTypeField

        user, _ = get_user_model().objects.get_or_create(
            username='polyrepro_user', defaults={'is_superuser': True, 'is_active': True})
        site_ot = ObjectType.objects.get(app_label='dcim', model='site')

        def req():
            r = RequestFactory().get(reverse('home')); r.id = uuid.uuid4(); r.user = user
            return r

        Branch.objects.filter(name='polyrepro').delete()
        CustomObjectType.objects.filter(slug__startswith='polyrepro-').delete()
        with event_tracking(req()):
            site, _ = Site.objects.get_or_create(slug='polyrepro-site', defaults={'name': 'PolyRepro Site'})

        # 1. create + activate a branch
        branch = Branch(name='polyrepro'); branch.save(provision=False); branch.provision(user=user)
        deadline = time.time() + 180
        while time.time() < deadline:
            branch.refresh_from_db()
            if branch.status == BranchStatusChoices.READY:
                break
            time.sleep(0.5)
        if branch.status != BranchStatusChoices.READY:
            raise CommandError(f'branch not ready: {branch.status}')
        self.stdout.write(f'branch ready: {branch.schema_name}')

        # 2. inside the branch: COT with a polymorphic OBJECT field + a MULTIOBJECT
        #    field (the multiobject through's deferred FK is what queues the
        #    pending trigger events when the row is deleted on revert).
        with activate_branch(branch), event_tracking(req()):
            cot = CustomObjectType.objects.create(name='polyrepro', slug='polyrepro-a')
            CustomObjectTypeField.objects.create(custom_object_type=cot, name='name', type=F.TYPE_TEXT, primary=True)
            ref = CustomObjectTypeField.objects.create(
                custom_object_type=cot, name='ref', type=F.TYPE_OBJECT, is_polymorphic=True)
            ref.related_object_types.set([site_ot])
            CustomObjectTypeField.objects.create(
                custom_object_type=cot, name='sites', type=F.TYPE_MULTIOBJECT, related_object_type=site_ot)

            model = cot.get_model()
            row = model.objects.create(name='row1', ref=site)
            row.sites.set([site])
        self.stdout.write('created COT (poly object + multiobject) + 1 row in branch')

        # 3. merge (succeeds)
        branch.merge(user=user, commit=True)
        self.stdout.write('merged OK')

        # 4. revert (fails with pending-trigger-events)
        try:
            branch.revert(user=user, commit=True)
            self.stdout.write('revert OK (bug fixed?)')
        except Exception as exc:
            self.stdout.write(f'revert FAILED: {type(exc).__name__}: {str(exc).splitlines()[0]}')
            traceback.print_exc(file=self.stdout)

        try:
            branch.refresh_from_db(); branch.delete()
        except Exception:
            pass
        CustomObjectType.objects.filter(slug__startswith='polyrepro-').delete()
        Site.objects.filter(slug='polyrepro-site').delete()

Expected Behavior

Revert should succeed

Observed Behavior

Revert fails with error.

Metadata

Metadata

Assignees

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions