Metadata-Version: 2.4
Name: pytest-mrt
Version: 0.2.0
Summary: Catch database migration rollback failures before they reach production
Project-URL: Homepage, https://github.com/croc100/pytest-mrt
Project-URL: Documentation, https://github.com/croc100/pytest-mrt#readme
Project-URL: Repository, https://github.com/croc100/pytest-mrt
Project-URL: Issues, https://github.com/croc100/pytest-mrt/issues
Project-URL: Changelog, https://github.com/croc100/pytest-mrt/releases
Author: croc100
License: MIT License
        
        Copyright (c) 2026 croc100
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: alembic,database,migrations,pytest,rollback,sqlalchemy,testing
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Database
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Requires-Dist: alembic>=1.9
Requires-Dist: pytest>=7.0
Requires-Dist: rich>=13.0
Requires-Dist: sqlalchemy>=2.0
Requires-Dist: typer>=0.9
Provides-Extra: asyncpg
Requires-Dist: asyncpg; extra == 'asyncpg'
Provides-Extra: dev
Requires-Dist: hatch; extra == 'dev'
Requires-Dist: psycopg2-binary; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Provides-Extra: postgres
Requires-Dist: psycopg2-binary; extra == 'postgres'
Description-Content-Type: text/markdown

# pytest-mrt

<p align="center">
  <a href="https://pypi.org/project/pytest-mrt"><img src="https://img.shields.io/pypi/v/pytest-mrt?color=blue" alt="PyPI"></a>
  <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&label=tests" alt="CI"></a>
  <a href="https://pypi.org/project/pytest-mrt"><img src="https://img.shields.io/pypi/pyversions/pytest-mrt" alt="Python"></a>
  <img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License">
</p>

---

`alembic downgrade -1` ran clean. No errors. Your monitoring went green.

But the users' phone numbers are gone. The column came back. The data didn't.

This is what pytest-mrt exists to prevent.

---

## What it does

Most tools verify that migrations *run* without errors. pytest-mrt verifies that your data *survives* a rollback — by seeding real rows before each migration, rolling back, and checking nothing was lost.

It also statically scans your migration files for 24 known dangerous patterns before you touch a database at all.

```bash
pip install pytest-mrt
```

---

## Quickstart

```python
# conftest.py
from pytest_mrt import MRTConfig

def pytest_configure(config):
    config._mrt_config = MRTConfig(
        alembic_ini="alembic.ini",
        db_url=os.environ["TEST_DATABASE_URL"],
    )
```

```python
# test_migrations.py
def test_migrations_are_safe(mrt):
    mrt.assert_all_reversible()
```

```
$ pytest test_migrations.py -s

  ──────────── MRT — Migration Rollback Test ────────────

  ✓  001  reversible
  ✓  002  reversible
  ✓  003  reversible
  ✗  004  data loss detected
     └─ Table 'users': 3/3 rows lost after rollback
  ✗  005  data loss detected
     └─ Table 'users' still exists after rollback — downgrade is incomplete

  ╭─────────────────────────────────────────────────────╮
  │  2 migration(s) will cause data loss on rollback.   │
  ╰─────────────────────────────────────────────────────╯
```

You can also check a single revision — useful in CI to only gate new migrations in a PR:

```python
def test_this_pr(mrt):
    result = mrt.check_revision("abc123")
    assert result.passed, result.failure_summary()
```

---

## Static analysis

No database needed. Scan your migration files directly:

```bash
mrt check migrations/versions/
```

```
╭──────────┬──────────────────────────────┬─────────┬──────────────────────────────────────────────────╮
│ Revision │ Pattern                      │ Sev     │ Message                                          │
├──────────┼──────────────────────────────┼─────────┼──────────────────────────────────────────────────┤
│ 004      │ DROP COLUMN in upgrade       │ error   │ Column dropped — data permanently lost on rollback│
│ 005      │ No-op downgrade              │ error   │ downgrade() does nothing — migration irreversible │
│ 006      │ ENUM value added             │ error   │ Cannot roll back if rows use the new value        │
│ 007      │ INDEX without CONCURRENTLY   │ warning │ Locks table during index build                    │
╰──────────┴──────────────────────────────┴─────────┴──────────────────────────────────────────────────╯
3 error(s), 1 warning(s)
```

Add `--strict` to make warnings fail the build too.

---

## What gets caught

**Errors** — these will cause data loss or a broken rollback:

| Pattern | Why |
|---|---|
| `op.drop_column()` in upgrade | Column data is gone even after rollback re-adds the column |
| `op.drop_table()` in upgrade | Every row in the table is permanently lost |
| `TRUNCATE` in migration | Destroys data with no undo |
| `def downgrade(): pass` | Rollback silently does nothing |
| No `downgrade()` function | Migration is completely irreversible |
| `rename_table` without reverse | Table stays under new name after rollback |
| `rename_column` without reverse | App code using old column name breaks |
| `DROP VIEW` without recreating | Application queries fail after rollback |
| `ALTER TYPE ... ADD VALUE` | Can't remove enum values once rows use them |
| Add + migrate data + drop original | The combined operation cannot be undone |

**Warnings** — worth reviewing before deploying:

| Pattern | Why |
|---|---|
| `NOT NULL` without `server_default` | Fails on non-empty tables |
| Column type change | Conversion may be lossy |
| Raw `op.execute()` | Content can't be verified automatically |
| Bulk `UPDATE` without reverse `UPDATE` | One-way data transformation |
| `ON DELETE CASCADE` added | Child rows silently deleted with parent |
| `CREATE INDEX` without `CONCURRENTLY` | Locks table during build (PostgreSQL) |
| `ADD COLUMN` with `DEFAULT` | Full table rewrite on PostgreSQL < 11 |
| `CREATE UNIQUE CONSTRAINT` | Fails if duplicates already exist |
| `DROP INDEX` without recreating | Query performance and uniqueness not restored |
| `DROP CONSTRAINT` without recreating | Data integrity guarantees removed |
| `ALTER SEQUENCE` / `setval` | Sequences don't roll back — gaps appear |
| `NOT NULL` via raw SQL without reverse | Column stays NOT NULL after rollback |
| `NOT NULL` without restoring `nullable` | Downgrade leaves column in wrong state |

---

## How the dynamic check works

For each revision, pytest-mrt:

1. Takes a snapshot of the current schema
2. Seeds real rows into every table (type-aware: generates valid integers, strings, timestamps, UUIDs, etc.)
3. Runs `alembic upgrade` to the revision
4. Runs `alembic downgrade -1`
5. Checks the schema is exactly restored — no missing tables, no leftover tables
6. Checks every seeded row is still there

This catches things static analysis can't: a migration where the schema comes back but the data doesn't, or a `downgrade()` that creates the table empty instead of restoring it.

---

## Databases

| | Static analysis | Dynamic verification |
|---|---|---|
| PostgreSQL | ✅ | ✅ |
| SQLite | ✅ | ✅ |
| MySQL / MariaDB | ✅ | 🔜 planned |

---

## CI

```yaml
- name: Check migrations
  run: |
    mrt check migrations/versions/
    pytest tests/test_migrations.py -v -s
```

For publishing via GitHub Actions with OIDC (no tokens needed), see [the publish workflow](.github/workflows/publish.yml).

---

## Examples

[`examples/blog/`](examples/blog/) has a complete Alembic project with intentionally safe and unsafe migrations. Run it to see what pytest-mrt catches:

```bash
cd examples/blog
pytest test_migrations.py -v -s
```

---

## Contributing

New risk patterns are the most valuable contribution. If you've been burned by a migration pattern that pytest-mrt doesn't catch, open an issue or PR. See [CONTRIBUTING.md](CONTRIBUTING.md).

---

## License

MIT
