pytest-mrt 0.1.0__tar.gz

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.
Files changed (31) hide show
  1. pytest_mrt-0.1.0/.github/workflows/ci.yml +59 -0
  2. pytest_mrt-0.1.0/.gitignore +18 -0
  3. pytest_mrt-0.1.0/CONTRIBUTING.md +65 -0
  4. pytest_mrt-0.1.0/LICENSE +21 -0
  5. pytest_mrt-0.1.0/PKG-INFO +335 -0
  6. pytest_mrt-0.1.0/README.md +276 -0
  7. pytest_mrt-0.1.0/examples/blog/alembic/env.py +30 -0
  8. pytest_mrt-0.1.0/examples/blog/alembic/versions/001_create_users.py +27 -0
  9. pytest_mrt-0.1.0/examples/blog/alembic/versions/002_create_posts.py +27 -0
  10. pytest_mrt-0.1.0/examples/blog/alembic/versions/003_add_bio.py +21 -0
  11. pytest_mrt-0.1.0/examples/blog/alembic/versions/004_drop_phone.py +26 -0
  12. pytest_mrt-0.1.0/examples/blog/alembic/versions/005_irreversible_data_migration.py +25 -0
  13. pytest_mrt-0.1.0/examples/blog/alembic.ini +37 -0
  14. pytest_mrt-0.1.0/examples/blog/conftest.py +9 -0
  15. pytest_mrt-0.1.0/examples/blog/test_migrations.py +31 -0
  16. pytest_mrt-0.1.0/pyproject.toml +63 -0
  17. pytest_mrt-0.1.0/pytest_mrt/__init__.py +4 -0
  18. pytest_mrt-0.1.0/pytest_mrt/adapters/__init__.py +0 -0
  19. pytest_mrt-0.1.0/pytest_mrt/cli.py +63 -0
  20. pytest_mrt-0.1.0/pytest_mrt/config.py +8 -0
  21. pytest_mrt-0.1.0/pytest_mrt/core/__init__.py +0 -0
  22. pytest_mrt-0.1.0/pytest_mrt/core/detector.py +259 -0
  23. pytest_mrt-0.1.0/pytest_mrt/core/runner.py +39 -0
  24. pytest_mrt-0.1.0/pytest_mrt/core/schema.py +120 -0
  25. pytest_mrt-0.1.0/pytest_mrt/core/seeder.py +152 -0
  26. pytest_mrt-0.1.0/pytest_mrt/core/verifier.py +72 -0
  27. pytest_mrt-0.1.0/pytest_mrt/plugin.py +77 -0
  28. pytest_mrt-0.1.0/pytest_mrt/reporter.py +55 -0
  29. pytest_mrt-0.1.0/tests/__init__.py +0 -0
  30. pytest_mrt-0.1.0/tests/test_detector.py +218 -0
  31. pytest_mrt-0.1.0/tests/test_integration.py +303 -0
@@ -0,0 +1,59 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+
23
+ - name: Install
24
+ run: pip install -e ".[dev]"
25
+
26
+ - name: Run tests
27
+ run: pytest tests/ -v
28
+
29
+ test-postgres:
30
+ runs-on: ubuntu-latest
31
+ services:
32
+ postgres:
33
+ image: postgres:15
34
+ env:
35
+ POSTGRES_USER: mrt
36
+ POSTGRES_PASSWORD: mrt
37
+ POSTGRES_DB: mrt_test
38
+ options: >-
39
+ --health-cmd pg_isready
40
+ --health-interval 10s
41
+ --health-timeout 5s
42
+ --health-retries 5
43
+ ports:
44
+ - 5432:5432
45
+
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+
49
+ - uses: actions/setup-python@v5
50
+ with:
51
+ python-version: "3.11"
52
+
53
+ - name: Install
54
+ run: pip install -e ".[dev]" psycopg2-binary
55
+
56
+ - name: Run postgres integration tests
57
+ env:
58
+ MRT_TEST_DB_URL: postgresql://mrt:mrt@localhost:5432/mrt_test
59
+ run: pytest tests/ -v -m postgres --ignore=tests/test_integration.py
@@ -0,0 +1,18 @@
1
+ # dependencies & build
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ *.egg-info/
6
+ dist/
7
+ .pytest_cache/
8
+
9
+ # PyPI credentials — never commit
10
+ .pypirc
11
+ *.token
12
+
13
+ # example DB files
14
+ examples/**/*.db
15
+
16
+ # secrets
17
+ .env
18
+ .env.*
@@ -0,0 +1,65 @@
1
+ # Contributing to pytest-mrt
2
+
3
+ Thank you for considering a contribution. Here's how to get started.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/croc100/pytest-mrt
9
+ cd pytest-mrt
10
+ python3 -m venv .venv
11
+ source .venv/bin/activate
12
+ pip install -e ".[dev]"
13
+ ```
14
+
15
+ ## Running tests
16
+
17
+ ```bash
18
+ # All tests (SQLite, no external dependencies)
19
+ pytest tests/ -v
20
+
21
+ # With PostgreSQL
22
+ MRT_TEST_DB_URL=postgresql://localhost/mrt_test pytest tests/ -v -m postgres
23
+ ```
24
+
25
+ ## What to work on
26
+
27
+ Good first issues:
28
+ - Add a new static analysis pattern to `pytest_mrt/core/detector.py`
29
+ - Add a new integration test case to `tests/test_integration.py`
30
+ - Improve error messages in `pytest_mrt/reporter.py`
31
+
32
+ Higher effort:
33
+ - Django Migrations adapter (`pytest_mrt/adapters/django.py`)
34
+ - MySQL support
35
+ - HTML report output
36
+
37
+ ## Adding a new risk pattern
38
+
39
+ 1. Write a `_check_*` function in `pytest_mrt/core/detector.py`
40
+ 2. Add it to the `_CHECKS` list at the bottom
41
+ 3. Write a test in `tests/test_detector.py` (both a positive and a negative case)
42
+
43
+ ```python
44
+ def _check_my_new_pattern(source: str, rev: str, fname: str) -> list[RiskWarning]:
45
+ body = _upgrade_body(source)
46
+ if re.search(r"some_pattern", body):
47
+ return [RiskWarning(rev, fname, "Pattern name",
48
+ "Human-readable explanation of the risk", "error")]
49
+ return []
50
+ ```
51
+
52
+ ## Commit style
53
+
54
+ ```
55
+ feat: add MySQL support
56
+ fix: handle empty downgrade body correctly
57
+ docs: add Django example
58
+ test: cover NOT NULL without default on existing table
59
+ ```
60
+
61
+ ## Pull request checklist
62
+
63
+ - [ ] Tests added for new behavior
64
+ - [ ] All existing tests pass (`pytest tests/ -v`)
65
+ - [ ] `mrt check examples/blog/alembic/versions/` still works
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 croc100
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.
@@ -0,0 +1,335 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-mrt
3
+ Version: 0.1.0
4
+ Summary: Catch database migration rollback failures before they reach production
5
+ Project-URL: Homepage, https://github.com/croc100/pytest-mrt
6
+ Project-URL: Repository, https://github.com/croc100/pytest-mrt
7
+ Project-URL: Issues, https://github.com/croc100/pytest-mrt/issues
8
+ Project-URL: Changelog, https://github.com/croc100/pytest-mrt/releases
9
+ Author: croc100
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 croc100
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: alembic,database,migrations,pytest,rollback,sqlalchemy,testing
33
+ Classifier: Development Status :: 4 - Beta
34
+ Classifier: Framework :: Pytest
35
+ Classifier: Intended Audience :: Developers
36
+ Classifier: License :: OSI Approved :: Apache Software License
37
+ Classifier: Operating System :: OS Independent
38
+ Classifier: Programming Language :: Python :: 3
39
+ Classifier: Programming Language :: Python :: 3.10
40
+ Classifier: Programming Language :: Python :: 3.11
41
+ Classifier: Programming Language :: Python :: 3.12
42
+ Classifier: Topic :: Database
43
+ Classifier: Topic :: Software Development :: Testing
44
+ Requires-Python: >=3.10
45
+ Requires-Dist: alembic>=1.9
46
+ Requires-Dist: pytest>=7.0
47
+ Requires-Dist: rich>=13.0
48
+ Requires-Dist: sqlalchemy>=2.0
49
+ Requires-Dist: typer>=0.9
50
+ Provides-Extra: asyncpg
51
+ Requires-Dist: asyncpg; extra == 'asyncpg'
52
+ Provides-Extra: dev
53
+ Requires-Dist: hatch; extra == 'dev'
54
+ Requires-Dist: psycopg2-binary; extra == 'dev'
55
+ Requires-Dist: pytest; extra == 'dev'
56
+ Provides-Extra: postgres
57
+ Requires-Dist: psycopg2-binary; extra == 'postgres'
58
+ Description-Content-Type: text/markdown
59
+
60
+ # pytest-mrt
61
+
62
+ <p align="center">
63
+ <strong>Migration Rollback Tester</strong><br>
64
+ Catch database migration disasters before they reach production.
65
+ </p>
66
+
67
+ <p align="center">
68
+ <a href="https://pypi.org/project/pytest-mrt"><img src="https://img.shields.io/pypi/v/pytest-mrt?color=blue" alt="PyPI"></a>
69
+ <a href="https://github.com/croc100/pytest-mrt/actions"><img src="https://img.shields.io/github/actions/workflow/status/croc100/pytest-mrt/ci.yml?branch=main" alt="CI"></a>
70
+ <a href="https://pypi.org/project/pytest-mrt"><img src="https://img.shields.io/pypi/pyversions/pytest-mrt" alt="Python"></a>
71
+ <a href="https://github.com/croc100/pytest-mrt/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-green" alt="License"></a>
72
+ </p>
73
+
74
+ ---
75
+
76
+ ## The problem
77
+
78
+ It's 2am. Your new feature is deployed. Something is wrong. You run `alembic downgrade -1`.
79
+
80
+ The command succeeds. But the data is gone.
81
+
82
+ The column came back. The rows didn't.
83
+
84
+ ---
85
+
86
+ This happens because **most tools only check if your migration runs without errors** — not whether your data survives the round-trip. `alembic downgrade` can succeed while silently destroying everything it was supposed to restore.
87
+
88
+ **pytest-mrt** tests the full cycle: seed real data → upgrade → downgrade → verify nothing was lost.
89
+
90
+ ---
91
+
92
+ ## Install
93
+
94
+ ```bash
95
+ pip install pytest-mrt
96
+ ```
97
+
98
+ ---
99
+
100
+ ## Quickstart
101
+
102
+ ```python
103
+ # conftest.py
104
+ from pytest_mrt import MRTConfig
105
+
106
+ def pytest_configure(config):
107
+ config._mrt_config = MRTConfig(
108
+ alembic_ini="alembic.ini",
109
+ db_url="postgresql://localhost/myapp_test",
110
+ )
111
+ ```
112
+
113
+ ```python
114
+ # test_migrations.py
115
+ def test_all_migrations_are_reversible(mrt):
116
+ mrt.assert_all_reversible()
117
+ ```
118
+
119
+ ```
120
+ $ pytest test_migrations.py -s
121
+
122
+ ──────────── MRT — Migration Rollback Test ────────────
123
+
124
+ ✓ 001 reversible
125
+ ✓ 002 reversible
126
+ ✓ 003 reversible
127
+ ✗ 004 data loss detected
128
+ └─ Table 'users': 3/3 rows lost after rollback
129
+ ✗ 005 data loss detected
130
+ └─ Table 'users' still exists after rollback — downgrade is incomplete
131
+
132
+ ╭─────────────────────────────────────────────────────╮
133
+ │ 2 migration(s) will cause data loss on rollback. │
134
+ │ 004 │
135
+ │ └─ Table 'users': 3/3 rows lost after rollback │
136
+ │ 005 │
137
+ │ └─ Table 'users' still exists after rollback │
138
+ ╰─────────────────────────────────────────────────────╯
139
+ ```
140
+
141
+ ---
142
+
143
+ ## What it catches
144
+
145
+ ### Static analysis — before you even run
146
+
147
+ | Pattern | Severity | Why it's dangerous |
148
+ |---|---|---|
149
+ | `op.drop_column()` in upgrade | 🔴 error | Column data is permanently gone |
150
+ | `op.drop_table()` in upgrade | 🔴 error | All table data is permanently gone |
151
+ | `TRUNCATE` in migration | 🔴 error | Destroys data with no undo |
152
+ | `def downgrade(): pass` | 🔴 error | Rollback silently does nothing |
153
+ | No `downgrade()` function | 🔴 error | Migration is completely irreversible |
154
+ | `RunPython` without `reverse_func` | 🔴 error | Data transformation cannot be undone |
155
+ | `NOT NULL` without `server_default` | 🟡 warning | Will fail on non-empty tables |
156
+ | `ALTER COLUMN type_=...` | 🟡 warning | Type conversion may lose data |
157
+ | `op.execute()` with raw SQL | 🟡 warning | Cannot verify reversibility |
158
+ | Bulk `UPDATE` without reverse | 🟡 warning | One-way data transformation |
159
+ | `ON DELETE CASCADE` added | 🟡 warning | Child rows silently deleted |
160
+ | `CREATE INDEX` without `CONCURRENTLY` | 🟡 warning | Locks table during index build |
161
+ | `ADD COLUMN` with `DEFAULT` | 🟡 warning | Full table rewrite on PostgreSQL < 11 |
162
+ | `CREATE UNIQUE CONSTRAINT` | 🟡 warning | Will fail if duplicates exist |
163
+ | `NOT NULL` without restoring `nullable` | 🟡 warning | Downgrade leaves column in wrong state |
164
+
165
+ Run static analysis without a database:
166
+
167
+ ```bash
168
+ mrt check migrations/versions/
169
+ ```
170
+
171
+ ```
172
+ ╭──────────────────────────────────────────────────────────────────────────────╮
173
+ │ Rollback Risk Analysis │
174
+ ├──────────┬──────────────────────┬─────────────┬─────────────────────────── │
175
+ │ Revision │ Pattern │ Sev │ Message │
176
+ ├──────────┼──────────────────────┼─────────────┼─────────────────────────── │
177
+ │ 004 │ DROP COLUMN │ error │ Data loss on rollback │
178
+ │ 005 │ No-op downgrade │ error │ downgrade() does nothing │
179
+ │ 006 │ INDEX without CONC. │ warning │ Locks table during build │
180
+ ╰──────────────────────────────────────────────────────────────────────────────╯
181
+ 2 error(s), 1 warning(s)
182
+ ```
183
+
184
+ ### Dynamic verification — with real data
185
+
186
+ pytest-mrt seeds actual rows before each migration, then checks they survive the downgrade:
187
+
188
+ ```python
189
+ def test_specific_revision(mrt):
190
+ result = mrt.check_revision("abc123")
191
+ assert result.passed, result.failure_summary()
192
+ ```
193
+
194
+ Or test everything at once:
195
+
196
+ ```python
197
+ def test_all_migrations(mrt):
198
+ mrt.assert_all_reversible()
199
+ ```
200
+
201
+ ---
202
+
203
+ ## How it works
204
+
205
+ For each migration revision, pytest-mrt:
206
+
207
+ ```
208
+ 1. Capture schema at current state
209
+ 2. Seed real data into all existing tables
210
+ 3. Run upgrade to this revision
211
+ 4. Run downgrade (one step back)
212
+ 5. Verify schema is exactly restored
213
+ 6. Verify every seeded row survived
214
+ ```
215
+
216
+ This catches failures that syntax checks miss:
217
+ - Schema comes back, but seeded rows are gone → **data loss**
218
+ - Downgrade is a no-op, table still exists → **rollback did nothing**
219
+ - Column returns but with wrong type → **schema drift**
220
+
221
+ ---
222
+
223
+ ## Supported databases
224
+
225
+ | Database | Status |
226
+ |---|---|
227
+ | PostgreSQL | ✅ Full support |
228
+ | SQLite | ✅ Full support (great for CI) |
229
+ | MySQL / MariaDB | 🔜 Planned |
230
+
231
+ ---
232
+
233
+ ## CI integration
234
+
235
+ Add to your GitHub Actions workflow:
236
+
237
+ ```yaml
238
+ - name: Test migration rollbacks
239
+ run: pytest tests/test_migrations.py -v -s
240
+ ```
241
+
242
+ Or use the static check as a fast pre-flight:
243
+
244
+ ```yaml
245
+ - name: Static migration analysis
246
+ run: mrt check migrations/versions/ --strict
247
+ ```
248
+
249
+ `--strict` makes warnings fail the build, not just errors.
250
+
251
+ ---
252
+
253
+ ## Configuration
254
+
255
+ ```python
256
+ # conftest.py
257
+ from pytest_mrt import MRTConfig
258
+
259
+ def pytest_configure(config):
260
+ config._mrt_config = MRTConfig(
261
+ alembic_ini="alembic.ini", # path to alembic.ini
262
+ db_url="postgresql://...", # test database URL
263
+ seed_rows=5, # rows to seed per table (default: 3)
264
+ )
265
+ ```
266
+
267
+ Use environment variables for CI:
268
+
269
+ ```python
270
+ import os
271
+ from pytest_mrt import MRTConfig
272
+
273
+ def pytest_configure(config):
274
+ config._mrt_config = MRTConfig(
275
+ alembic_ini="alembic.ini",
276
+ db_url=os.environ["TEST_DATABASE_URL"],
277
+ )
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Examples
283
+
284
+ See [`examples/blog/`](examples/blog/) for a complete working example with:
285
+ - Safe migrations (add nullable column, create table)
286
+ - Dangerous migrations (drop column with data, no-op downgrade)
287
+ - How pytest-mrt catches each failure
288
+
289
+ ```bash
290
+ cd examples/blog
291
+ pip install pytest-mrt
292
+ pytest test_migrations.py -v -s
293
+ ```
294
+
295
+ ---
296
+
297
+ ## FAQ
298
+
299
+ **Does it modify my production database?**
300
+ No. pytest-mrt only runs against the database URL you provide in `MRTConfig`. Always use a test database.
301
+
302
+ **Does it work with Django migrations?**
303
+ Django support is on the roadmap. Currently only Alembic is supported.
304
+
305
+ **How is this different from pytest-alembic?**
306
+ `pytest-alembic` checks that migrations run without errors and that your schema matches your models. It does **not** verify that data survives a rollback. pytest-mrt focuses specifically on that gap.
307
+
308
+ **My migration intentionally drops a column. Will this always fail?**
309
+ Yes — dropping a column destroys data. That's exactly what pytest-mrt warns you about. If you want to proceed, you can exclude specific revisions or mark the test as expected-to-fail.
310
+
311
+ ---
312
+
313
+ ## Roadmap
314
+
315
+ - [x] Alembic support
316
+ - [x] Static risk analysis CLI (`mrt check`)
317
+ - [x] Dynamic data integrity verification
318
+ - [x] GitHub Actions CI
319
+ - [ ] Django Migrations support
320
+ - [ ] MySQL / MariaDB support
321
+ - [ ] HTML report output
322
+ - [ ] Per-revision exclusions (`@mrt.skip("004", reason="...")`)
323
+ - [ ] PyPI release
324
+
325
+ ---
326
+
327
+ ## Contributing
328
+
329
+ Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md).
330
+
331
+ ---
332
+
333
+ ## License
334
+
335
+ Apache 2.0