django-migration-audit
A forensic Django tool that verifies whether a live database schema is historically consistent with its applied migrations.
⚠️ Work in Progress
This project is under active development and not yet ready for production use. The core functionality is being implemented and tested, but the API may change and some features are still being refined. Use at your own risk and expect breaking changes.
Why This Tool Exists
Django assumes: if a migration is recorded as applied, the schema must match.
Reality: That assumption can be false.
Common scenarios where this breaks:
- Modified migration files after application
- Manual database schema changes
- Fake-applied migrations (
--fake) - Squashed migrations with mismatches
- Database restores from backups
- Schema drift over time
This tool verifies both assumptions:
- Reachability: Can we trust the migration history?
- Consistency: Does the actual schema match what the history claims?
Installation
pip install django-migration-audit
Or install from source:
git clone https://github.com/yourusername/django-migration-audit.git
cd django-migration-audit
pip install -e .
Add to your Django project's INSTALLED_APPS:
INSTALLED_APPS = [
# ... other apps
'django_migration_audit',
]
Quick Start
Basic Usage
# Run full audit (both comparisons)
python manage.py audit_migrations
# Audit specific database
python manage.py audit_migrations --database=replica
# Run only trust verification (Comparison A)
python manage.py audit_migrations --comparison=a
# Run only reality check (Comparison B)
python manage.py audit_migrations --comparison=b
Example Output (Clean State)
=== Django Migration Audit ===
Database: default
Loading migration history and code...
Applied migrations: 15
Migration files on disk: 15
Missing files: 0
Squashed replacements: 0
🔍 Comparison A: Trust Verification
(Migration history ↔ Migration code)
Checking: No Missing Migration Files...
✅ Pass
Checking: Squash Migrations Properly Replaced...
✅ Pass
🔍 Comparison B: Reality Check
(Expected schema ↔ Actual schema)
Building expected schema from migrations...
Introspecting actual database schema...
Expected tables: 8
Actual tables: 8
Checking: All Expected Tables Exist...
✅ Pass
Checking: No Unexpected Tables...
✅ Pass
Checking: All Expected Columns Exist...
✅ Pass
=== Summary ===
✅ No violations found! Migration state is consistent.
Example Output (Issues Detected)
=== Django Migration Audit ===
Database: default
🔍 Comparison A: Trust Verification
Checking: No Missing Migration Files...
❌ 1 violation(s)
🔍 Comparison B: Reality Check
Checking: All Expected Tables Exist...
❌ 2 violation(s)
Checking: No Unexpected Tables...
❌ 1 violation(s)
=== Summary ===
❌ Found 4 violation(s):
Errors: 3
Warnings: 1
[ERROR] No Missing Migration Files: Migration myapp.0003_add_email is recorded as applied but file is missing
[ERROR] All Expected Tables Exist: Expected table 'myapp_profile' does not exist in database
[ERROR] All Expected Columns Exist: Expected column 'myapp_user.email' does not exist
[WARNING] No Unexpected Tables: Unexpected table 'legacy_data' exists in database
How Detection Works
A common question is: "How does the tool know if a migration file was edited?"
Django only records that a migration ran — not what was in it. There is no hash or
checksum stored in the django_migrations table. The tool detects modifications
indirectly:
- It reads the migration files currently on disk.
- It replays every applied migration's operations in dependency order to build an expected schema — what the database should look like if those exact files were applied unchanged.
- It compares that expected schema to the actual live database schema.
If the expected schema and the actual schema disagree, something went wrong — whether
that is an edited migration, a --fake apply, a manual ALTER TABLE, a database
restore, or a Django version upgrade that changed type storage (e.g. PostgreSQL
serial → identity in Django 4.1).
This means the tool catches all forms of drift, not only edited files.
Migration files on disk
│
│ replay operations
▼
Expected schema ──── Comparison B ──── Actual database schema
(ground truth)
The django_migrations table is only used for Comparison A (trust verification) —
to check that every migration recorded as applied still has a corresponding file on
disk, and that squash migrations are properly set up.
Suppressing Invariants
By default all invariants run. You can suppress specific ones via a CLI flag, Django settings, or programmatically — they are silently skipped and do not appear in output.
CLI flag (one-off runs)
# Skip a single invariant by name (case-sensitive)
python manage.py audit_migrations --skip-invariants "No Unexpected Tables"
# Skip multiple
python manage.py audit_migrations --skip-invariants "No Unexpected Tables" "Column Nullability Matches"
Django settings (persistent per-project baseline)
# settings.py
MIGRATION_AUDIT = {
"SKIP_INVARIANTS": [
"No Unexpected Tables",
"Column Nullability Matches",
],
}
CLI --skip-invariants merges with SKIP_INVARIANTS from settings — both apply.
Architecture Overview
The Three Inputs
-
Migration History (
django_migrationstable)- What Django thinks happened
- Which migrations are recorded as applied, and in what order
- No schema details—just names and app labels
-
Migration Code (migration files on disk:
migrations/*.py)- What the project currently says should happen
- The operations that were supposed to run
- Detects: edited migrations, squashed migrations, rewritten history
-
Live Database Schema (database introspection)
- What actually exists right now
- Ground truth: tables, columns, indexes, constraints
- The reality that everything else must match
The Two Comparisons
(1) Migration history
│
│ 🔍 Comparison A: Trust Verification
▼
(2) Migration code
│
│ produces expected schema
▼
Expected schema
│
│ 🔍 Comparison B: Reality Check
▼
(3) Live database schema
🔍 Comparison A: Trust Verification
Migration history ↔ Migration code
Detects:
- Modified migration files
- Missing migration files
- Fake-applied migrations
- Squash mismatches
Answers: "Can we trust the migration history at all?"
🔍 Comparison B: Reality Check
Expected schema ↔ Actual schema
Detects:
- Schema drift
- Manual database edits
- Broken legacy assumptions
- Missing/extra tables
- Column type mismatches
Development
Setup
# Clone the repository
git clone https://github.com/yourusername/django-migration-audit.git
cd django-migration-audit
# Install uv (if not already installed)
# Linux/Mac:
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows:
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"
# Setup development environment
uv venv
uv sync
Running Tests
# Run all tests
uv run pytest
# Run with coverage
uv run pytest --cov=django_migration_audit --cov-report=html
# Run specific test file
uv run pytest src/django_migration_audit/tests/unit/test_loader.py
Code Quality
# Format code
uv run ruff format
# Lint code
uv run ruff check
# Fix linting issues
uv run ruff check --fix
Pre-commit Hooks
This project uses pre-commit to automatically check code quality before commits.
To set up the hooks:
# Install the hooks
uv run pre-commit install
Now, pre-commit will run automatically on git commit. You can also run it manually against all files:
uv run pre-commit run --all-files
Contributing
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Run tests and linting
- Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
BSD-3-Clause - see LICENSE file for details.
Credits
Created by Johanan Oppong Amoateng
Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Release files for django-migration-audit 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_migration_audit-0.3.0.tar.gz | 20.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_migration_audit-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 45.9 kB
Release files / django_migration_audit-0.3.0.tar.gz
| Download URL | django_migration_audit-0.3.0.tar.gz |
|---|---|
| Size | 20.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eee9d4aca2448df3e4d96ef9fa4bc376c27cfd35d893224f594f9b73bb18372c
|
|
BLAKE2b-256 checksum How to use checksums |
9f07583171357c77350d2fba0639a4ee8f8c597f7e4f0824a3af8a18ddd796d9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 5, 2026.
Transparency logRelease files / django_migration_audit-0.3.0-py3-none-any.whl
| Download URL | django_migration_audit-0.3.0-py3-none-any.whl |
|---|---|
| Size | 25.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ace36b60f40549365e2152b70ce60a2ab34a606aa598b80bc88f6fca6966c4f2
|
|
BLAKE2b-256 checksum How to use checksums |
f0ac07d7f296bc737baea15b471b1e63e218949e21b788e506508698310ca551
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.7
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Mar 5, 2026.
Transparency log