Skip to main content

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.

License Python Django Latest on Django Packages

⚠️ 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:

  1. It reads the migration files currently on disk.
  2. 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.
  3. 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 serialidentity 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

  1. Migration History (django_migrations table)

    • What Django thinks happened
    • Which migrations are recorded as applied, and in what order
    • No schema details—just names and app labels
  2. 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
  3. 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:

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Run tests and linting
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

License

BSD-3-Clause - see LICENSE file for details.

Credits

Created by Johanan Oppong Amoateng

Support

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

django_migration_audit-0.3.0.tar.gz (20.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_migration_audit-0.3.0-py3-none-any.whl (25.3 kB view details)

Uploaded Python 3

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

Hashes for django_migration_audit-0.3.0.tar.gz
Algorithm Hash digest
SHA256 eee9d4aca2448df3e4d96ef9fa4bc376c27cfd35d893224f594f9b73bb18372c
MD5 78984551f3daf27237c116403f1c39a8
BLAKE2b-256 9f07583171357c77350d2fba0639a4ee8f8c597f7e4f0824a3af8a18ddd796d9

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_migration_audit-0.3.0.tar.gz:

Publisher: release.yml on JohananOppongAmoateng/django-migration-audit

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file django_migration_audit-0.3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_migration_audit-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ace36b60f40549365e2152b70ce60a2ab34a606aa598b80bc88f6fca6966c4f2
MD5 08013327df46e2e7b72645a4ca96449a
BLAKE2b-256 f0ac07d7f296bc737baea15b471b1e63e218949e21b788e506508698310ca551

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page