{"id":50925019,"url":"https://github.com/croc100/pytest-mrt","last_synced_at":"2026-06-16T22:00:58.619Z","repository":{"id":362285150,"uuid":"1258231990","full_name":"croc100/pytest-mrt","owner":"croc100","description":"Catch database migration rollback failures before they reach production","archived":false,"fork":false,"pushed_at":"2026-06-10T12:12:51.000Z","size":593,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-06-10T14:09:48.505Z","etag":null,"topics":["alembic","database","devtools","migrations","pytest","python","rollback","sqlalchemy","testing"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/pytest-mrt/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/croc100.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-03T11:45:42.000Z","updated_at":"2026-06-10T12:13:01.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/croc100/pytest-mrt","commit_stats":null,"previous_names":["croc100/pytest-mrt"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/croc100/pytest-mrt","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croc100%2Fpytest-mrt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croc100%2Fpytest-mrt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croc100%2Fpytest-mrt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croc100%2Fpytest-mrt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/croc100","download_url":"https://codeload.github.com/croc100/pytest-mrt/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/croc100%2Fpytest-mrt/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34425024,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alembic","database","devtools","migrations","pytest","python","rollback","sqlalchemy","testing"],"created_at":"2026-06-16T22:00:24.495Z","updated_at":"2026-06-16T22:00:58.605Z","avatar_url":"https://github.com/croc100.png","language":"Python","funding_links":["https://github.com/sponsors/croc100"],"categories":[],"sub_categories":[],"readme":"# pytest-mrt\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://pypi.org/project/pytest-mrt\"\u003e\u003cimg src=\"https://img.shields.io/pypi/v/pytest-mrt?color=blue\" alt=\"PyPI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://pepy.tech/project/pytest-mrt\"\u003e\u003cimg src=\"https://static.pepy.tech/badge/pytest-mrt\" alt=\"Downloads\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/croc100/pytest-mrt/network/dependents\"\u003e\u003cimg src=\"https://img.shields.io/badge/used%20by-see%20dependents-informational\" alt=\"Used by\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/croc100/pytest-mrt/actions\"\u003e\u003cimg src=\"https://img.shields.io/github/actions/workflow/status/croc100/pytest-mrt/ci.yml?branch=main\u0026label=tests\" alt=\"CI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://codecov.io/gh/croc100/pytest-mrt\"\u003e\u003cimg src=\"https://codecov.io/gh/croc100/pytest-mrt/graph/badge.svg?token=CODECOV_TOKEN\" alt=\"Coverage\"\u003e\u003c/a\u003e\n  \u003cimg src=\"https://img.shields.io/badge/status-stable-brightgreen\" alt=\"Production/Stable\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue\" alt=\"Python 3.10-3.14\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/license-MIT-green\" alt=\"MIT License\"\u003e\n  \u003ca href=\"https://gcv-five.vercel.app/croc100/pytest-mrt\"\u003e\u003cimg src=\"https://img.shields.io/badge/contributors-GCV-6e40c9?logo=github\" alt=\"Contributors\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  A pytest plugin that catches database migration rollback failures before they reach production.\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"docs/demo.gif\" alt=\"mrt check catching DROP COLUMN data loss\" width=\"100%\" /\u003e\n\u003c/p\u003e\n\n---\n\n`alembic downgrade -1` ran clean. No errors. Your monitoring went green.\n\nBut the users' phone numbers are gone. The column came back. The data didn't.\n\npytest-mrt would have caught this before it reached production:\n\n```\n$ mrt check migrations/versions/\n\n                         Rollback Risk Analysis\n╭──────────┬────────┬───────────────────────────┬───────┬──────┬─────────────────────────────────────╮\n│ Revision │ Code   │ Pattern                   │ Sev   │ Line │ Message                             │\n├──────────┼────────┼───────────────────────────┼───────┼──────┼─────────────────────────────────────┤\n│ 042      │ MRT201 │ DROP COLUMN in upgrade    │ error │   18 │ op.drop_column('users', 'phone') —  │\n│          │        │                           │       │      │ column data is permanently lost     │\n│          │        │                           │       │      │ even if downgrade re-adds the column│\n╰──────────┴────────┴───────────────────────────┴───────┴──────┴─────────────────────────────────────╯\n1 error(s), 0 warning(s)\n```\n\nNon-invasive — installs in 2 minutes, zero changes to your existing tests.\n\n---\n\n## What it does\n\nMost tools verify that migrations *run* without errors.  \npytest-mrt verifies that your data *survives* a rollback.\n\nIt seeds real rows before each migration, rolls back, and checks nothing was lost.\nIt also statically scans migration files for 44 known dangerous patterns across both Alembic and Django migrations.\n\n## Install\n\n```bash\npip install pytest-mrt\n```\n\n## Setup (2 minutes)\n\nAdd this to `conftest.py`:\n\n```python\n# conftest.py\nimport os\nfrom pytest_mrt import MRTConfig\n\n\ndef pytest_configure(config):\n    config._mrt_config = MRTConfig(\n        alembic_ini=\"alembic.ini\",\n        db_url=os.environ.get(\"TEST_DATABASE_URL\", \"sqlite:///test.db\"),\n    )\n```\n\nThat's it. Run `pytest` and 6 safety tests appear automatically — no test files needed:\n\n```\nPASSED test_mrt_single_head          - Migration history has exactly one head\nPASSED test_mrt_upgrade              - alembic upgrade head completes without error\nPASSED test_mrt_downgrade_base       - alembic downgrade base then re-upgrade completes cleanly\nPASSED test_mrt_up_down_consistency  - Every migration is safely reversible\nPASSED test_mrt_static_no_errors     - Zero static analysis errors in all migration files\nPASSED test_mrt_schema_matches_models- Database schema matches ORM models after upgrade\n```\n\n\u003e Want to write custom rollback tests? Use the `mrt` fixture — just add it as a parameter to any test function, no import needed:\n\u003e\n\u003e ```python\n\u003e def test_migration_003(mrt):\n\u003e     mrt.assert_reversible(\"abc1234\")\n\u003e ```\n\n## Static analysis (no database needed)\n\n```bash\nmrt check migrations/versions/\n```\n\n```\n╭──────────┬──────────────────────────┬─────────┬──────┬─────────┬────────────────────────────────────╮\n│ Revision │ Pattern                  │ Sev     │ Line │ Code    │ Message                            │\n├──────────┼──────────────────────────┼─────────┼──────┼─────────┼────────────────────────────────────┤\n│ 004      │ DROP COLUMN in upgrade   │ error   │   12 │ MRT103  │ Data permanently lost on rollback  │\n│ 005      │ No-op downgrade          │ error   │    8 │ MRT102  │ downgrade() does nothing           │\n│ 006      │ INDEX without CONCURR.   │ warning │   19 │ MRT207  │ Locks table during index build     │\n╰──────────┴──────────────────────────┴─────────┴──────┴─────────┴────────────────────────────────────╯\n2 error(s), 1 warning(s)\n```\n\n## What gets caught\n\n**Errors** (will cause data loss or a broken rollback):\n\n- `op.drop_column()` in upgrade — data is gone even if downgrade re-adds the column\n- `op.drop_table()` in upgrade — all rows permanently lost\n- `TRUNCATE` in migration\n- `def downgrade(): pass` — rollback silently does nothing\n- No `downgrade()` function\n- `rename_table` / `rename_column` without reverse\n- `DROP VIEW` without recreating in downgrade\n- `ALTER TYPE ... ADD VALUE` (PostgreSQL ENUM) — can't roll back once rows use the new value\n- Add column + migrate data + drop original in one migration\n\n**Warnings** (review before deploying):\n\n- `NOT NULL` without `server_default`\n- Column type change\n- Raw `op.execute()` / `context.execute()` without reverse\n- `op.execute(sa.text(...))` — SQL inside `sa.text()` wrapper now fully analyzed\n- `op.bulk_insert()` without corresponding `DELETE` in downgrade\n- Bulk `UPDATE` without a reverse `UPDATE` in downgrade\n- `ON DELETE CASCADE` added\n- `CREATE INDEX` without `CONCURRENTLY` (PostgreSQL)\n- `ADD COLUMN` with `DEFAULT` on large tables\n- `CREATE UNIQUE CONSTRAINT` on existing data\n- `DROP INDEX` without recreating\n- `DROP CONSTRAINT` without recreating\n- `ALTER SEQUENCE` / `setval`\n- `NOT NULL` via raw SQL without reverse\n- `NOT NULL` without restoring `nullable` in downgrade\n\n## Databases\n\n| | Static analysis | Dynamic verification |\n|---|---|---|\n| PostgreSQL | Yes | Yes |\n| SQLite | Yes | Yes |\n| MySQL / MariaDB | Yes | Yes |\n| Oracle | Yes | Yes |\n| SQL Server | Yes | Yes |\n\n```bash\npip install pytest-mrt[mysql]    # PyMySQL\npip install pytest-mrt[oracle]   # python-oracledb\npip install pytest-mrt[mssql]    # pymssql\n```\n\n## pre-commit integration\n\nAdd to `.pre-commit-config.yaml` to run `mrt check` automatically before every push:\n\n```yaml\n# Alembic\n- repo: https://github.com/croc100/pytest-mrt\n  rev: v1.5.0\n  hooks:\n    - id: mrt-check\n      args: [alembic/versions/]\n\n# Django\n- repo: https://github.com/croc100/pytest-mrt\n  rev: v1.5.0\n  hooks:\n    - id: mrt-check\n      args: [myapp/migrations/]\n```\n\nUpdate `rev` to the latest release tag. Run `pre-commit autoupdate` to keep it current.\n\n## Incremental CI — `--since`\n\nCheck only migrations added since a given revision. Keeps CI fast on large codebases:\n\n```bash\n# Alembic — pass a revision ID\nmrt check migrations/versions/ --since a1b2c3d4\n\n# Django — pass app_label.migration_name (filename without .py)\nmrt check myapp/migrations/ --since myapp.0010_add_email\n```\n\nPass the last migration on the base branch; only PR-new migrations are scanned.\n\n\u003e 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](docs/cli.md#--since--incremental-scanning) for the full format specification.\n\n## CI/CD integration\n\nDrop `mrt check` into any pipeline as a pre-deploy gate:\n\n```yaml\n# GitHub Actions — blocks merge if unsafe migrations are detected\n- name: Migration safety check\n  run: mrt check alembic/versions/ --strict\n```\n\nFull examples for GitHub Actions, GitLab CI, Jenkins, and pre-commit hooks are in [`examples/ci-integration/`](examples/ci-integration/).\n\n## Docker\n\nRun tests locally against PostgreSQL or MySQL without installing anything:\n\n```bash\ndocker compose run test-postgres\ndocker compose run test-mysql\n```\n\nSee [`docker-compose.yml`](docker-compose.yml) for the full configuration.\n\n## Performance\n\n| | 10 migrations | 50 migrations | 100 migrations |\n|---|---|---|---|\n| `mrt check` (static, no DB) | 22 ms | 108 ms | 216 ms |\n| `mrt` fixture (SQLite) | 0.33 s | 4.3 s | 15.6 s |\n\nSafe to run `mrt check` on every commit. Dynamic suite fits comfortably for projects up to ~200 migrations.\nFor larger codebases, use `MRTConfig(skip={...})` to exclude already-reviewed revisions.\nSee [benchmarks](docs/benchmarks.md) for methodology and PostgreSQL/MySQL numbers.\n\n\n## Suppress known risks (v1.2.0)\n\nUse `# noqa: MRTxxx` on any line to suppress a specific warning — the same convention as ruff and flake8:\n\n```python\ndef upgrade():\n    op.drop_column(\"users\", \"phone\")  # noqa: MRT103\n```\n\nTo suppress all MRT warnings on a line:\n\n```python\n    op.drop_column(\"users\", \"legacy_col\")  # noqa\n```\n\nLegacy syntax `# mrt: ignore` is still supported for backward compatibility.\n\n## How it compares\n\n| | pytest-mrt | [pytest-alembic](https://github.com/schireson/pytest-alembic) | [alembic check](https://alembic.sqlalchemy.org/en/latest/ops.html#alembic.operations.Operations.check) | [django-test-migrations](https://github.com/wemake-services/django-test-migrations) |\n|---|:---:|:---:|:---:|:---:|\n| Static analysis (no DB required) | ✅ 44 patterns | ❌ | ❌ | ❌ |\n| Dynamic rollback testing | ✅ | ✅ | ❌ | ✅ |\n| **Data survival check** (seeds rows, verifies after rollback) | ✅ | ❌ schema only | ❌ | ❌ |\n| Django support | ✅ | ❌ | ❌ | ✅ |\n| Pre-commit hook | ✅ | ❌ | ❌ | ❌ |\n| Inline suppression (`# noqa: MRTxxx`) | ✅ | ❌ | ❌ | ❌ |\n\nThe 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.\n\n## What's new in v1.6.0\n\n- **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:\n\n```python\ndef test_data_migration(mrt):\n    mrt.upgrade_to(\"abc123\")          # upgrade to a specific revision\n    mrt.seed(\"users\", [...])          # seed data at that checkpoint\n    mrt.upgrade_one()                 # apply exactly one more step\n    assert mrt.current_revision() == \"def456\"\n    mrt.downgrade_one()               # roll back one step\n    mrt.downgrade_to(\"base\")          # roll all the way back\n```\n\n## Migrating from v1.4.x\n\n**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\u003c1.5.0`.\n\nThe `fixable` field in `mrt check --format json` output was also removed in v1.5.0.\n\n## Changelog\n\nSee [CHANGELOG.md](CHANGELOG.md) for the full release history.\n\n## Documentation\n\nFull docs at **[croc100.github.io/pytest-mrt](https://croc100.github.io/pytest-mrt)**\n\n- [Getting started (step-by-step)](https://croc100.github.io/pytest-mrt/quickstart/)\n- [All 44 patterns explained](https://croc100.github.io/pytest-mrt/patterns/)\n- [CLI \u0026 fixture reference](https://croc100.github.io/pytest-mrt/cli/)\n- [Detection accuracy report](docs/accuracy.md) — what each pattern catches and doesn't catch\n- [API reference](docs/api.md) — stable public API\n- [FAQ](docs/faq.md) — timeouts, large codebases, Django, error handling\n\n## Production SQLite monitoring\n\npytest-mrt catches rollback failures at test time. For production SQLite monitoring — schema drift detection, backup integrity, and continuous alerting — see **[Litescope](https://github.com/croc100/Litescope)**.\n\n```bash\n# Catch drift in production after deploy\nlitescope monitor check production.db --baseline baseline.json\n```\n\n---\n\n## Sponsorship\n\npytest-mrt is MIT-licensed and free to use. If it saves you from a production incident, consider sponsoring development:\n\n**[github.com/sponsors/croc100](https://github.com/sponsors/croc100)**\n\nSponsorship directly funds:\n- New pattern development (Oracle, SQL Server, more Django patterns)\n- Maintained compatibility with new Alembic and SQLAlchemy releases\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcroc100%2Fpytest-mrt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcroc100%2Fpytest-mrt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcroc100%2Fpytest-mrt/lists"}