Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,10 @@ jobs:
allow-prereleases: true

- name: Install
run: pip install -e ".[dev]" coverage
run: pip install -e ".[dev]"

- name: Run tests
run: coverage run -m pytest tests/ -v --ignore=tests/test_postgres.py --ignore=tests/test_mysql.py --ignore=tests/test_oracle.py --ignore=tests/test_mssql.py

- name: Generate coverage report
run: coverage xml
run: pytest tests/ -v --ignore=tests/test_postgres.py --ignore=tests/test_mysql.py --ignore=tests/test_oracle.py --ignore=tests/test_mssql.py --cov=pytest_mrt --cov-report=xml

- name: Upload coverage
uses: codecov/codecov-action@v7
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

---

## [1.5.0] — 2026-06-11

### Removed
- **`mrt fix` command and the entire auto-fix subsystem** (`core/fixer.py`, `adapters/django_fixer.py`, `commands/fix.py`). Migration code generation is a *transform*, not a *verify*, operation and was out of scope for a rollback-testing tool. **Breaking change.** Projects that need automatic `downgrade()` generation should pin `pytest-mrt<1.5.0`.
- **`mrt clean-backups` command** — it only cleaned the `_mrt_backups` table created by `mrt fix`-generated migrations, which no longer exist. **Breaking change.** If you have `_mrt_backups` tables from a previous `mrt fix` run, drop them manually or run `pytest-mrt<1.5.0` one last time.
- **`fixable` field in `mrt check --format json` output** — it advertised the now-removed auto-fix capability. **Breaking change** for downstream tooling that read this field.

### Fixed
- **`RollbackVerifier` false positive on failed custom seeds** — a custom seed row whose `INSERT` failed (caught and warned) was still tracked, so `verify()` reported it as "lost after rollback" and blamed the migration for data the seed never inserted. Rows are now tracked only after a successful insert.

---

## [1.4.1] — 2026-06-11

### Added
Expand Down
3 changes: 0 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ pytest_mrt/
├── core/
│ ├── ast_analyzer.py # MigrationAST — Alembic AST parsing
│ ├── detector.py # Built-in Alembic risk checks
│ ├── fixer.py # mrt fix — auto-generate downgrade()
│ ├── graph.py # Migration dependency graph
│ ├── html_report.py # mrt report — HTML output
│ ├── runner.py # MigrationRunner — wraps alembic commands
Expand All @@ -135,7 +134,6 @@ tests/
├── test_cli.py
├── test_detector.py
├── test_django_detector.py
├── test_fixer.py
├── test_graph.py
├── test_html_report.py
├── test_integration.py # SQLite end-to-end tests
Expand Down Expand Up @@ -167,7 +165,6 @@ Check [open issues](https://github.com/croc100/pytest-mrt/issues) and the [ROADM
- Add an example to `examples/` for a common use case
- Add a new CI integration guide to `examples/ci-integration/`
- Improve HTML report output
- Add a new `mrt fix` heuristic

### Higher effort (discuss in an issue first)

Expand Down
23 changes: 0 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,27 +160,6 @@ pip install pytest-mrt[oracle] # python-oracledb
pip install pytest-mrt[mssql] # pymssql
```

## Auto-fix missing reverse operations

`mrt fix` generates missing reverse operations for both Alembic and Django migrations.

**Alembic** — generates a missing or stub `downgrade()`:
```bash
mrt fix migrations/versions/0042_drop_phone.py --apply
```

**Django** — adds `reverse_sql`, `reverse_code`, and full backup/restore scaffolding for data-loss operations (`RemoveField`, `DeleteModel`):
```bash
mrt fix myapp/migrations/0042_remove_user_phone.py --apply
```

For `RemoveField` and `DeleteModel`, the generated code backs up data to a `_mrt_backups` table before the migration runs, and restores it on rollback. After deployment is confirmed stable, clean up the backup rows:

```bash
mrt clean-backups --db $DATABASE_URL
mrt clean-backups --db $DATABASE_URL --label 0042_remove_user_phone --yes
```

## pre-commit integration

Add to `.pre-commit-config.yaml` to run `mrt check` automatically before every push:
Expand Down Expand Up @@ -279,7 +258,6 @@ Legacy syntax `# mrt: ignore` is still supported for backward compatibility.
| Dynamic rollback testing | ✅ | ✅ | ❌ | ✅ |
| **Data survival check** (seeds rows, verifies after rollback) | ✅ | ❌ schema only | ❌ | ❌ |
| Django support | ✅ | ❌ | ❌ | ✅ |
| Auto-fix (`mrt fix`) | ✅ | ❌ | ❌ | ❌ |
| Pre-commit hook | ✅ | ❌ | ❌ | ❌ |
| Inline suppression (`# noqa: MRTxxx`) | ✅ | ❌ | ❌ | ❌ |

Expand All @@ -290,7 +268,6 @@ The key difference from pytest-alembic: pytest-mrt seeds actual rows before each
- **`mrt check --format json/html`** — structured JSON output for CI tooling; self-contained HTML safety report
- **`mrt check --watch`** — re-runs automatically whenever a migration file changes
- **`mrt check --min-revision`** — skip revisions older than a configured floor (mirrors `MRTConfig.minimum_downgrade_revision`)
- **`mrt fix --apply` batch mode** — fix all auto-fixable migrations at once; `--dry-run` previews without writing
- **Django squashmigrations detection** — MRT601/MRT602 catch unsafe `RunPython` in squashed migrations
- **`minimum_downgrade_revision` in dynamic tests** — floor now respected by the `mrt` fixture `check_all()`, not just static analysis

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ These are not committed to a specific version yet:
- **Sentry integration** — report migration failures as Sentry events
- **GitHub App** — automated PR comments with migration risk summary
- **VS Code extension** — inline warnings in migration files
- **`mrt fix --apply` batch mode** — fix all migration files in a directory at once

---

## What won't be in scope

- Executing migrations in production (this is a *testing* tool only)
- **Migration code generation / auto-fix** — pytest-mrt verifies migrations; it does not rewrite them. The former `mrt fix` / `mrt clean-backups` commands were removed in v1.5.0 (out of scope: "transform", not "verify")
- Schema diff tools (use `alembic check` or `django-migration-linter`)
- ORM-agnostic support (focused on Alembic and Django)

Expand Down
15 changes: 0 additions & 15 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,21 +298,6 @@ mrt check alembic/versions/ --format json

---

### `mrt fix <migration_file>`

Suggest or apply a missing/broken `downgrade()` function.

```
mrt fix alembic/versions/001_create_users.py
mrt fix alembic/versions/001_create_users.py --apply
```

| Option | Default | Description |
|--------|---------|-------------|
| `--apply` | `False` | Write the suggested fix to the file |

---

### `mrt report <versions_dir>`

Generate an HTML safety report for all migrations.
Expand Down
2 changes: 0 additions & 2 deletions docs/best-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ def downgrade():
op.drop_table("users")
```

Use `mrt fix` to auto-generate a `downgrade()` when you're unsure what it should do.

### 2. Test rollback locally before code review

```bash
Expand Down
87 changes: 1 addition & 86 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ For GitHub Actions integration, see [croc100/pytest-mrt-action](https://github.c
| `mrt check <dir> --format json` | Structured JSON output for CI / downstream tools | No |
| `mrt check <dir> --format html` | Self-contained HTML safety report | No |
| `mrt check <dir> --watch` | Re-run on file change (dev mode) | No |
| `mrt fix <file>` | Auto-generate reverse operations for a single file | No |
| `mrt fix --apply` | Batch-fix all auto-fixable migrations in a directory | No |
| `mrt clean-backups` | Remove `_mrt_backups` rows after stable deployment | Yes |
| `mrt drift` | Compare live DB schema against ORM models | Yes |
| `mrt report <dir>` | HTML safety report of entire migration history | No |
| `mrt explain <file>` | AI explanation in plain English | No (needs API key) |
Expand Down Expand Up @@ -158,8 +155,7 @@ Output schema:
"rule": "MRT103",
"severity": "error",
"pattern": "DROP COLUMN in upgrade",
"message": "Column dropped — data permanently lost on rollback",
"fixable": false
"message": "Column dropped — data permanently lost on rollback"
}
]
}
Expand Down Expand Up @@ -232,87 +228,6 @@ When there are no problems:

---

## CLI: `mrt fix`

Auto-generates missing reverse operations. Works for both Alembic and Django migrations.

```bash
# Single-file mode
mrt fix <migration-file> # preview suggested fix
mrt fix <migration-file> --apply # write fix to file

# Batch mode (v1.4.0)
mrt fix --apply # fix all auto-fixable migrations in the detected directory
mrt fix --apply --dry-run # preview without writing
mrt fix --apply --dir <path> # specify the directory explicitly
```

### Batch mode options

| Option | Description |
|---|---|
| `--apply` | Required to write fixes. Without a file argument, scans the whole directory. |
| `--dry-run` | Preview what would change without writing. Use with `--apply`. |
| `--dir <path>` | Directory to scan. Auto-detected from cwd if omitted (`migrations/`, `alembic/versions/`, `versions/`). |

### Alembic

Generates a missing or stub `downgrade()` function with the inverse operations inferred from `upgrade()`.

### Django (v1.3.0)

Detects and fixes four operation types:

| Operation | Fix | Confidence |
|---|---|---|
| `RunSQL` without `reverse_sql` | Adds `reverse_sql=migrations.RunSQL.noop` | High |
| `RunPython` without `reverse_code` | Adds `reverse_code=migrations.RunPython.noop` | Medium |
| `RemoveField` | Injects `RunPython(backup, restore)` before the op | Medium |
| `DeleteModel` | Injects `RunPython(backup, restore)` before the op | Medium |

For `RemoveField` and `DeleteModel`, the generated code:

1. **Backs up** the column/row data into a `_mrt_backups` table using keyset pagination (safe for large tables, no server-side cursors required)
2. **Restores** the data when the migration is reversed, with constraint checking disabled for FK safety
3. **Inlines a type codec** (`__mrt_enc`/`__mrt_dec`) directly into the migration file — no runtime dependency on pytest-mrt in your production migrations

After you've confirmed the deployment is stable and rollback is no longer needed:

```bash
mrt clean-backups --db $DATABASE_URL
mrt clean-backups --db $DATABASE_URL --label 0042_remove_user_phone --yes
```

#### Known limitations (documented in generated code)

- Type fidelity depends on the codec: complex custom types may not round-trip perfectly
- Very large tables (millions of rows) increase migration time proportionally
- The backup table (`_mrt_backups`) persists until explicitly cleaned

---

## CLI: `mrt clean-backups` (v1.3.0)

Removes rows from the `_mrt_backups` table created by Django `mrt fix` backup/restore operations.

```bash
mrt clean-backups --db <database-url>
mrt clean-backups --db $DATABASE_URL --list # preview without deleting
mrt clean-backups --db $DATABASE_URL --label 0042 # delete one migration's backup
mrt clean-backups --db $DATABASE_URL --yes # skip confirmation prompt
```

### Options

| Option | Description |
|---|---|
| `--db` | SQLAlchemy database URL. Also reads from `DATABASE_URL` env var. |
| `--label` | Delete only rows for this migration label. Omit to delete all backup data. |
| `--list`, `-l` | List backup labels and row counts without deleting. |
| `--yes`, `-y` | Skip the confirmation prompt. |

---

## CLI: `mrt version`

```bash
Expand Down
4 changes: 1 addition & 3 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,13 @@ pytest-mrt auto-detects which framework you're using based on whether the migrat
|---|---|---|
| **Static analysis** | Yes | Yes |
| **Dynamic rollback** | Yes | Yes (`DjangoMigrationRunner`) |
| **`mrt fix`** | Yes | Yes (v1.3.0) |

### Django migrations don't have `downgrade()` — how does pytest-mrt help?

For Django, pytest-mrt provides **static analysis**, **dynamic rollback verification**, and (since v1.3.0) **auto-fix**:
For Django, pytest-mrt provides **static analysis** and **dynamic rollback verification**:

- **Static**: detects `RemoveField`, `DeleteModel`, `RunPython` without `reverse_code`, `RunSQL` without `reverse_sql`, unsafe `AddField` patterns, and more (10 Django-specific patterns).
- **Dynamic**: `DjangoMigrationRunner` runs the full `migrate` / `migrate --backwards` cycle and verifies data integrity, just like Alembic mode.
- **Fix** (`mrt fix --apply`): adds `reverse_sql`/`reverse_code` where missing and generates transactional backup/restore scaffolding for `RemoveField` and `DeleteModel`. The generated code is self-contained — no runtime dependency on pytest-mrt in your production migrations.

---

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "pytest-mrt"
version = "1.4.1"
version = "1.5.0"
description = "Catch database migration rollback failures before they reach production"
readme = "README.md"
license = { text = "MIT" }
Expand Down Expand Up @@ -47,6 +47,7 @@ Changelog = "https://github.com/croc100/pytest-mrt/releases"
[project.optional-dependencies]
dev = [
"pytest",
"pytest-cov",
"psycopg2-binary",
"hatch",
"ruff",
Expand Down
Loading
Loading