Closes: #391 (Phase 2) - post_migrate auto-heal for mixin column drift#481
Closes: #391 (Phase 2) - post_migrate auto-heal for mixin column drift#481bctiemann wants to merge 3 commits into391-base-column-snapshotfrom
Conversation
Add netbox_custom_objects/mixin_migration.py with:
- _expected_base_fields(): resolves base columns from live CustomObject model
- _can_auto_add(): determines if a missing column can be safely ADD COLUMNed
- heal_cot(): detects drift for one COT, adds safe columns, warns on unsafe
ones, never auto-drops; refreshes schema_document snapshot on success
- heal_all_cots(): iterates all COTs, returns {total, healed, warnings}
Wire _heal_mixin_columns() post_migrate signal handler in __init__.py:ready()
so the heal runs automatically after every 'manage.py migrate'. A module-level
_heal_ran flag deduplicates the call (post_migrate fires once per installed
app). makemigrations and collectstatic are excluded.
Add management command 'upgrade_custom_objects' with --dry-run and --cot flags
for explicit inspection and repair outside the migrate flow.
Tests cover: _can_auto_add() unit tests, _expected_base_fields() with/without
user fields, heal_cot() adding nullable columns, warning on non-nullable, not
dropping removed columns, snapshot refresh, dry_run correctness,
heal_all_cots() summary counts, and all management command flags.
61b9fe2 to
f446e28
Compare
- Snapshot refresh: derive updated base_columns by merging new fields into the existing snapshot dict rather than re-running _store_base_column_snapshot(model), which re-derives from model._meta and misses columns added directly to the DB by this heal pass (schema_editor.add_field does not update _meta). - removed-column test: actually add ghost_col to the DB table so the 'if col_name in actual_names' guard in heal_cot() fires correctly.
|
@claude /review |
|
Claude finished @bctiemann's task in 2m 11s —— View job Code Review — PR #481: Phase 2 post_migrate auto-heal for mixin column drift
OverviewThis is a well-structured implementation of automatic mixin column drift detection and repair. The design is sound: post-migrate signal wiring, a module-level deduplication flag, a management command escape hatch, and clear safety rules about what can/cannot be auto-fixed. Overall the code is clean and the test coverage is thorough. A few issues are worth addressing before merge. Issues FoundBug: Field name vs. column name mismatch
# Current: uses f.name (Python attribute name)
return {f.name: f for f in model._meta.concrete_fields if ...}
# Should use f.attname or f.column for FK fields and db_column overrides:
return {f.column: f for f in model._meta.concrete_fields if ...}Also, the user-field exclusion at line 41 checks Missing implementation: type-change detection
Design concern: logging vs. stdout in the management command
Consider having Minor:
|
| Issue | Severity |
|---|---|
f.name vs f.column in drift comparison |
Medium — would produce incorrect results for db_column-overriding fields |
| Missing type-change detection | Low — documented in docstring but unimplemented |
| Logger vs. stdout mixing in command | Low — UX/DX concern |
Double get_model() call |
Negligible |
The core architecture is solid. The f.name vs f.column issue is the most important to address before merge.
- Key by f.column (not f.name) in _expected_base_fields for correct FK field comparison - Accept optional model arg in _expected_base_fields to avoid duplicate get_model() - Add type-change detection: warn when a present base column field class differs from snapshot - Route per-column output to self.stdout by having command iterate COTs directly - Add test for type-change detection warning Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
All review items addressed. |
Summary
Implements automatic detection and repair of mixin column drift for Custom Object Type tables (Phase 2 of #391). When NetBox is upgraded and a mixin (e.g.
ChangeLoggingMixin) gains a new concrete column, existing COT tables would previously be missing that column silently. This PR closes that gap.Depends on PR #480 (Phase 1 -- base column snapshot).
New module:
mixin_migration.py_expected_base_fields(cot)-- resolves the full set of base columns from the liveCustomObjectmodel, excluding user-defined fields_can_auto_add(field)-- determines whether a missing column can be safely added (ADD COLUMN) without breaking existing rows (nullable or has a default)heal_cot(cot, verbosity, dry_run)-- detects drift for a single COT, adds safe columns viaschema_editor.add_field(), warns on unsafe ones (NOT NULL/no default, type changes, removals), never auto-drops, refreshesschema_document["base_columns"]after successful additionsheal_all_cots(verbosity, dry_run)-- iterates all COTs, returns{total, healed, warnings}summarySignal wiring (
__init__.py)_heal_mixin_columns()is connected topost_migrateinready(). It fires automatically after everymanage.py migraterun. A module-level_heal_ranflag ensures the work happens only once per process even thoughpost_migratefires once per installed app. Excluded duringmakemigrationsandcollectstatic.Management command:
upgrade_custom_objectsExplicit escape hatch for inspection and manual repair:
Safety rules
Test plan
CanAutoAddTestCase-- nullable, defaulted, both, neitherExpectedBaseFieldsTestCase-- returns id/created/last_updated, excludes user fields, returns real Django Field instancesHealCotTestCase-- no-drift returns empty; nullable column is added to DB; non-nullable warns; snapshot updated after addition; removed column warns but not dropped; dry_run reports without touching DB; dry_run does not update snapshotHealAllCotsTestCase-- total matches COT count, healed=0 when no drift, correct summary keysUpgradeCustomObjectsCommandTestCase-- runs without error, --dry-run flag, --cot by name, --cot by ID, unknown COT raises CommandErrorGenerated with Claude Code