Skip to main content

Detect unsafe Django migrations before they break production

Project description

Django Safe Migrations

PyPI version Downloads CI codecov Python Versions Django Versions License: MIT

Detect unsafe Django migrations before they break production.

Django Safe Migrations analyzes your Django migrations and warns you about operations that could cause downtime, lock tables, or cause data loss in production environments.

Features

  • Detect unsafe operations before they reach production
  • PostgreSQL-aware rules for concurrent index creation and more
  • Clear fix suggestions with safe migration patterns
  • Multiple output formats: Console (with colors), JSON, GitHub Actions annotations
  • Easy CI/CD integration with GitHub Actions and pre-commit hooks
  • Configurable rules to match your deployment strategy

Rules

Rule ID Name Severity Description
SM001 not_null_without_default ERROR Adding NOT NULL column without default will lock table
SM002 drop_column_unsafe WARNING Dropping column while old code may reference it
SM003 drop_table_unsafe WARNING Dropping table while old code may reference it
SM004 alter_column_type WARNING Changing column type may rewrite table
SM005 add_foreign_key_validates WARNING FK constraint validates existing rows (locks)
SM006 rename_column INFO Column rename may break old code during deployment
SM007 run_sql_unsafe WARNING RunSQL without reverse_sql is not reversible
SM008 large_data_migration INFO Data migration may be slow on large tables
SM009 add_unique_constraint ERROR Adding unique constraint requires full table scan
SM010 index_not_concurrent ERROR Index creation without CONCURRENTLY (PostgreSQL)
SM011 unique_constraint_not_concurrent ERROR Unique constraint without concurrent index
SM012 enum_add_value_transaction ERROR Adding enum value inside transaction (PostgreSQL)
SM013 alter_varchar_length WARNING Decreasing VARCHAR length rewrites table
SM014 rename_model WARNING Model rename may break FKs and external references
SM015 alter_unique_together WARNING Deprecated in favor of UniqueConstraint
SM016 run_python_no_reverse INFO RunPython without reverse_code is not reversible
SM017 add_check_constraint WARNING Check constraint validates all existing rows

Installation

pip install django-safe-migrations

Add to your INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    'django_safe_migrations',
]

Usage

Management Command

# Check all migrations
python manage.py check_migrations

# Check specific apps
python manage.py check_migrations myapp otherapp

# Only check unapplied migrations
python manage.py check_migrations --new-only

# JSON output for CI
python manage.py check_migrations --format=json

# GitHub Actions annotations
python manage.py check_migrations --format=github

# Fail on warnings too
python manage.py check_migrations --fail-on-warning

Example Output

Found 2 migration issue(s):

โœ– ERROR [SM001] myapp/migrations/0002_add_email.py:15
   Adding NOT NULL field 'email' to 'user' without a default value will lock the table
   Operation: AddField(user.email)

   ๐Ÿ’ก Suggestion:
      Safe pattern for adding NOT NULL field:

      1. Migration 1 - Add field as nullable:
         migrations.AddField(
             model_name='user',
             name='email',
             field=models.CharField(max_length=255, null=True),
         )

      2. Data migration - Backfill existing rows in batches

      3. Migration 3 - Add NOT NULL constraint:
         migrations.AlterField(
             model_name='user',
             name='email',
             field=models.CharField(max_length=255, null=False),
         )

โš  WARNING [SM002] myapp/migrations/0003_remove_old.py:10
   Dropping column 'old_field' from 'user' - ensure all code references have been removed first
   Operation: RemoveField(user.old_field)

โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Summary: 1 error(s), 1 warning(s)

๐Ÿ”„ CI/CD Integration

GitHub Actions

# .github/workflows/check-migrations.yml
name: Check Migrations

on:
  pull_request:
    paths:
      - "**/migrations/**"

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install dependencies
        run: pip install django-safe-migrations Django

      - name: Check migrations
        run: python manage.py check_migrations --format=github --fail-on-warning

Pre-commit Hook

# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: check-migrations
        name: Check Django migrations
        entry: python manage.py check_migrations --new-only
        language: system
        types: [python]
        pass_filenames: false

โš™๏ธ Configuration

Command Options

Option Description
--format Output format: console, json, github
--fail-on-warning Exit with error code on warnings
--new-only Only check unapplied migrations
--no-suggestions Hide fix suggestions
--exclude-apps Apps to exclude from checking
--include-django-apps Include Django's built-in apps

Programmatic Usage

from django_safe_migrations import MigrationAnalyzer

analyzer = MigrationAnalyzer()

# Analyze all migrations
issues = analyzer.analyze_all()

# Analyze specific app
issues = analyzer.analyze_app('myapp')

# Analyze only new migrations
issues = analyzer.analyze_new_migrations()

for issue in issues:
    print(f"[{issue.rule_id}] {issue.message}")
    if issue.suggestion:
        print(f"Suggestion: {issue.suggestion}")

๐Ÿ“š Safe Migration Patterns

Adding a NOT NULL Column

โŒ Unsafe:

migrations.AddField(
    model_name='user',
    name='email',
    field=models.CharField(max_length=255),  # NOT NULL, no default!
)

โœ… Safe:

# Migration 1: Add nullable field
migrations.AddField(
    model_name='user',
    name='email',
    field=models.CharField(max_length=255, null=True),
)

# Migration 2: Backfill data (data migration)
def backfill_emails(apps, schema_editor):
    User = apps.get_model('myapp', 'User')
    User.objects.filter(email__isnull=True).update(email='default@example.com')

migrations.RunPython(backfill_emails)

# Migration 3: Add NOT NULL constraint
migrations.AlterField(
    model_name='user',
    name='email',
    field=models.CharField(max_length=255),
)

Creating an Index (PostgreSQL)

โŒ Unsafe:

migrations.AddIndex(
    model_name='user',
    index=models.Index(fields=['email'], name='user_email_idx'),
)

โœ… Safe:

from django.contrib.postgres.operations import AddIndexConcurrently

class Migration(migrations.Migration):
    atomic = False  # Required!

    operations = [
        AddIndexConcurrently(
            model_name='user',
            index=models.Index(fields=['email'], name='user_email_idx'),
        ),
    ]

๐Ÿค Contributing

Contributions are welcome! Please read our Contributing Guide for details.

# Clone the repo
git clone https://github.com/YasserShkeir/django-safe-migrations.git
cd django-safe-migrations

# Install dev dependencies
pip install -e ".[dev]"
pre-commit install

# Run tests
pytest

# Run linters
make lint

๐Ÿ“„ License

MIT License - see LICENSE for details.

๏ฟฝ Support

If this project helps you ship safer migrations, consider supporting its development:

Sponsor

๏ฟฝ๐Ÿ™ Acknowledgments

Inspired by:

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_safe_migrations-0.1.2.tar.gz (65.9 kB view details)

Uploaded Source

Built Distribution

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

django_safe_migrations-0.1.2-py3-none-any.whl (38.1 kB view details)

Uploaded Python 3

File details

Details for the file django_safe_migrations-0.1.2.tar.gz.

File metadata

  • Download URL: django_safe_migrations-0.1.2.tar.gz
  • Upload date:
  • Size: 65.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for django_safe_migrations-0.1.2.tar.gz
Algorithm Hash digest
SHA256 ae45c7c8fe97163b7cf214f70ddbf5ec379207d8a0e8a076012d82ce56dc5a5d
MD5 7a576ca505a1d57e1fc027f34c9db7eb
BLAKE2b-256 84236e90b0fcf85493520f82d5a3805293993048ccef2a2b04fed2de8a0513de

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_safe_migrations-0.1.2.tar.gz:

Publisher: publish.yml on YasserShkeir/django-safe-migrations

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_safe_migrations-0.1.2-py3-none-any.whl.

File metadata

File hashes

Hashes for django_safe_migrations-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 00f7ebb82c174031b452b5412e0f7339399ec1b3023f98e74ba73fa3340f169b
MD5 4967eb01a4d546010f085e44b0a7e525
BLAKE2b-256 918119a56ecb36028c61a66e1ff0d590ffb367c3a7f25af085c5915e1f05c4d1

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_safe_migrations-0.1.2-py3-none-any.whl:

Publisher: publish.yml on YasserShkeir/django-safe-migrations

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