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 found1— lint violations found (errors). Warnings alone do NOT cause exit code 1 unless--warnings-as-errorsis 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):
- CLI flags (
--select,--ignore,--warnings-as-errors) zero-downtime-migrations.tomlin the current directorypyproject.toml[tool.zdm]section- 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 --checkto 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
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 Distributions
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_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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c644a0e146bc1bd3ddaac48c7eb20d758e8d5a4f2101f7864a5526ef7cdae3bc
|
|
| MD5 |
1c24f19155aa27fdf9663953945917e0
|
|
| BLAKE2b-256 |
095005e23f163a6908b640640833376dd4481c979f12a8c8c85b7db73ddc1704
|
Provenance
The following attestation bundles were made for django_zdm-0.3.1.tar.gz:
Publisher:
release.yml on Photoroom/zero-downtime-migrations
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_zdm-0.3.1.tar.gz -
Subject digest:
c644a0e146bc1bd3ddaac48c7eb20d758e8d5a4f2101f7864a5526ef7cdae3bc - Sigstore transparency entry: 1262353689
- Sigstore integration time:
-
Permalink:
Photoroom/zero-downtime-migrations@5d3034584abbb9a04d2162d861d1c3d1271b47e7 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/Photoroom
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5d3034584abbb9a04d2162d861d1c3d1271b47e7 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a1a22976cd9c1afac584c35e91fe6dbad8b3054d69c79252490773200121f076
|
|
| MD5 |
53bea66577a7b7a2507821dc6e738f90
|
|
| BLAKE2b-256 |
b5d270d50ae9b1c9ab3842ad21c338da4fcda368268138d395d4693b7360e615
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_zdm-0.3.1-py3-none-win_arm64.whl -
Subject digest:
a1a22976cd9c1afac584c35e91fe6dbad8b3054d69c79252490773200121f076 - Sigstore transparency entry: 1262353737
- Sigstore integration time:
-
Permalink:
Photoroom/zero-downtime-migrations@5d3034584abbb9a04d2162d861d1c3d1271b47e7 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/Photoroom
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5d3034584abbb9a04d2162d861d1c3d1271b47e7 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1da9751eeeb403cbbda4e9cf1a9501a14037ca6ed10707913a26ca86a677f3a3
|
|
| MD5 |
7756b434c39488bf074a2246b449daea
|
|
| BLAKE2b-256 |
b917c3a8b64a3423f5b80fded4467590a7e37d7a0cae051ecdc6fb42d8202a0d
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_zdm-0.3.1-py3-none-win_amd64.whl -
Subject digest:
1da9751eeeb403cbbda4e9cf1a9501a14037ca6ed10707913a26ca86a677f3a3 - Sigstore transparency entry: 1262353708
- Sigstore integration time:
-
Permalink:
Photoroom/zero-downtime-migrations@5d3034584abbb9a04d2162d861d1c3d1271b47e7 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/Photoroom
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5d3034584abbb9a04d2162d861d1c3d1271b47e7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_zdm-0.3.1-py3-none-manylinux_2_28_aarch64.whl.
File metadata
- Download URL: django_zdm-0.3.1-py3-none-manylinux_2_28_aarch64.whl
- Upload date:
- Size: 6.5 MB
- Tags: Python 3, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
56737a63bcd3d135892781d1510532fde1dff0ef624fedaed9ab07ff8290f9b0
|
|
| MD5 |
01e6e65337eb8dde797ce80ef16411b9
|
|
| BLAKE2b-256 |
353f581db1f49ed9fd80045720111f0a25c4497138939e03bb38fefe29c167a2
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_zdm-0.3.1-py3-none-manylinux_2_28_aarch64.whl -
Subject digest:
56737a63bcd3d135892781d1510532fde1dff0ef624fedaed9ab07ff8290f9b0 - Sigstore transparency entry: 1262353750
- Sigstore integration time:
-
Permalink:
Photoroom/zero-downtime-migrations@5d3034584abbb9a04d2162d861d1c3d1271b47e7 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/Photoroom
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5d3034584abbb9a04d2162d861d1c3d1271b47e7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file django_zdm-0.3.1-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: django_zdm-0.3.1-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 5.3 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7511f0c3c2a53bb5aa77451eb450445ce04567cd543e374ce11d3b4f8ed46f64
|
|
| MD5 |
cff45db53e197c9f13297da21e7b7039
|
|
| BLAKE2b-256 |
567ebabeea38c9f83a0d1b88efc7174b510797164d33246767a36d2d4b37d64b
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
django_zdm-0.3.1-py3-none-macosx_11_0_arm64.whl -
Subject digest:
7511f0c3c2a53bb5aa77451eb450445ce04567cd543e374ce11d3b4f8ed46f64 - Sigstore transparency entry: 1262353723
- Sigstore integration time:
-
Permalink:
Photoroom/zero-downtime-migrations@5d3034584abbb9a04d2162d861d1c3d1271b47e7 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/Photoroom
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5d3034584abbb9a04d2162d861d1c3d1271b47e7 -
Trigger Event:
push
-
Statement type: