404 Add branching support#476
Conversation
|
@claude /review |
|
Claude finished @arthanson's task in 3m 42s —— View job PR Review: 404 Add branching supportTasks:
OverviewThis is a large, well-scoped PR that implements native netbox-branching support for dynamically-generated custom object types. The core challenge — making runtime Django models coexist with branching's multi-schema PostgreSQL architecture — is solved with several interlocking mechanisms. The implementation is thoughtful and well-documented. Below is a detailed breakdown. Architecture AssessmentSchema-aware DDL routing ( Branch-aware model cache — Keying
Issues1. TOCTOU race in The outer lock is acquired, the cache is checked, and then released before the model is generated and re-cached. A second thread entering 2. Deviation from Django's Both 3. try:
branch = active_branch.get()
except LookupError:
return None
4. The lock around 5. Temporary models in old_through_model = generate_model(
f'_TempOldThrough_{old_through}',
(models.Model,),
{...},
)
schema_editor.alter_db_table(old_through_model, old_through, new_through)
6. When a 7. self.clear_model_cache(self.id, all_branches=True)
model = self.get_model()
Minor Notes
Test Coverage
One concern: the
SummaryThis is a high-quality, well-architected PR. The problems it solves (DDL schema routing, branch-aware model caching, squash-merge ordering, conflict resolution for field renames) are all genuinely hard, and the solutions are sound. The critical issues above are mostly edge-case correctness concerns or operational hygiene (model registry leaks, silent data loss on orphaned CT rows) rather than fundamental design flaws. Recommend: Address items 2 (document |
|
All claude concerns addressed. |
Fixes: #404
Adds native NetBox Branching support to the netbox-custom-objects plugin. Custom object types, fields, and instances can now be created, modified, and deleted inside a branch and then merged, synced, or reverted like any other model - the previous exempt_models workaround in PLUGINS_CONFIG and the branch-warning UI banner are no longer required.
Note: A separate PR will be added to add in a check framework to check for minimum versions of NetBox and branching if branching is installed. This isn't added here as it would break any tests running until those versions are released.
Key Changes
Schema-aware DDL routing
_get_schema_connection()helper routes schema-editor operations (add/remove/alter column, create/drop tables) to the active branch's PostgreSQL schema instead of always targeting main.Branch-aware model cache
CustomObjectType._model_cacheis now keyed by(cot_id, branch_id)so main and each branch get their own generated class. Only main's class is registered in Django's app registry, socontent_type.model_class()consistently resolves to a class matching main's schema even when called inside an active-branch context.Deserialization hooks for branch merge/revert
deserialize_objectclassmethods onCustomObject,CustomObjectType, andCustomObjectTypeFieldsonetbox-branching'sObjectChange.apply()runs the full save lifecycle (model generation, schema editor, signals) instead of bypassing it viasave_base(raw=True).Squash-merge ordering fix
M2M change visibility
add/remove/clear/seton the M2M descriptor now emitm2m_changed, and the through model is flaggedauto_createdso Django's serializer includes M2M values inObjectChange.postchange_data. Without these, a merge or sync replays zero through-table rows for CustomObject M2M fields.Conflict resolution
_schema_alter_fielddetects divergent renames (same field PK renamed differently in main and branch), looks up the live column name in the target schema, and renames it to converge on the merge target._rename_objectchange_field_keyupdates JSON keys incore_objectchangeandnetbox_branching_changediffso historical audit data stays consistent post-rename.Polymorphic fields
Branching extension hooks
PluginConfig.serializer_resolverresolves dynamically-generated CO serializers (which have no importable path under the conventional{app_label}.api.serializers.{Model}Serializer).branching.supports_branching_resolver(registered viaregister_branching_resolver) marks dynamically-generated CO M2M through-models as branchable so their queries are routed to the active branch's connection.branching.add_custom_object_dependencies(connected tosquash_dependency_graph_builtsignal) extends squash's dependency graph with CO-specific edges (local M2M target lists, CustomObjectTypeField PKs from _field_objects, and polymorphic POLY_M2M_SIDECAR_KEY entries) so squash applies field CREATEs before CO CREATEs and orders self-referential M2M targets correctly.Other
cache_timestampis excluded from ObjectChange serialization.snapshot()is called before saves that bumpcache_timestampso change logging records a correct pre-state.pre_deletehandler reconnect moved into a try/finally flow so it's always restored.is_in_branchAPI/UI gates and branch-bypass warning banners are removed.Tests
New
tests/test_branching.py(~1,500 lines) covering:Merge/revert and rename-chain tests run under both iterative and squash strategies via parameterized base classes (
BaseBranchingTests,BaseConcurrentEditMergeTests,SequentialRenameTestCase/SequentialRenameSquashTestCase).Test infrastructure additions in
tests/base.py: stale-model purge, ContentType recreation, and ObjectChange cleanup betweenTransactionTestCaseruns.