Migration conflict resolution, safety analysis, and linting for Django teams
Project description
django-migration-doctor
Safe, deterministic migration conflict resolution, safety analysis, and linting for Django teams.
The Problem
Django migrations work well in isolation but break down under team scale:
1. Migration Conflicts -- Parallel branches create conflicting migrations that require manual merging, with no insight into whether it's safe.
2. Unsafe Operations -- Migrations that drop columns, run raw SQL, or add non-nullable fields without defaults can cause downtime, but Django doesn't warn you.
3. Anti-patterns -- Non-reversible data migrations, table-locking index operations, and risky renames slip into production unreviewed.
4. Environment Drift -- Staging and production migration state diverge, causing broken deployments.
5. Circular Dependencies -- Cross-app migration dependencies create cycles that are hard to detect and debug.
django-migration-doctor adds the missing safety layer.
Installation
pip install django-migration-doctor
INSTALLED_APPS = [
...
"drmigrate",
]
Quick Start
# Health dashboard -- overview of all migration issues
python manage.py drmigrate
# CI mode -- run all checks, exit non-zero on failure
python manage.py drmigrate check
# Detect and analyze conflicts
python manage.py drmigrate conflicts
# Auto-generate merge migration (if safe)
python manage.py drmigrate conflicts --apply
# Lint migrations for anti-patterns
python manage.py drmigrate lint
# Classify all migrations by safety level
python manage.py drmigrate safety
Features
Migration Conflict Detection
Detects parallel migration branches and analyzes whether auto-merging is safe:
$ python manage.py drmigrate conflicts
[WARN] conflicts
------------------------------------------------------------
[WARN ] CONFLICT: Conflicting migrations in 'users': 0002_add_age, 0002_add_email (contains risky operations)
-> Review operations carefully, then run: drmigrate conflicts --apply
The analyzer checks for:
- Field-level conflicts (two branches modifying the same field)
- Operation safety (additive vs destructive)
- Common ancestors in the migration graph
Safety Classification
Every migration operation is classified:
| Level | Operations | Behavior |
|---|---|---|
| SAFE | CreateModel, AddField (nullable/with default), AddIndex, AddConstraint | No warnings |
| RISKY | AlterField, RenameField, RenameModel, RemoveIndex, non-nullable AddField | Warning |
| UNSAFE | DeleteModel, RemoveField, RunPython, RunSQL | Error |
$ python manage.py drmigrate safety
[WARN] safety
------------------------------------------------------------
[ERROR] SAFETY_UNSAFE: users.0005_data_backfill: UNSAFE operations (RunPython)
-> Review carefully before applying to production
[WARN ] SAFETY_RISKY: users.0004_rename_name: RISKY operations (RenameField)
-> Verify these operations won't cause issues in production
Summary: 3 safe, 1 risky, 1 unsafe (total: 5)
Migration Linting
Four built-in lint rules catch common anti-patterns:
| Rule | Name | Severity | What it catches |
|---|---|---|---|
| SM001 | non-reversible-migration | warning | RunPython/RunSQL without reverse code |
| SM002 | non-nullable-without-default | error | AddField that will fail on existing rows |
| SM003 | rename-detected | warning | RenameField/RenameModel that may break references |
| SM004 | potential-table-lock | warning | AddIndex without CONCURRENTLY (PostgreSQL) |
$ python manage.py drmigrate lint
[FAIL] lint
------------------------------------------------------------
[ERROR] SM002: AddField 'required_field' on 'user' is non-nullable without a default
-> Add null=True, or provide a default value, or use a two-step migration
[WARN ] SM001: RunPython operation without reverse_code in users.0005_backfill
-> Add a reverse_code function: RunPython(forward, reverse_code=reverse)
CI Integration
Run all checks in CI with a single command:
# Fail on errors (default)
python manage.py drmigrate check
# Fail on warnings too
python manage.py drmigrate check --fail-level=warning
# JSON output for tooling
python manage.py drmigrate check --format=json
# GitHub Actions annotations
python manage.py drmigrate check --format=github
Exit code is non-zero if issues are found at or above the fail level.
Configuration
Add a MIGRATION_DOCTOR dict to your Django settings:
MIGRATION_DOCTOR = {
# Disable specific lint rules
"DISABLED_RULES": ["SM003"],
# Tables known to be large (flags destructive ops as errors)
"LARGE_TABLES": ["users_user", "orders_order"],
# Minimum severity to fail CI check: "warning" or "error"
"FAIL_LEVEL": "error",
# Default output format: "text", "json", or "github"
"DEFAULT_FORMAT": "text",
# Custom lint rules (dotted import paths)
"EXTRA_LINT_RULES": [
"myapp.lint_rules.MyCustomRule",
],
}
Writing Custom Lint Rules
from drmigrate.linters.base import BaseLintRule, LintViolation
class NoRawSQL(BaseLintRule):
id = "CUSTOM001"
name = "no-raw-sql"
description = "Raw SQL is not allowed in migrations"
severity = "error"
def check(self, migration, migration_key):
from django.db.migrations.operations.special import RunSQL
violations = []
for i, op in enumerate(migration.operations):
if isinstance(op, RunSQL):
violations.append(LintViolation(
rule_id=self.id,
rule_name=self.name,
severity=self.severity,
message=f"Raw SQL found in {migration_key[0]}.{migration_key[1]}",
migration_key=migration_key,
operation_index=i,
suggestion="Use Django ORM operations instead",
))
return violations
Then register it:
MIGRATION_DOCTOR = {
"EXTRA_LINT_RULES": ["myapp.lint_rules.NoRawSQL"],
}
Command Reference
| Command | Description |
|---|---|
drmigrate |
Health dashboard (default) |
drmigrate check |
Run all checks for CI |
drmigrate conflicts |
Detect migration conflicts |
drmigrate conflicts --apply |
Generate merge migration |
drmigrate lint |
Run lint rules |
drmigrate lint --rule=SM001 |
Run a specific rule |
drmigrate lint --exclude SM003 |
Exclude rules |
drmigrate safety |
Classify all migrations |
Common flags:
| Flag | Description |
|---|---|
--app=<label> |
Filter to a specific Django app |
--format={text,json,github} |
Output format |
--verbosity={0,1,2,3} |
Verbosity level |
--no-color |
Disable colored output |
Compatibility
- Python 3.10+
- Django 4.2, 5.0, 5.1, 6.0
Roadmap
- Environment drift detection (staging vs production)
- Circular dependency detection
- Stale migration detection
- Migration squash analysis
- Graph visualization
- Pre-commit hooks
- Migration state locking
Contributing
Contributions are welcome. Good starting points:
- New lint rules
- Safety classification improvements
- Real-world edge case testing
- CI/CD integration examples
License
MIT
Project details
Release history Release notifications | RSS feed
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_doctor-0.1.0.tar.gz.
File metadata
- Download URL: django_migration_doctor-0.1.0.tar.gz
- Upload date:
- Size: 21.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
94075aefea893c0d96e71f715929a3ea509bd679450491db3620cdf55e238ea6
|
|
| MD5 |
6409b31dbb500c6e3f8648c0ff01c78e
|
|
| BLAKE2b-256 |
41498699845c82ba52bd7fbf0eb9a30ad8d2bf5404cc1f2665441db006ad65c2
|
File details
Details for the file django_migration_doctor-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_migration_doctor-0.1.0-py3-none-any.whl
- Upload date:
- Size: 26.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.8
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12f9a4d8f3d92896ab1ca953b449b0784beef32d48fcce25fe29e39b0c384bbe
|
|
| MD5 |
5695df30af3db83829f533a57bd515ac
|
|
| BLAKE2b-256 |
643271d8cbce6826b575ed519cbec7150ee123be3618058a1f19883d70c5244b
|