Closes: #389 - Upgrade executor tool#453
Closes: #389 - Upgrade executor tool#453bctiemann wants to merge 8 commits into387-schema-comparatorfrom
Conversation
|
@claude /review |
|
Claude finished @bctiemann's task in 2m 29s —— View job Code Review — PR #453: Upgrade Executor Tool
OverviewThis PR implements a well-structured two-phase upgrade executor for COT schemas. The design is sound: Phase 1 creates/updates COT records (establishing backing DB tables), Phase 2 applies all field operations — this neatly avoids FK-ordering problems. The public API is clean, the exception hierarchy is well-typed, and the test suite is thorough. Issues1. schema_type = schema_def["type"]
type_choice = SCHEMA_TYPE_TO_CHOICES[schema_type] # bare dict lookupIf the schema contains an unknown type string, this raises a bare 2. Silent REMOVE skip in elif fc.op is FieldOp.REMOVE:
# allow_destructive=False would have raised before we got here;
# the conditional is a defensive belt-and-suspenders check.
if allow_destructive:
_apply_field_remove(cot, fc)The comment is accurate when called through 3. for diff in ordered:
cot = cot_map[diff.slug]
_update_schema_document(cot, type_defs_by_slug[diff.slug])
_sync_next_schema_id(cot, diff)For a document with 20 COTs where only 1 has changed, 4. :class:`~netbox_custom_objects.comparator.COTDiff`
...
:func:`~netbox_custom_objects.comparator.diff_document`After the rename to 5. field.save()Unlike Minor Observations
Strengths
Overall: Ready to merge with the stale docstring paths fixed (issue 4). The |
|
Issues 1-4 are addressed; 5 is not changed: For the executor PR, the honest answer is: this is a non-issue. field.save() must be called unconditionally because save() always invokes schema_editor.alter_field() I'd leave this as a comment on the PR rather than making a code change: |
Closes: #389
Implements the COT upgrade executor: a public Python API that takes a schema document (or pre-computed
COTDifflist from the comparator, #387) and applies all changes to the live DB atomically.Public API —
executor.pyapply_documentis the primary entry point.apply_diffsis the lower-level primitive used by the schema apply API endpoint (#390, PR #454) when diffs are pre-computed for preview then applied separately.Two-phase execution
All DB writes are wrapped in a single
transaction.atomic()block. Any exception causes a full rollback — including DDL (table creates/drops) since PostgreSQL supports transactional DDL.Phase 1 — COT records
New
CustomObjectTypeinstances are created (which triggersCustomObjectType.create_model()and the backingCREATE TABLE). Existing COTs have their top-level attributes updated. New COTs are created in topologically-sorted order with respect to cross-COTrelated_object_typereferences, so a referenced COT's table always exists when the referencing COT's field is added in Phase 2.Phase 2 — Fields
All
ADD,ALTER, andREMOVEfield operations are applied using the existingCustomObjectTypeField.save()/delete()mechanisms, which encapsulate all required DDL (add_field,alter_field,remove_field). By this point all COT tables are guaranteed to exist, so cross-COT FK resolution never fails due to ordering.Finalisation
After Phase 2, for each COT:
schema_documentis updated (viaQuerySet.update()to avoid clearing the model cache) andnext_schema_idis synced to cover anyschema_idvalues explicitly assigned during the apply.Dependency ordering
_build_dep_order(diffs)performs a DFS-based topological sort over the new-COT subset of the diff list. Onlycustom-objects/<slug>references within the document are treated as ordering constraints; references to existing COTs and built-in NetBox models are ignored. Self-references are not treated as cycles.Raises
CircularDependencyErrorif a cycle is detected before any DB writes.Exceptions
DestructiveChangesErrorREMOVEoperations present andallow_destructive=False(checked before the transaction opens)CircularDependencyErrorUnknownChoiceSetErrorchoice_setname cannot be resolved to aCustomFieldChoiceSetUnknownObjectTypeErrorrelated_object_typestring cannot be resolved to anObjectTypeTests —
test_executor.py(50 tests, 11 classes)ExecutorBuildDepOrderTestCase_build_dep_orderlogic without DB: empty list, single diff, chain ordering, circular detection, self-reference, built-in ROT ignoredExecutorDestructiveGuardTestCaseallow_destructiveflag,DestructiveChangesError.diffsattribute, field survives rejected applyExecutorNewCOTTestCasenext_schema_idsynced,schema_documentpersistedExecutorCOTAttrsTestCaseExecutorFieldAddTestCaseUnknownChoiceSetError/UnknownObjectTypeError;next_schema_idupdateExecutorFieldAlterTestCasevalidation_minimumExecutorFieldRemoveTestCaseschema_documentExecutorSchemaDocumentTestCaseschema_documentset after no-op, contains correct slug andschema_versionExecutorDependencyTestCaseExecutorAtomicityTestCaseExecutorApplyDiffsTestCaseapply_diffslow-level interface works; raisesDestructiveChangesErrorcorrectly