Catch database migration rollback failures before they reach production
Project description
pytest-mrt
Migration Rollback Tester
Catch database migration disasters before they reach production.
The problem
It's 2am. Your new feature is deployed. Something is wrong. You run alembic downgrade -1.
The command succeeds. But the data is gone.
The column came back. The rows didn't.
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.
pytest-mrt tests the full cycle: seed real data → upgrade → downgrade → verify nothing was lost.
Install
pip install pytest-mrt
Quickstart
# conftest.py
from pytest_mrt import MRTConfig
def pytest_configure(config):
config._mrt_config = MRTConfig(
alembic_ini="alembic.ini",
db_url="postgresql://localhost/myapp_test",
)
# test_migrations.py
def test_all_migrations_are_reversible(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. │
│ 004 │
│ └─ Table 'users': 3/3 rows lost after rollback │
│ 005 │
│ └─ Table 'users' still exists after rollback │
╰─────────────────────────────────────────────────────╯
What it catches
Static analysis — before you even run
| Pattern | Severity | Why it's dangerous |
|---|---|---|
op.drop_column() in upgrade |
🔴 error | Column data is permanently gone |
op.drop_table() in upgrade |
🔴 error | All table data is permanently gone |
TRUNCATE in migration |
🔴 error | Destroys data with no undo |
def downgrade(): pass |
🔴 error | Rollback silently does nothing |
No downgrade() function |
🔴 error | Migration is completely irreversible |
RunPython without reverse_func |
🔴 error | Data transformation cannot be undone |
NOT NULL without server_default |
🟡 warning | Will fail on non-empty tables |
ALTER COLUMN type_=... |
🟡 warning | Type conversion may lose data |
op.execute() with raw SQL |
🟡 warning | Cannot verify reversibility |
Bulk UPDATE without reverse |
🟡 warning | One-way data transformation |
ON DELETE CASCADE added |
🟡 warning | Child rows silently deleted |
CREATE INDEX without CONCURRENTLY |
🟡 warning | Locks table during index build |
ADD COLUMN with DEFAULT |
🟡 warning | Full table rewrite on PostgreSQL < 11 |
CREATE UNIQUE CONSTRAINT |
🟡 warning | Will fail if duplicates exist |
NOT NULL without restoring nullable |
🟡 warning | Downgrade leaves column in wrong state |
Run static analysis without a database:
mrt check migrations/versions/
╭──────────────────────────────────────────────────────────────────────────────╮
│ Rollback Risk Analysis │
├──────────┬──────────────────────┬─────────────┬─────────────────────────── │
│ Revision │ Pattern │ Sev │ Message │
├──────────┼──────────────────────┼─────────────┼─────────────────────────── │
│ 004 │ DROP COLUMN │ error │ Data loss on rollback │
│ 005 │ No-op downgrade │ error │ downgrade() does nothing │
│ 006 │ INDEX without CONC. │ warning │ Locks table during build │
╰──────────────────────────────────────────────────────────────────────────────╯
2 error(s), 1 warning(s)
Dynamic verification — with real data
pytest-mrt seeds actual rows before each migration, then checks they survive the downgrade:
def test_specific_revision(mrt):
result = mrt.check_revision("abc123")
assert result.passed, result.failure_summary()
Or test everything at once:
def test_all_migrations(mrt):
mrt.assert_all_reversible()
How it works
For each migration revision, pytest-mrt:
1. Capture schema at current state
2. Seed real data into all existing tables
3. Run upgrade to this revision
4. Run downgrade (one step back)
5. Verify schema is exactly restored
6. Verify every seeded row survived
This catches failures that syntax checks miss:
- Schema comes back, but seeded rows are gone → data loss
- Downgrade is a no-op, table still exists → rollback did nothing
- Column returns but with wrong type → schema drift
Supported databases
| Database | Status |
|---|---|
| PostgreSQL | ✅ Full support |
| SQLite | ✅ Full support (great for CI) |
| MySQL / MariaDB | 🔜 Planned |
CI integration
Add to your GitHub Actions workflow:
- name: Test migration rollbacks
run: pytest tests/test_migrations.py -v -s
Or use the static check as a fast pre-flight:
- name: Static migration analysis
run: mrt check migrations/versions/ --strict
--strict makes warnings fail the build, not just errors.
Configuration
# conftest.py
from pytest_mrt import MRTConfig
def pytest_configure(config):
config._mrt_config = MRTConfig(
alembic_ini="alembic.ini", # path to alembic.ini
db_url="postgresql://...", # test database URL
seed_rows=5, # rows to seed per table (default: 3)
)
Use environment variables for CI:
import os
from pytest_mrt import MRTConfig
def pytest_configure(config):
config._mrt_config = MRTConfig(
alembic_ini="alembic.ini",
db_url=os.environ["TEST_DATABASE_URL"],
)
Examples
See examples/blog/ for a complete working example with:
- Safe migrations (add nullable column, create table)
- Dangerous migrations (drop column with data, no-op downgrade)
- How pytest-mrt catches each failure
cd examples/blog
pip install pytest-mrt
pytest test_migrations.py -v -s
FAQ
Does it modify my production database?
No. pytest-mrt only runs against the database URL you provide in MRTConfig. Always use a test database.
Does it work with Django migrations? Django support is on the roadmap. Currently only Alembic is supported.
How is this different from pytest-alembic?
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.
My migration intentionally drops a column. Will this always fail? 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.
Roadmap
- Alembic support
- Static risk analysis CLI (
mrt check) - Dynamic data integrity verification
- GitHub Actions CI
- Django Migrations support
- MySQL / MariaDB support
- HTML report output
- Per-revision exclusions (
@mrt.skip("004", reason="...")) - PyPI release
Contributing
Contributions are welcome. See CONTRIBUTING.md.
License
Apache 2.0
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 Distribution
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 pytest_mrt-0.1.0.tar.gz.
File metadata
- Download URL: pytest_mrt-0.1.0.tar.gz
- Upload date:
- Size: 18.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.17.0 {"ci":null,"cpu":"arm64","distro":{"name":"macOS","version":"26.4.1"},"implementation":{"name":"CPython","version":"3.14.3"},"installer":{"name":"hatch","version":"1.17.0"},"openssl_version":"OpenSSL 3.6.2 7 Apr 2026","python":"3.14.3","system":{"name":"Darwin","release":"25.4.0"}} HTTPX2/2.3.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8135bf5fb6e10d9de4d62f9c021e35bd27366b069ed080a359a7d0f227805b56
|
|
| MD5 |
94a01e39f6069d61af3692448b932750
|
|
| BLAKE2b-256 |
baef8d3d20f216d1ab482874f18a2a064cdea2dab177be31eb2c0e3c4231cf1a
|
File details
Details for the file pytest_mrt-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pytest_mrt-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: Hatch/1.17.0 {"ci":null,"cpu":"arm64","distro":{"name":"macOS","version":"26.4.1"},"implementation":{"name":"CPython","version":"3.14.3"},"installer":{"name":"hatch","version":"1.17.0"},"openssl_version":"OpenSSL 3.6.2 7 Apr 2026","python":"3.14.3","system":{"name":"Darwin","release":"25.4.0"}} HTTPX2/2.3.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2321b497190133a70ca5497b55ce59804b9d176e14a0a855ffbb56fd096543b4
|
|
| MD5 |
d082e7b62460acafb00451d23c1b8140
|
|
| BLAKE2b-256 |
e34c9465ee28fe21218328ca5ede21e1542cb2ef75b80863d139cfc6f784d1de
|