Skip to main content

Catch database migration rollback failures before they reach production

Project description

pytest-mrt

PyPI CI Python MIT License


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.

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=os.environ["TEST_DATABASE_URL"],
    )
# 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:

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:

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

- 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.


Examples

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

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.


License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pytest_mrt-0.2.1.tar.gz (21.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pytest_mrt-0.2.1-py3-none-any.whl (17.8 kB view details)

Uploaded Python 3

File details

Details for the file pytest_mrt-0.2.1.tar.gz.

File metadata

  • Download URL: pytest_mrt-0.2.1.tar.gz
  • Upload date:
  • Size: 21.4 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

Hashes for pytest_mrt-0.2.1.tar.gz
Algorithm Hash digest
SHA256 4e6bb68fb85de7dad3fa4e8e97e229f06efae267689abd24c771247f745a64a1
MD5 f1c19b75f3975304d17458fd1c62f0e1
BLAKE2b-256 79a97e5d2eb25db5e4dccf3a13d05888b18c290a565ebcefb7219a74e6cda80f

See more details on using hashes here.

File details

Details for the file pytest_mrt-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: pytest_mrt-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 17.8 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

Hashes for pytest_mrt-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9ab96d9081f568799ee4dcad07a572790a2f6849bbd138737356bd896df031bc
MD5 59df13ebe6cd016beb4e310264004812
BLAKE2b-256 b4a2d262c6f937fd67d8a8faa8d7f9bfe0d7f633b258d8463668215affe574f1

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page