You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Deleting a type in a branch leaves its reverse accessor on the target type, which then reads main's rows #1048
main at b34ee80 (introduced by #1035, v1.0.0 is not affected)
NetBox Version
4.7.4
Python Version
3.12
Steps to Reproduce
With netbox-branching 1.2.3 installed:
In main, create Custom Object Type product with a text field name.
Create Custom Object Type service with a text field name and an object field product pointing at product, with related name services.
Create product prod-1 and service svc-1 linked to it.
Create a branch and activate it.
Load both types' models in the branch (any page or query that uses them).
In the branch, delete the service type.
In the branch, run product.services.all() on prod-1, for example from a custom script or an export template.
Expected Behavior
product has no services accessor in the branch, the same as at v1.0.0 (AttributeError).
Observed Behavior
The accessor is still there and prod-1.services.all() returns ['svc-1'], read from main's table, though service no longer exists in the branch. No error is raised.
If the branch has also changed service's schema (for example added a field tier), the same call fails instead:
ProgrammingError: column custom_objects_N.tier does not exist
Suspected Cause
#1035 installs a branch's reverse accessors on the branch's own class for the target type, and _update_branch_reverse_accessors() retracts them when the field is deleted or renamed. Deleting the whole source type takes another path. CustomObjectType.delete() calls unregister_model(model, branch_id=...) (models.py#L2982), and unregister_model() retracts accessors only when branch_id is None (models.py#L3010-L3013). The branch's entry in _branch_reverse_accessor_cache is never consulted.
Triage notes. Regression from #1035 (57dd567); v1.0.0 isn't affected.
Root cause (confirmed in code):
CustomObjectType.delete() (models.py:2885) removes the type's fields by DB cascade, so field.delete() never runs and the cleanup in _update_branch_reverse_accessors (models.py:2665) never fires. It only runs on model regeneration (models.py:2541).
delete() ends with unregister_model(model, branch_id=<branch>) (models.py:2982). unregister_model (models.py:2995-3013) retracts accessors only when branch_id is None, and never reads or pops _branch_reverse_accessor_cache[(cot_id, branch_id)] (models.py:1724).
So the target type's branch class (still in _model_cache; nothing bumps its timestamp) keeps the services descriptor pointing at the unregistered branch class. Its branch table is gone, so the query falls through search_path to main's table: main's rows, or a ProgrammingError for a branch-only column. The fall-through is inferred from _table_exists, not traced.
Where the fix lives: custom objects. Reverse accessors and the branch accessor cache are CO model-cache internals; netbox-branching has nothing comparable. In unregister_model's branch path, retract _branch_reverse_accessor_cache.pop((cot_id, branch_id), set()) | _reverse_accessors(model) with _retract_reverse_accessors(cot_id, accessors, branch_id=branch_id), as the main path does, and add a test from the repro above.
Remaining gap (inferred): deleting in a branch doesn't notify other workers (signal_custom_object_type_deleted runs only in main), so they keep the stale accessor until the target is regenerated. Decide whether that's in scope.
Dependencies: none on netbox-branching.
Severity / size: medium (silently reads main's data inside a branch through a reverse lookup), but it's an unreleased regression on main, so it should be fixed before the next release. Small.
Related:#1047 is the mirror case (type deleted in main) in the same delete(); fix together if convenient.
Activity
bctiemann commentedon Nov 10, 2026
Triage notes. Regression from #1035 (57dd567); v1.0.0 isn't affected.
Root cause (confirmed in code):
CustomObjectType.delete()(models.py:2885) removes the type's fields by DB cascade, sofield.delete()never runs and the cleanup in_update_branch_reverse_accessors(models.py:2665) never fires. It only runs on model regeneration (models.py:2541).delete()ends withunregister_model(model, branch_id=<branch>)(models.py:2982).unregister_model(models.py:2995-3013) retracts accessors only whenbranch_id is None, and never reads or pops_branch_reverse_accessor_cache[(cot_id, branch_id)](models.py:1724)._model_cache; nothing bumps its timestamp) keeps theservicesdescriptor pointing at the unregistered branch class. Its branch table is gone, so the query falls throughsearch_pathto main's table: main's rows, or aProgrammingErrorfor a branch-only column. The fall-through is inferred from_table_exists, not traced.Where the fix lives: custom objects. Reverse accessors and the branch accessor cache are CO model-cache internals; netbox-branching has nothing comparable. In
unregister_model's branch path, retract_branch_reverse_accessor_cache.pop((cot_id, branch_id), set()) | _reverse_accessors(model)with_retract_reverse_accessors(cot_id, accessors, branch_id=branch_id), as the main path does, and add a test from the repro above.signal_custom_object_type_deletedruns only in main), so they keep the stale accessor until the target is regenerated. Decide whether that's in scope.Dependencies: none on netbox-branching.
Severity / size: medium (silently reads main's data inside a branch through a reverse lookup), but it's an unreleased regression on
main, so it should be fixed before the next release. Small.Related: #1047 is the mirror case (type deleted in main) in the same
delete(); fix together if convenient.Release plan for this batch (#1042–#1048): the branching-side fixes (netboxlabs/netbox-branching#728 / PR netboxlabs/netbox-branching#729, netboxlabs/netbox-branching#731, netboxlabs/netbox-branching#732) go into the next netbox-branching release. The custom-objects fixes land in the custom-objects release that raises the minimum netbox-branching version to it. Suggested custom-objects order: #1048, then #1042 + #1043, then #1047, then #1044 + #1045. Line numbers above are at custom-objects
b34ee80and netbox-branching5f72279.