Skip to Content
FeaturesModulesMigratorAuto-Repair

Auto-Repair

RepairEngine takes a DriftReport and applies targeted history changes to bring django_migrations in line with the live schema. After it returns, a plain migrate --database=X should succeed without “already exists” or “does not exist” errors.

Auto-repair only modifies the django_migrations history table — it never issues DDL (ALTER TABLE, CREATE TABLE, etc.). The goal is to make the recorded history match reality so Django’s own migrate can proceed safely.

Repair order

The engine applies repairs in a specific order to avoid conflicts:

1. Fake-apply unrecorded_present

DDL is already in the database but django_migrations has no record. Inserting the history row with no DDL is safe — the schema is already correct.

-- What fake_apply does internally: INSERT INTO django_migrations (app, name, applied) VALUES ('catalog', '0003_add_source_meta', NOW());

2. Fake-apply half_applied

The table from CreateModel exists but columns from subsequent AddField operations are missing AND the migration is unrecorded. Plain migrate would fail on the existing table.

The repair fake-applies the history row (marking the whole migration as done) to unblock further migrations, then adds a warning:

vehicles:catalog.0007_add_columns was half-applied — fake-applied to unblock migrate, but column catalog_vehicle_photo.source_meta missing (expected by AddField). Add a follow-up migration to recreate the missing columns, or restore from backup.

Half-applied repairs only unblock migrate. The missing columns must be recovered manually: add a new migration with AddField for each missing column, or restore the database to a backup taken before the partial migration ran.

3. Fake-rewind recorded_missing

django_migrations has the row but the DDL was never applied (or was rolled back). Plain migrate would skip the migration (it thinks it already ran). A fake-rewind removes the history rows back to just before the earliest drifted migration, so migrate re-applies them forward.

Planning the rewind: The engine groups recorded_missing incidents by app, finds the earliest drifted migration in the dependency graph, and removes all history from that point forward for the app. This ensures migration dependencies are preserved — no migration is re-applied out of order.

# Example: catalog has 0005, 0006, 0007 recorded but 0005's table is missing # Engine rewinds to "0004" (the migration before 0005) # Then plain migrate re-applies 0005, 0006, 0007 in order

Companion-field fake-apply

Some Django fields auto-create a hidden database column via contribute_to_class (for example, django_currency.MoneyField adds a _currency column when the parent DecimalField is declared). The migration recorded for that companion column would fail with “already exists” because the column was added implicitly.

Field implementations register a predicate at import time:

from django_cfg.modules.django_migrator import register_fake_detector from django.db import migrations as dj_migrations def is_currency_companion(op) -> bool: if not isinstance(op, dj_migrations.AddField): return False return getattr(op.field, "is_companion_currency_field", False) register_fake_detector(is_currency_companion)

The orchestrator runs through unapplied migrations once per DB at the start of each run. Migrations whose operations all match a registered detector and whose columns physically exist in the schema are fake-applied automatically before migrate runs.

Companion fake-apply failures are surfaced as warnings, not errors — they do not block downstream migrations.

Enabling auto-repair

Pass --repair to the management command:

# Interactive (prompts before applying) python manage.py migrate_all --repair # CI mode (no prompts) python manage.py migrate_all --repair --non-interactive

Or set repair=True in MigratorOptions:

from django_cfg.modules.django_migrator import Migrator, MigratorOptions migrator = Migrator( options=MigratorOptions( repair=True, interactive=False, ) ) report = migrator.migrate_all()

What the report shows after repair

• vehicles — in sync ✅ Drift: 0 recorded-missing, 1 unrecorded-present, 0 half-applied - unrecorded_present catalog.0003_vehicle_source_meta Repairs: ✓ fake-apply vehicles:catalog.0003_vehicle_source_meta Executed: migrate --database=vehicles

db_report.repairs_applied contains one string per repair action:

  • "fake-apply {alias}:{app}.{migration}" — for unrecorded_present
  • "fake-apply (half-applied) {alias}:{app}.{migration}" — for half_applied
  • "fake-rewind {alias}:{app} → {target} (re-applies N)" — for recorded_missing
  • "fake-apply-companion {alias}:{app}.{migration}" — for companion fields

Dry-run (no mutations)

Pass dry_run=True to scan and report drift without applying any changes:

python manage.py migrate_all --check
migrator = Migrator(options=MigratorOptions(dry_run=True)) report = migrator.check() # alias for migrate_all with dry_run enforced

Dry-run mode runs guards and drift detection but skips all repair steps, companion fake-applies, and the actual migrate call.

Last updated on