A forensic Django tool that verifies whether a live database schema is historically consistent with its applied migrations.
Project description
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
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_migration_audit-0.3.0.tar.gz.
File metadata
- Download URL: django_migration_audit-0.3.0.tar.gz
- Upload date:
- Size: 20.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eee9d4aca2448df3e4d96ef9fa4bc376c27cfd35d893224f594f9b73bb18372c
|
|
| MD5 |
78984551f3daf27237c116403f1c39a8
|
|
| BLAKE2b-256 |
9f07583171357c77350d2fba0639a4ee8f8c597f7e4f0824a3af8a18ddd796d9
|
Provenance
The following attestation bundles were made for django_migration_audit-0.3.0.tar.gz:
Publisher:
release.yml on JohananOppongAmoateng/django-migration-audit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_migration_audit-0.3.0.tar.gz -
Subject digest:
eee9d4aca2448df3e4d96ef9fa4bc376c27cfd35d893224f594f9b73bb18372c - Sigstore transparency entry: 1035947189
- Sigstore integration time:
-
Permalink:
JohananOppongAmoateng/django-migration-audit@56f1094841fcdd7c307e257858663bae8b8d5e3c -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/JohananOppongAmoateng
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@56f1094841fcdd7c307e257858663bae8b8d5e3c -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_migration_audit-0.3.0-py3-none-any.whl.
File metadata
- Download URL: django_migration_audit-0.3.0-py3-none-any.whl
- Upload date:
- Size: 25.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ace36b60f40549365e2152b70ce60a2ab34a606aa598b80bc88f6fca6966c4f2
|
|
| MD5 |
08013327df46e2e7b72645a4ca96449a
|
|
| BLAKE2b-256 |
f0ac07d7f296bc737baea15b471b1e63e218949e21b788e506508698310ca551
|
Provenance
The following attestation bundles were made for django_migration_audit-0.3.0-py3-none-any.whl:
Publisher:
release.yml on JohananOppongAmoateng/django-migration-audit
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_migration_audit-0.3.0-py3-none-any.whl -
Subject digest:
ace36b60f40549365e2152b70ce60a2ab34a606aa598b80bc88f6fca6966c4f2 - Sigstore transparency entry: 1035947231
- Sigstore integration time:
-
Permalink:
JohananOppongAmoateng/django-migration-audit@56f1094841fcdd7c307e257858663bae8b8d5e3c -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/JohananOppongAmoateng
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@56f1094841fcdd7c307e257858663bae8b8d5e3c -
Trigger Event:
push
-
Statement type: