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

# 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"]
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

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.1.tar.gz (58.0 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.1-py3-none-win_arm64.whl (2.4 MB view details)

Uploaded Python 3Windows ARM64

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

Uploaded Python 3Windows x86-64

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

Uploaded Python 3manylinux: glibc 2.28+ ARM64

django_zdm-0.3.1-py3-none-macosx_11_0_arm64.whl (5.3 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

File details

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

File metadata

  • Download URL: django_zdm-0.3.1.tar.gz
  • Upload date:
  • Size: 58.0 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.1.tar.gz
Algorithm Hash digest
SHA256 c644a0e146bc1bd3ddaac48c7eb20d758e8d5a4f2101f7864a5526ef7cdae3bc
MD5 1c24f19155aa27fdf9663953945917e0
BLAKE2b-256 095005e23f163a6908b640640833376dd4481c979f12a8c8c85b7db73ddc1704

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_zdm-0.3.1.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.1-py3-none-win_arm64.whl.

File metadata

  • Download URL: django_zdm-0.3.1-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.1-py3-none-win_arm64.whl
Algorithm Hash digest
SHA256 a1a22976cd9c1afac584c35e91fe6dbad8b3054d69c79252490773200121f076
MD5 53bea66577a7b7a2507821dc6e738f90
BLAKE2b-256 b5d270d50ae9b1c9ab3842ad21c338da4fcda368268138d395d4693b7360e615

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_zdm-0.3.1-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.1-py3-none-win_amd64.whl.

File metadata

  • Download URL: django_zdm-0.3.1-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.1-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 1da9751eeeb403cbbda4e9cf1a9501a14037ca6ed10707913a26ca86a677f3a3
MD5 7756b434c39488bf074a2246b449daea
BLAKE2b-256 b917c3a8b64a3423f5b80fded4467590a7e37d7a0cae051ecdc6fb42d8202a0d

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_zdm-0.3.1-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.1-py3-none-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for django_zdm-0.3.1-py3-none-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 56737a63bcd3d135892781d1510532fde1dff0ef624fedaed9ab07ff8290f9b0
MD5 01e6e65337eb8dde797ce80ef16411b9
BLAKE2b-256 353f581db1f49ed9fd80045720111f0a25c4497138939e03bb38fefe29c167a2

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_zdm-0.3.1-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.1-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for django_zdm-0.3.1-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 7511f0c3c2a53bb5aa77451eb450445ce04567cd543e374ce11d3b4f8ed46f64
MD5 cff45db53e197c9f13297da21e7b7039
BLAKE2b-256 567ebabeea38c9f83a0d1b88efc7174b510797164d33246767a36d2d4b37d64b

See more details on using hashes here.

Provenance

The following attestation bundles were made for django_zdm-0.3.1-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