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.0.tar.gz (21.5 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.0-py3-none-any.whl (18.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pytest_mrt-0.2.0.tar.gz
  • Upload date:
  • Size: 21.5 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.0.tar.gz
Algorithm Hash digest
SHA256 9dc553e98fcc35d534306e8fa7ce13cc44c5304d118490c80c667988742f55a5
MD5 aa35f974e089230301f71de9cb1c8372
BLAKE2b-256 181738abd35f2ecfe3ac015263abe38f53c698b09b2a15f9bab9618b12af8281

See more details on using hashes here.

File details

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

File metadata

  • Download URL: pytest_mrt-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 18.4 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.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4e27b4934249d4b0c584c5bf1b9a1e16b80db0671692dcf40fc0ccd3c336bf9c
MD5 b9ba921eb8e7346b639dbd23142bfea5
BLAKE2b-256 3381700c477e1b39aa801b69059458e652cf332f510668462e8d48923ee6443e

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