Skip to main content

A PostgreSQL migration safety linter for Django

Project description

zero-downtime-migrations (zdm)

A PostgreSQL migration safety linter for Django.

Why

Deploying database migrations without downtime requires careful attention to how PostgreSQL acquires locks. Operations like adding an index, altering a column to NOT NULL, or adding a foreign key can lock tables for extended periods on large datasets, blocking reads and writes and causing outages. zdm statically analyzes Django migration files to catch these unsafe patterns before they reach production, helping teams ship schema changes safely during normal deployments.

What

A standalone Rust CLI tool that statically analyzes Django migration files to catch unsafe patterns that cause table locks, outages, and data loss on large PostgreSQL databases. Distributed like ruff/uv — a single fast binary, installable via pip, uvx, or standalone download.

Supports Django 3.2+ — zdm parses migration files directly without importing Django, so it works with any Django version and doesn't require Django to be installed.

Installation

# Install via pip
pip install zero-downtime-migrations

# Or use uvx to run without installing
uvx zero-downtime-migrations .

# Or install with pipx
pipx install zero-downtime-migrations

Usage

# These are equivalent
zero-downtime-migrations app/migrations/0042_add_index.py
zdm app/migrations/0042_add_index.py

# Lint all migrations in a directory
zdm app/migrations/

# Lint all migrations in the project
zdm .

# Diff mode: lint changed migrations in a PR
zdm --diff origin/main

# Staged diff mode: lint changes being committed by pre-commit
zdm --diff-staged origin/main

# Output formats
zdm --output-format json .
zdm --output-format compact .

# Select/ignore specific rules
zdm --select R001,R003 .
zdm --ignore R008 .

# Show explanation for a rule
zdm rule R001

# Treat warnings as errors
zdm --warnings-as-errors .

Exit Codes

  • 0 — no issues found
  • 1 — lint violations found (errors). Warnings alone do NOT cause exit code 1 unless --warnings-as-errors is set.
  • 2 — tool error (bad arguments, config parse failure, invalid file path)

Rules

Rule Name Severity Description
R001 non-concurrent-add-index Error Use AddIndexConcurrently instead of AddIndex
R002 unique-constraint-without-index Error Unique constraints should have a concurrent index
R003 runsql-create-index Error Use AddIndexConcurrently instead of raw SQL CREATE INDEX
R004 missing-atomic-false Error Non-atomic migrations require atomic = False
R005 remove-field-without-separate Error Use SeparateDatabaseAndState to remove fields safely
R006 add-field-foreign-key Warning Adding FK creates index and validates constraint
R007 fk-without-concurrent-index Warning Foreign keys should have a concurrent index
R008 disallowed-file-changes Warning Don't change app code alongside migrations
R009 separate-db-state-same-pr Warning Don't deploy both steps of SeparateDatabaseAndState together
R010 add-field-not-null Error Adding NOT NULL field without default rewrites table
R011 rename-field Warning Renaming fields can break running code
R012 irreversible-run-python Warning RunPython should have a reverse function
R013 irreversible-run-sql Warning RunSQL should have a reverse SQL
R014 model-imports Error Don't import models in RunPython
R015 alter-field-not-null Error Changing field to NOT NULL validates all rows
R016 non-concurrent-remove-index Error Use RemoveIndexConcurrently instead of RemoveIndex
R017 non-concurrent-add-constraint Warning Adding CHECK/FK constraints validates all rows

CreateModel Exemption

Several rules (R001, R002, R006, R007, R010, R017) automatically exempt operations that target models created in the same migration. This is because operations on newly created (empty) tables don't cause the locking issues these rules detect.

For example, this migration will NOT trigger R001:

class Migration(migrations.Migration):
    operations = [
        migrations.CreateModel(
            name='Order',
            fields=[('id', models.AutoField(primary_key=True))],
        ),
        migrations.AddIndex(  # Exempt: 'order' was just created above
            model_name='order',
            index=models.Index(fields=['created_at'], name='order_idx'),
        ),
    ]

R015 Limitation

R015 (alter-field-not-null) cannot determine whether a field was previously nullable. It flags ALL AlterField operations where the resulting field is NOT NULL, which may produce false positives when the field was already NOT NULL. This is a fundamental limitation of static analysis without schema history. Use # zdm: ignore R015 inline comments for legitimate AlterField operations that don't change nullability.

Configuration

Configure via pyproject.toml or zero-downtime-migrations.toml:

[tool.zdm]
select = ["R001", "R002"]
ignore = ["R008"]
warnings-as-errors = false
allowed-file-patterns = ["*.txt", "*.md", "models.py"]
exclude = ["**/test_migrations/**"]

Configuration Precedence

Settings are applied in this order (highest to lowest priority):

  1. CLI flags (--select, --ignore, --warnings-as-errors)
  2. zero-downtime-migrations.toml in the current directory
  3. pyproject.toml [tool.zdm] section
  4. Default values

CLI flags always override config file settings. If both zero-downtime-migrations.toml and pyproject.toml exist, the standalone file takes precedence.

Pre-commit Integration

Add to your .pre-commit-config.yaml:

repos:
  - repo: https://github.com/Photoroom/zero-downtime-migrations
    rev: v0.1.0
    hooks:
      - id: zdm

Or use diff mode to only check changed migrations:

repos:
  - repo: https://github.com/Photoroom/zero-downtime-migrations
    rev: v0.1.0
    hooks:
      - id: zdm-diff

The zdm-diff hook uses --diff-staged so it checks the staged index that pre-commit is validating, rather than the previous HEAD commit.

GitHub Actions

- name: Install zdm
  run: pip install zero-downtime-migrations

- name: Lint migrations
  run: zdm --diff origin/main

Comparison with Other Tools

zdm django-migration-linter Django's makemigrations --check
Requires Django installed No Yes Yes
Requires project setup No Yes (settings.py) Yes (full environment)
Checks for missing migrations No No Yes
Checks for unsafe operations Yes (17 rules) Yes (~8 rules) No
Can run without database Yes Yes No
Language Rust Python Python

When to use what:

  • Use makemigrations --check to ensure all model changes have migrations
  • Use zdm or django-migration-linter to catch unsafe migration patterns
  • zdm is useful when you want to run checks in CI without setting up Django, or when you need the additional rules (NOT NULL alterations, RenameField, irreversible migrations, RemoveIndex)

License

MIT

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_zdm-0.3.2.tar.gz (59.8 kB view details)

Uploaded Source

Built Distributions

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

django_zdm-0.3.2-py3-none-win_arm64.whl (2.4 MB view details)

Uploaded Python 3Windows ARM64

django_zdm-0.3.2-py3-none-win_amd64.whl (2.6 MB view details)

Uploaded Python 3Windows x86-64

django_zdm-0.3.2-py3-none-manylinux_2_28_aarch64.whl (6.5 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ ARM64

django_zdm-0.3.2-py3-none-macosx_11_0_arm64.whl (5.4 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

File details

Details for the file django_zdm-0.3.2.tar.gz.

File metadata

  • Download URL: django_zdm-0.3.2.tar.gz
  • Upload date:
  • Size: 59.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_zdm-0.3.2.tar.gz
Algorithm Hash digest
SHA256 6d4a7f5f87dc8226e5780409ed0aa8c8b202086ed287c513cbc680b6477ae771
MD5 efa055b8db66585425380d5dcad6d304
BLAKE2b-256 c0daa432ebba609978b540f2cd43e4f5dbc6809feed561ffde7c9e336598b948

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_zdm-0.3.2.tar.gz:

Publisher: release.yml on Photoroom/zero-downtime-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_zdm-0.3.2-py3-none-win_arm64.whl.

File metadata

  • Download URL: django_zdm-0.3.2-py3-none-win_arm64.whl
  • Upload date:
  • Size: 2.4 MB
  • Tags: Python 3, Windows ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_zdm-0.3.2-py3-none-win_arm64.whl
Algorithm Hash digest
SHA256 559001b5c7e90f363e1dc9ccdcb9724dadaa193af093888885c05cc896c899c7
MD5 03d982ab318ea5c9a2c1d0d07f772f43
BLAKE2b-256 9de836117e183c607e69b356dcf0d2a9db09203256b1b5af0ad51890763ef7ab

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_zdm-0.3.2-py3-none-win_arm64.whl:

Publisher: release.yml on Photoroom/zero-downtime-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_zdm-0.3.2-py3-none-win_amd64.whl.

File metadata

  • Download URL: django_zdm-0.3.2-py3-none-win_amd64.whl
  • Upload date:
  • Size: 2.6 MB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for django_zdm-0.3.2-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 9b48282a8aefcf4f68df4ea342fd5e7d39255e43db113fa4180499d9d253a057
MD5 c3d9345d73ba9544bffeafe75f398e8a
BLAKE2b-256 0c303e72b7a75be44af510e247789c28b1017be9c6e7462b4113f27b194110de

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_zdm-0.3.2-py3-none-win_amd64.whl:

Publisher: release.yml on Photoroom/zero-downtime-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_zdm-0.3.2-py3-none-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for django_zdm-0.3.2-py3-none-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 296fc778da38a6193c02234d2d58228074e100d0343c35d04e23559b44ee72d7
MD5 18d36127c99e86c31acb1ae172480822
BLAKE2b-256 8f8b71a67c140cc13f9888cca35022ff9b8bf045a9657ee0e9c25d1a792f210a

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_zdm-0.3.2-py3-none-manylinux_2_28_aarch64.whl:

Publisher: release.yml on Photoroom/zero-downtime-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_zdm-0.3.2-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for django_zdm-0.3.2-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 b04e10e07dde573bd921c1f757d42cd4c0dca1799a4959675c41c1b37a42a31d
MD5 67a41b89b7e9f7c1ad4e08ed6dce3f8e
BLAKE2b-256 cd492a4e3f2f5cd4932c4a8d6658815cc6fd701594ccd59b661dc556c4959cb1

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_zdm-0.3.2-py3-none-macosx_11_0_arm64.whl:

Publisher: release.yml on Photoroom/zero-downtime-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