zdm 0.6.0__py3-none-win_amd64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -0,0 +1,348 @@
1
+ Metadata-Version: 2.4
2
+ Name: zdm
3
+ Version: 0.6.0
4
+ Classifier: Development Status :: 4 - Beta
5
+ Classifier: Environment :: Console
6
+ Classifier: Framework :: Django
7
+ Classifier: Framework :: Django :: 3.2
8
+ Classifier: Framework :: Django :: 4.0
9
+ Classifier: Framework :: Django :: 4.1
10
+ Classifier: Framework :: Django :: 4.2
11
+ Classifier: Framework :: Django :: 5.0
12
+ Classifier: Framework :: Django :: 5.1
13
+ Classifier: Framework :: Django :: 6.0
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: Microsoft :: Windows
18
+ Classifier: Operating System :: POSIX :: Linux
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.8
21
+ Classifier: Programming Language :: Python :: 3.9
22
+ Classifier: Programming Language :: Python :: 3.10
23
+ Classifier: Programming Language :: Python :: 3.11
24
+ Classifier: Programming Language :: Python :: 3.12
25
+ Classifier: Programming Language :: Python :: 3.13
26
+ Classifier: Programming Language :: Rust
27
+ Classifier: Topic :: Software Development :: Quality Assurance
28
+ Classifier: Topic :: Database
29
+ License-File: LICENSE
30
+ Summary: A PostgreSQL migration safety linter for Django, Alembic, and Aerich
31
+ Keywords: django,tortoise,aerich,postgresql,migrations,linter,database,safety
32
+ Author-email: Photoroom <eng@photoroom.com>
33
+ License: MIT
34
+ Requires-Python: >=3.8
35
+ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
36
+ Project-URL: Bug Tracker, https://github.com/Photoroom/zero-downtime-migrations/issues
37
+ Project-URL: Documentation, https://github.com/Photoroom/zero-downtime-migrations#readme
38
+ Project-URL: Homepage, https://github.com/Photoroom/zero-downtime-migrations
39
+ Project-URL: Repository, https://github.com/Photoroom/zero-downtime-migrations
40
+
41
+ # zero-downtime-migrations (zdm)
42
+
43
+ A PostgreSQL migration safety linter for Django, Alembic, and Aerich/Tortoise.
44
+
45
+ ## Why
46
+
47
+ 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 migrations and supported Alembic and Aerich revisions to catch these unsafe patterns before they reach production, helping teams ship schema changes safely during normal deployments.
48
+
49
+ ## What
50
+
51
+ A standalone Rust CLI tool that statically analyzes Django, Alembic, and Aerich 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.
52
+
53
+ **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.
54
+
55
+ ### Alembic support
56
+
57
+ zdm also discovers Alembic revision scripts directly under `alembic/versions/*.py`. It statically supports direct `op.*` calls in `upgrade()`:
58
+
59
+ - `create_table`, `create_index`, `drop_index`
60
+ - `create_foreign_key` and `create_check_constraint` (including `postgresql_not_valid=True`), plus `create_exclude_constraint`
61
+ - `alter_column(nullable=False)` and `alter_column(new_column_name=...)`
62
+ - `drop_column` and `execute("<static SQL>")`
63
+
64
+ Use `postgresql_concurrently=True` only inside the canonical boundary:
65
+
66
+ ```python
67
+ with op.get_context().autocommit_block():
68
+ op.create_index("jobs_state_idx", "jobs", ["state"], postgresql_concurrently=True)
69
+ ```
70
+
71
+ The Alembic path is intentionally static: zdm does not import or execute revision scripts, connect to a database, inspect SQLAlchemy models, resolve aliases/custom operations, or evaluate dynamic SQL. `op.execute` is inspected only when its first positional argument or `sqltext` keyword is a string literal; use explicit SQL when you want it checked.
72
+
73
+ ### Aerich/Tortoise support
74
+
75
+ zdm discovers Aerich revisions under `migrations/<app>/<number>_*.py`. It inspects literal SQL returned by `upgrade()` and literal SQL passed to `execute_statement(connection, sql)` from local helpers reached by `upgrade()`, including callbacks such as `run_with_lock_timeout(db, _upgrade_attempt)`. It maps PostgreSQL `CREATE/DROP INDEX`, `CREATE TABLE`, and `ALTER TABLE` column and constraint statements to the same safety rules used for Django and Alembic.
76
+
77
+ The Aerich path is static: zdm does not import Tortoise, execute migrations, connect to a database, resolve imports, or evaluate variables and f-strings. It follows only local helper names and literal positional `execute_statement` SQL, so qualified calls, keyword arguments and dynamically assembled SQL are not checked. `CREATE TABLE IF NOT EXISTS` is not considered a fresh-table exemption. `CREATE INDEX CONCURRENTLY` and `DROP INDEX CONCURRENTLY` are accepted, while R004 does not apply because Aerich exposes no equivalent to Django's `atomic = False` or Alembic's autocommit block.
78
+
79
+ ## Installation
80
+
81
+ > **Breaking change:** the `zero-downtime-migrations` command alias has been removed. Use `zdm`. (`alias zero-downtime-migrations=zdm` in your shell is a one-line workaround if you depended on the old name.)
82
+
83
+ ```bash
84
+ # Install via pip
85
+ pip install zdm
86
+
87
+ # Or use uvx to run without installing
88
+ uvx --from zdm zdm .
89
+
90
+ # Or install with pipx
91
+ pipx install zdm
92
+ ```
93
+
94
+ ## Usage
95
+
96
+ ```bash
97
+ # Lint a single migration
98
+ zdm app/migrations/0042_add_index.py
99
+ zdm alembic/versions/20260809_add_jobs.py
100
+ zdm migrations/models/1_20260823_add_jobs.py
101
+
102
+ # Lint all migrations in a directory
103
+ zdm app/migrations/
104
+
105
+ # Lint all migrations in the project
106
+ zdm .
107
+
108
+ # Diff mode: lint changed migrations in a PR
109
+ zdm --diff origin/main
110
+
111
+ # Staged diff mode: lint changes being committed by pre-commit
112
+ zdm --diff-staged origin/main
113
+
114
+ # Output formats
115
+ zdm --output-format json .
116
+ zdm --output-format compact .
117
+
118
+ # Select/ignore specific rules
119
+ zdm --select R001,R003 .
120
+ zdm --ignore R008 .
121
+
122
+ # Show explanation for a rule
123
+ zdm rule R001
124
+
125
+ # List every rule the binary recognises
126
+ zdm --list-rules
127
+
128
+ # Treat warnings as errors
129
+ zdm --warnings-as-errors .
130
+ ```
131
+
132
+ `--diff` compares the merge base to `HEAD` and reads file contents from the
133
+ `HEAD` tree, giving deterministic PR/CI results even when the worktree is
134
+ dirty. `--diff-staged` compares the same merge base to the index and reads the
135
+ staged blobs.
136
+
137
+ ### Exit Codes
138
+
139
+ - `0` — no issues found
140
+ - `1` — lint violations found (errors). Warnings alone do NOT cause exit code 1 unless `--warnings-as-errors` is set.
141
+ - `2` — tool error (bad arguments, config parse failure, invalid file path)
142
+
143
+ ### JSON Output Schema
144
+
145
+ `zdm --output-format json` writes a single JSON object to stdout:
146
+
147
+ ```json
148
+ {
149
+ "diagnostics": [
150
+ {
151
+ "rule_id": "R001",
152
+ "rule_name": "non-concurrent-add-index",
153
+ "severity": "error",
154
+ "message": "Use AddIndexConcurrently instead of AddIndex …",
155
+ "path": "app/migrations/0001_bad.py",
156
+ "line": 8,
157
+ "column": 9,
158
+ "help": "Replace migrations.AddIndex with …"
159
+ }
160
+ ],
161
+ "summary": { "total": 1, "errors": 1, "warnings": 0 }
162
+ }
163
+ ```
164
+
165
+ `severity` is `"error"` or `"warning"`. `help` is `null` when the
166
+ rule has no help text. The schema is pinned by the integration
167
+ test suite — every field above is guaranteed on every diagnostic.
168
+
169
+ ## Rules
170
+
171
+ | Rule | Name | Severity | Description |
172
+ |------|------|----------|-------------|
173
+ | R001 | non-concurrent-add-index | Error | Use `AddIndexConcurrently` instead of `AddIndex` |
174
+ | R002 | unique-constraint-without-index | Error | Unique constraints should have a concurrent index |
175
+ | R003 | runsql-create-index | Error | Use `AddIndexConcurrently` instead of raw SQL `CREATE INDEX` |
176
+ | R004 | missing-atomic-false | Error | Non-atomic migrations require `atomic = False` |
177
+ | R005 | remove-field-without-separate | Error | Use `SeparateDatabaseAndState` to remove fields safely |
178
+ | R006 | add-field-foreign-key | Error | Adding FK creates index and validates constraint (merged R007) |
179
+ | R008 | disallowed-file-changes | Error | Don't change app code alongside migrations |
180
+ | R009 | separate-db-state-same-pr | Error | Don't deploy both steps of `SeparateDatabaseAndState` together |
181
+ | R010 | add-field-not-null | Error | Adding NOT NULL field without default rewrites table |
182
+ | R011 | rename-field | Error | Renaming fields can break running code |
183
+ | R012 | irreversible-run-python | Warning | `RunPython` should have a reverse function |
184
+ | R013 | irreversible-run-sql | Warning | `RunSQL` should have a reverse SQL |
185
+ | R014 | model-imports | Error | Don't import models in `RunPython` |
186
+ | R015 | alter-field-not-null | Warning | `AlterField` that sets NOT NULL may scan rows, and type changes may rewrite the table |
187
+ | R016 | non-concurrent-remove-index | Error | Use `RemoveIndexConcurrently` instead of `RemoveIndex` |
188
+ | R017 | non-concurrent-add-constraint | Error | CHECK constraint validates all rows; EXCLUDE constraint builds an index non-concurrently |
189
+
190
+ For Alembic revisions, zdm evaluates R001, R003-R005, R011, R015-R017 against direct `op.*` calls in `upgrade()`. For Aerich revisions, zdm evaluates R001-R002, R005-R006, R010-R011, and R015-R017 against supported literal PostgreSQL DDL reachable from `upgrade()`. In diff modes, changeset rule R008 also applies. The Django API references in this table apply only to Django; Alembic and Aerich diagnostics name their equivalent operations.
191
+
192
+ ### CreateModel Exemption
193
+
194
+ Several rules (R001, R002, R006, R010, R016, 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. The exemption is order-aware—a `CreateModel` that runs *after* the flagged op cannot retroactively exempt it—and follows `RenameModel` when the fresh table is renamed before a later operation.
195
+
196
+ > **Note:** R007 (`fk-without-concurrent-index`) was merged into R006 and retired. R006 now takes the conservative stance that a prebuilt concurrent index does not make a one-step `AddField(ForeignKey)` safe on an existing table. Split the rollout instead of relying on an index exemption.
197
+
198
+ For example, this migration will NOT trigger R001:
199
+
200
+ ```python
201
+ class Migration(migrations.Migration):
202
+ operations = [
203
+ migrations.CreateModel(
204
+ name='Order',
205
+ fields=[('id', models.AutoField(primary_key=True))],
206
+ ),
207
+ migrations.AddIndex( # Exempt: 'order' was just created above
208
+ model_name='order',
209
+ index=models.Index(fields=['created_at'], name='order_idx'),
210
+ ),
211
+ ]
212
+ ```
213
+
214
+ ### R015 Limitation
215
+
216
+ R015 (alter-field-not-null) cannot tell, from a single `AlterField` operation, whether the column was previously nullable. It flags any `AlterField` whose resulting field is NOT NULL, which catches a genuine nullable→NOT NULL transition (the dangerous case) alongside benign re-stipulations of an already-NOT-NULL column. Because static analysis has no schema history, the rule emits `Warning` rather than `Error` — surfaced for review without breaking CI. Add `# zdm: ignore R015` on operations you have verified are safe.
217
+
218
+ ### Inline Suppression
219
+
220
+ You can silence specific rules on a per-operation basis with a comment:
221
+
222
+ ```python
223
+ operations = [
224
+ # zdm: ignore R001
225
+ migrations.AddIndex(
226
+ model_name='order',
227
+ index=models.Index(fields=['created_at'], name='order_idx'),
228
+ ),
229
+ migrations.AlterField( # zdm: ignore R015, R010
230
+ model_name='product',
231
+ name='sku',
232
+ field=models.CharField(max_length=50),
233
+ ),
234
+ ]
235
+ ```
236
+
237
+ The comment can sit on the line just above the operation or on the same line as any line in the operation's range. Multiple rule IDs may be listed, separated by commas.
238
+
239
+ ## Configuration
240
+
241
+ Configure via `pyproject.toml` or `zero-downtime-migrations.toml`:
242
+
243
+ ```toml
244
+ [tool.zdm]
245
+ select = ["R001", "R002"]
246
+ ignore = ["R008"]
247
+ warnings-as-errors = false
248
+ allowed-file-patterns = ["*.txt", "*.md", "models.py"]
249
+ exclude = ["**/test_migrations/**"]
250
+ ```
251
+
252
+ ### Configuration Precedence
253
+
254
+ Settings are applied in this order (highest to lowest priority):
255
+
256
+ 1. **CLI flags** (`--select`, `--ignore`, `--warnings-as-errors`)
257
+ 2. **`zero-downtime-migrations.toml`** found in the current directory or a trusted repo ancestor
258
+ 3. **`pyproject.toml`** `[tool.zdm]` section in the same directory
259
+ 4. **Default values**
260
+
261
+ The config search starts in the current working directory. On Unix, if zdm is running inside a trusted git repository, it walks upward within that repository and stops at the first directory that contains `zero-downtime-migrations.toml` or a `pyproject.toml` with `[tool.zdm]`. The nearest `.git` is always the boundary; an untrusted boundary never falls through to an outer repository. A pyproject for another tool is ignored, so running `zdm` from `repo/apps/myapp/migrations/` still picks up `repo/zero-downtime-migrations.toml`. Without a trusted `.git` ancestor—and on Windows, where zdm cannot yet validate repository ACL ownership—only the current directory is checked. Config inputs must be regular UTF-8 files no larger than 1 MiB.
262
+
263
+ CLI flags always override config file settings. If both `zero-downtime-migrations.toml` and `pyproject.toml` exist in the same directory, the standalone file takes precedence; multi-level merging is not performed.
264
+
265
+ ## Pre-commit Integration
266
+
267
+ Install `zdm` in the environment where pre-commit runs, then call that installed
268
+ binary from your `.pre-commit-config.yaml`:
269
+
270
+ ```yaml
271
+ repos:
272
+ - repo: local
273
+ hooks:
274
+ - id: zdm
275
+ name: zdm
276
+ entry: zdm
277
+ language: system
278
+ types: [python]
279
+ files: (^|.*/)(migrations|alembic/versions)/.*\.py$
280
+ exclude: __init__\.py$
281
+ ```
282
+
283
+ Or use diff mode to only check changed migrations:
284
+
285
+ ```yaml
286
+ repos:
287
+ - repo: local
288
+ hooks:
289
+ - id: zdm-diff
290
+ name: zdm diff
291
+ entry: zdm --diff-staged origin/main
292
+ language: system
293
+ pass_filenames: false
294
+ always_run: true
295
+ ```
296
+
297
+ The `zdm-diff` hook uses `--diff-staged` so it checks the staged index that
298
+ pre-commit is validating, rather than the previous `HEAD` commit.
299
+
300
+ The repository also publishes source-based pre-commit hooks for users who prefer
301
+ `repo: https://github.com/Photoroom/zero-downtime-migrations` with
302
+ `rev: <latest release tag>`. Those hooks install the package from source, so
303
+ Rust must be available in the pre-commit environment.
304
+
305
+ ## GitHub Actions
306
+
307
+ ```yaml
308
+ - uses: actions/checkout@v4
309
+ with:
310
+ # --diff needs the base ref and enough history to compute a merge base.
311
+ fetch-depth: 0
312
+
313
+ - name: Install zdm
314
+ run: pip install zdm
315
+
316
+ - name: Lint migrations
317
+ run: zdm --diff origin/main
318
+ ```
319
+
320
+ ## Rust library API
321
+
322
+ The Rust crate exposes a small programmatic API, but it remains experimental
323
+ while the project is in the 0.x series. Prefer `Migration::from_path` or
324
+ `Migration::from_source`, `Config`, and the built-in rule registries. Low-level
325
+ parser, extractor, diagnostic-construction, discovery, and git helpers may
326
+ change between minor 0.x releases.
327
+
328
+ ## Comparison with Other Tools
329
+
330
+ | | zdm | django-migration-linter | Django's `makemigrations --check` |
331
+ |---|---|---|---|
332
+ | **Requires Django installed** | No | Yes | Yes |
333
+ | **Requires project setup** | No | Yes (settings.py) | Yes (full environment) |
334
+ | **Checks for missing migrations** | No | No | Yes |
335
+ | **Checks for unsafe operations** | Yes (16 active rules; `zdm --list-rules`) | Yes (~8 rules) | No |
336
+ | **Configurable via `pyproject.toml`** | Yes (walks up within trusted repos) | Yes | N/A |
337
+ | **Can run without database** | Yes | Yes | No |
338
+ | **Language** | Rust | Python | Python |
339
+
340
+ **When to use what:**
341
+ - Use `makemigrations --check` to ensure all model changes have migrations
342
+ - Use zdm or django-migration-linter to catch unsafe migration patterns
343
+ - 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)
344
+
345
+ ## License
346
+
347
+ MIT
348
+
@@ -0,0 +1,6 @@
1
+ zdm-0.6.0.data/scripts/zdm.exe,sha256=xw_k6hCaWZIc63_xa0YGA37FHqwI7vzFf1Qfe5jRzf0,3099648
2
+ zdm-0.6.0.dist-info/METADATA,sha256=xKJ2l_DrxCf6ruVphlyGOs-r0Un6tcCKkgmn8qKvUjo,16421
3
+ zdm-0.6.0.dist-info/WHEEL,sha256=2zDlIYIdD4m4N3p5DVEG3iJhGLdhsBQgdH-FqVkAur8,94
4
+ zdm-0.6.0.dist-info/licenses/LICENSE,sha256=Fv0B418S86_u2qQaLAvYi1Wjc9OC6hCIoWpmqRxYZkc,1087
5
+ zdm-0.6.0.dist-info/sboms/zero-downtime-migrations.cyclonedx.json,sha256=dQBuNjMABAjekAHzeSYoW36cYjIh1jUK81sOQhz2X4Q,79315
6
+ zdm-0.6.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: maturin (1.14.1)
3
+ Root-Is-Purelib: false
4
+ Tag: py3-none-win_amd64
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Photoroom
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.