Skip to main content

pytest-mrt

PyPI Downloads Used by CI Coverage Production/Stable Python 3.10-3.14 MIT License Contributors

A pytest plugin that catches database migration rollback failures before they reach production.

mrt check catching DROP COLUMN data loss


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.

pytest-mrt would have caught this before it reached production:

$ mrt check migrations/versions/

                         Rollback Risk Analysis
╭──────────┬────────┬───────────────────────────┬───────┬──────┬─────────────────────────────────────╮
│ Revision │ Code   │ Pattern                   │ Sev   │ Line │ Message                             │
├──────────┼────────┼───────────────────────────┼───────┼──────┼─────────────────────────────────────┤
│ 042      │ MRT201 │ DROP COLUMN in upgrade    │ error │   18 │ op.drop_column('users', 'phone') —  │
│          │        │                           │       │      │ column data is permanently lost     │
│          │        │                           │       │      │ even if downgrade re-adds the column│
╰──────────┴────────┴───────────────────────────┴───────┴──────┴─────────────────────────────────────╯
1 error(s), 0 warning(s)

Non-invasive — installs in 2 minutes, zero changes to your existing tests.


What it does

Most tools verify that migrations run without errors.
pytest-mrt verifies that your data survives a rollback.

It seeds real rows before each migration, rolls back, and checks nothing was lost. It also statically scans migration files for 44 known dangerous patterns across both Alembic and Django migrations.

Install

pip install pytest-mrt

Setup (2 minutes)

Add this to conftest.py:

# conftest.py
import os
from pytest_mrt import MRTConfig


def pytest_configure(config):
    config._mrt_config = MRTConfig(
        alembic_ini="alembic.ini",
        db_url=os.environ.get("TEST_DATABASE_URL", "sqlite:///test.db"),
    )

That's it. Run pytest and 6 safety tests appear automatically — no test files needed:

PASSED test_mrt_single_head          - Migration history has exactly one head
PASSED test_mrt_upgrade              - alembic upgrade head completes without error
PASSED test_mrt_downgrade_base       - alembic downgrade base then re-upgrade completes cleanly
PASSED test_mrt_up_down_consistency  - Every migration is safely reversible
PASSED test_mrt_static_no_errors     - Zero static analysis errors in all migration files
PASSED test_mrt_schema_matches_models- Database schema matches ORM models after upgrade

Want to write custom rollback tests? Use the mrt fixture — just add it as a parameter to any test function, no import needed:

def test_migration_003(mrt):
    mrt.assert_reversible("abc1234")

Static analysis (no database needed)

mrt check migrations/versions/
╭──────────┬──────────────────────────┬─────────┬──────┬─────────┬────────────────────────────────────╮
│ Revision │ Pattern                  │ Sev     │ Line │ Code    │ Message                            │
├──────────┼──────────────────────────┼─────────┼──────┼─────────┼────────────────────────────────────┤
│ 004      │ DROP COLUMN in upgrade   │ error   │   12 │ MRT103  │ Data permanently lost on rollback  │
│ 005      │ No-op downgrade          │ error   │    8 │ MRT102  │ downgrade() does nothing           │
│ 006      │ INDEX without CONCURR.   │ warning │   19 │ MRT207  │ Locks table during index build     │
╰──────────┴──────────────────────────┴─────────┴──────┴─────────┴────────────────────────────────────╯
2 error(s), 1 warning(s)

What gets caught

Errors (will cause data loss or a broken rollback):

  • op.drop_column() in upgrade — data is gone even if downgrade re-adds the column
  • op.drop_table() in upgrade — all rows permanently lost
  • TRUNCATE in migration
  • def downgrade(): pass — rollback silently does nothing
  • No downgrade() function
  • rename_table / rename_column without reverse
  • DROP VIEW without recreating in downgrade
  • ALTER TYPE ... ADD VALUE (PostgreSQL ENUM) — can't roll back once rows use the new value
  • Add column + migrate data + drop original in one migration

Warnings (review before deploying):

  • NOT NULL without server_default
  • Column type change
  • Raw op.execute() / context.execute() without reverse
  • op.execute(sa.text(...)) — SQL inside sa.text() wrapper now fully analyzed
  • op.bulk_insert() without corresponding DELETE in downgrade
  • Bulk UPDATE without a reverse UPDATE in downgrade
  • ON DELETE CASCADE added
  • CREATE INDEX without CONCURRENTLY (PostgreSQL)
  • ADD COLUMN with DEFAULT on large tables
  • CREATE UNIQUE CONSTRAINT on existing data
  • DROP INDEX without recreating
  • DROP CONSTRAINT without recreating
  • ALTER SEQUENCE / setval
  • NOT NULL via raw SQL without reverse
  • NOT NULL without restoring nullable in downgrade

Databases

Static analysis Dynamic verification
PostgreSQL Yes Yes
SQLite Yes Yes
MySQL / MariaDB Yes Yes
Oracle Yes Yes
SQL Server Yes Yes
pip install pytest-mrt[mysql]    # PyMySQL
pip install pytest-mrt[oracle]   # python-oracledb
pip install pytest-mrt[mssql]    # pymssql

pre-commit integration

Add to .pre-commit-config.yaml to run mrt check automatically before every push:

# Alembic
- repo: https://github.com/croc100/pytest-mrt
  rev: v1.8.0
  hooks:
    - id: mrt-check
      args: [alembic/versions/]

# Django
- repo: https://github.com/croc100/pytest-mrt
  rev: v1.8.0
  hooks:
    - id: mrt-check
      args: [myapp/migrations/]

Update rev to the latest release tag. Run pre-commit autoupdate to keep it current.

Incremental CI — --since

Check only migrations added since a given revision. Keeps CI fast on large codebases:

# Alembic — pass a revision ID
mrt check migrations/versions/ --since a1b2c3d4

# Django — pass app_label.migration_name (filename without .py)
mrt check myapp/migrations/ --since myapp.0010_add_email

Pass the last migration on the base branch; only PR-new migrations are scanned.

When --since is active, graph-level checks (orphan detection, data-hole analysis) are skipped. Run without --since periodically for full coverage. See the CLI reference for the full format specification.

CI/CD integration

Drop mrt check into any pipeline as a pre-deploy gate:

# GitHub Actions — blocks merge if unsafe migrations are detected
- name: Migration safety check
  run: mrt check alembic/versions/ --strict

Full examples for GitHub Actions, GitLab CI, Jenkins, and pre-commit hooks are in examples/ci-integration/.

Docker

Run tests locally against PostgreSQL or MySQL without installing anything:

docker compose run test-postgres
docker compose run test-mysql

See docker-compose.yml for the full configuration.

Performance

10 migrations 50 migrations 100 migrations
mrt check (static, no DB) 22 ms 108 ms 216 ms
mrt fixture (SQLite) 0.33 s 4.3 s 15.6 s

Safe to run mrt check on every commit. Dynamic suite fits comfortably for projects up to ~200 migrations. For larger codebases, use MRTConfig(skip={...}) to exclude already-reviewed revisions. See benchmarks for methodology and PostgreSQL/MySQL numbers.

Suppress known risks (v1.2.0)

Use # noqa: MRTxxx on any line to suppress a specific warning — the same convention as ruff and flake8:

def upgrade():
    op.drop_column("users", "phone")  # noqa: MRT103

To suppress all MRT warnings on a line:

    op.drop_column("users", "legacy_col")  # noqa

Legacy syntax # mrt: ignore is still supported for backward compatibility.

How it compares

pytest-mrt pytest-alembic alembic check django-test-migrations
Static analysis (no DB required) ✅ 44 patterns ❌ ❌ ❌
Dynamic rollback testing ✅ ✅ ❌ ✅
Data survival check (seeds rows, verifies after rollback) ✅ ❌ schema only ❌ ❌
Django support ✅ ❌ ❌ ✅
Pre-commit hook ✅ ❌ ❌ ❌
Inline suppression (# noqa: MRTxxx) ✅ ❌ ❌ ❌

The key difference from pytest-alembic: pytest-mrt seeds actual rows before each rollback and verifies they survive. A migration that reverses the schema cleanly but silently destroys data will pass pytest-alembic and fail pytest-mrt.

What's new in v1.7.0

  • mrt check --check-compat now works for Django migrations. Rolling-deploy compatibility checks (MRT7xx) were Alembic-only; Django operations now map to the same patterns — RemoveField (MRT701), RenameField (MRT702), DeleteModel/RenameModel/AlterModelTable (MRT703), and AddField NOT NULL without a default (MRT704). MRT705 (type change) stays Alembic-only.
mrt check myapp/migrations/ --check-compat

What's new in v1.6.0

  • Fine-grained migration step control — upgrade_to(), upgrade_one(), downgrade_one(), downgrade_to(), current_revision() let you test data migration logic at any point in the chain:
def test_data_migration(mrt):
    mrt.upgrade_to("abc123")          # upgrade to a specific revision
    mrt.seed("users", [...])          # seed data at that checkpoint
    mrt.upgrade_one()                 # apply exactly one more step
    assert mrt.current_revision() == "def456"
    mrt.downgrade_one()               # roll back one step
    mrt.downgrade_to("base")          # roll all the way back

Migrating from v1.4.x

v1.5.0 removed mrt fix and mrt clean-backups — migration code generation is a transform operation, not a verify operation, and was out of scope. Projects relying on these commands should pin pytest-mrt<1.5.0.

The fixable field in mrt check --format json output was also removed in v1.5.0.

Changelog

See CHANGELOG.md for the full release history.

Documentation

Full docs at croc100.github.io/pytest-mrt

Production SQLite monitoring

pytest-mrt catches rollback failures at test time. For production SQLite monitoring — schema drift detection, backup integrity, and continuous alerting — see Litescope.

# Catch drift in production after deploy
litescope monitor check production.db --baseline baseline.json

Sponsorship

pytest-mrt is MIT-licensed and free to use. If it saves you from a production incident, consider sponsoring development:

github.com/sponsors/croc100

Sponsorship directly funds:

  • New pattern development (Oracle, SQL Server, more Django patterns)
  • Maintained compatibility with new Alembic and SQLAlchemy releases

License

MIT

Metadata

Release files for pytest-mrt 1.9.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pytest-mrt 1.9.0
File Size Uploaded
pytest_mrt-1.9.0.tar.gz 266.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pytest-mrt 1.9.0
File Interpreter ABI Platform
pytest_mrt-1.9.0-py3-none-any.whl Python 3 none any Details

Total release size: 339.5 kB

Release files / pytest_mrt-1.9.0.tar.gz

Download URL pytest_mrt-1.9.0.tar.gz
Size 266.5 kB
Tags Source
SHA-256 checksum
How to use checksums
0bb4fad49ab700d5a70754b6d9d260421b5782f9c92df65cd5ad864347684b1c
BLAKE2b-256 checksum
How to use checksums
5bb7e0b7128d6bb87d9c1f5d9a1d39e43b70320506c48b86163135d94f68f02e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release files / pytest_mrt-1.9.0-py3-none-any.whl

Download URL pytest_mrt-1.9.0-py3-none-any.whl
Size 73.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
686d9022b9f52ad9f93983d7fee72ca97ff1666f8ada83a84de4611de4bf0ec7
BLAKE2b-256 checksum
How to use checksums
909260fc22549795a35fd1855f634c42cd637475c82b5709157d5b5e08ff38fe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 27, 2026.

Transparency log

Release history Release notifications | RSS feed

1.9.1

2 release files

This release

1.9.0 This release

2 release files

1.8.0

2 release files

1.7.0

2 release files

1.6.0

2 release files

1.5.0

2 release files

1.4.1

2 release files

1.4.0

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.1

2 release files

0.6.0

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page