Architecture
The three inputs
(1) django_migrations table (2) Migration files on disk (3) Live database
"what Django recorded" "what should happen" "what actually exists"
1. Migration history (django_migrations table)
- Which migrations are recorded as applied, and in what order
- No schema details — just app labels and migration names
- Can be wrong (fake applies, manual insertions, deleted rows)
2. Migration code (files on disk)
- The Python operations that define schema changes
- Can be wrong (edited after apply, deleted, rewritten)
- The tool reads these to construct the expected schema
3. Live database schema
- Tables, columns, indexes, and constraints that currently exist
- Ground truth — the reality everything else must be compared against
- Read via Django's
connection.introspectionAPI
The two comparisons
(1) Migration history
│
│ Comparison A: Trust Verification
│ "Can we trust the migration history?"
▼
(2) Migration code ──── produces ────► Expected schema
│
│ Comparison B: Reality Check
│ "Does the DB match the migrations?"
▼
(3) Live database schema
Comparison A — Trust Verification
Checks that the migration history and migration files are consistent with each other.
Invariants:
No Missing Migration Files— every applied migration still has a file on diskSquash Migrations Properly Replaced— squash migrations correctly replace originals
Answers: "Can we trust the migration history at all?"
Comparison B — Reality Check
Replays all applied migration files to build an expected schema, then compares it against the live database.
Invariants:
All Expected Tables Exist/No Unexpected TablesAll Expected Columns Exist/No Unexpected ColumnsColumn Nullability MatchesAll Expected Indexes Exist/No Unexpected IndexesAll Expected Constraints Exist/No Unexpected Constraints
Answers: "Does the actual database match what the migrations say it should be?"
Canonical schema representation
Both the extractor (from migrations) and the introspection module (from the live DB) produce the same data structures, enabling direct comparison:
ColumnState— name, type, nullability, defaultIndexState— name, columnsConstraintState— name, type (unique/check), columnsTableState— name + dicts of the aboveSchemaState— dict ofTableState
The type system normalises database-specific type names (AutoField, CharField, etc.)
to canonical strings (integer, varchar, etc.) so comparisons work across SQLite,
PostgreSQL, and MySQL without backend-specific logic in the invariants.