Metadata-Version: 2.4
Name: django-migration-audit
Version: 0.3.0
Summary: A forensic Django tool that verifies whether a live database schema is historically consistent with its applied migrations.
Project-URL: Download, https://pypi.org/project/django-migration-audit/
Project-URL: Documentation, https://django-migration-audit.readthedocs.io/
Project-URL: Homepage, https://github.com/JohananOppongAmoateng/django-migration-audit
Project-URL: Repository, https://github.com/JohananOppongAmoateng/django-migration-audit
Project-URL: Issues, https://github.com/JohananOppongAmoateng/django-migration-audit/issues
Project-URL: Changelog, https://github.com/JohananOppongAmoateng/django-migration-audit/releases
Author-email: Johanan Oppong Amoateng <johananoppongamoateng2001@gmail.com>
License-Expression: BSD-3-Clause
License-File: LICENSE
Keywords: audit,consistency,database,django,forensic,migrations,schema
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Web Environment
Classifier: Framework :: Django
Classifier: Framework :: Django :: 4.2
Classifier: Framework :: Django :: 5.2
Classifier: Framework :: Django :: 6.0
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Database
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: django>=4.2
Provides-Extra: docs
Requires-Dist: doc8>=1.0.0; extra == 'docs'
Requires-Dist: myst-parser>=2.0.0; extra == 'docs'
Requires-Dist: sphinx-autobuild>=2024.0.0; extra == 'docs'
Requires-Dist: sphinx-rtd-theme>=2.0.0; extra == 'docs'
Requires-Dist: sphinx>=7.0.0; extra == 'docs'
Description-Content-Type: text/markdown

# django-migration-audit

**A forensic Django tool that verifies whether a live database schema is historically consistent with its applied migrations.**

[![License](https://img.shields.io/badge/license-BSD--3--Clause-blue.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
[![Django](https://img.shields.io/badge/django-4.2+-green.svg)](https://www.djangoproject.com/)
[![Latest on Django Packages](https://img.shields.io/badge/PyPI--django--migration--audit--tags--8c3c26.svg)](https://djangopackages.org/packages/p/django-migration-audit/)


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

```bash
pip install django-migration-audit
```

Or install from source:

```bash
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`:

```python
INSTALLED_APPS = [
    # ... other apps
    'django_migration_audit',
]
```

## Quick Start

### Basic Usage

```bash
# 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
`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)

```bash
# 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)

```python
# 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

```bash
# 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

```bash
# 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

```bash
# 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](https://pre-commit.com/) to automatically check code quality before commits.

To set up the hooks:

```bash
# 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:

```bash
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](LICENSE) file for details.

## Credits

Created by Johanan Oppong Amoateng

## Support

- **Issues**: [GitHub Issues](https://github.com/JohananOppongAmoateng/django-migration-audit/issues)
- **Discussions**: [GitHub Discussions](https://github.com/JohananOppongAmoateng/django-migration-audit/discussions)
