# Review Alembic Migrations in FastAPI Pull Requests

An agent reads the Alembic migrations a FastAPI pull request adds, applies them to a fresh sandbox Postgres with data, and reviews locks, data loss and downgrades.

[Run this on Rush](https://cloud.getrush.ai/run/review-database-migrations/fastapi/), in a Project that has your repository.

## What the agent does

The agent checks out the pull request's branch in its own sandbox and reads the migration setup before the migrations. In a FastAPI app like fastapi/full-stack-fastapi-template, `backend/alembic.ini` points `script_location` at `app/alembic`, `backend/app/alembic/env.py` takes the URL from `settings.DATABASE_URL`, sets `target_metadata` to `SQLModel.metadata` and configures `compare_type=True`, and `backend/scripts/prestart.sh` is the production path: `alembic upgrade head` followed by `python app/initial_data.py`. `pyproject.toml` requires Python `>=3.14` with `alembic>=1.19.1`, `sqlmodel>=0.0.46` and `psycopg[binary]`, and `mypy` and `ruff` both exclude the `alembic` directory, so the migrations get no static check the rest of the code gets.

Then it reads the chain. The existing revisions under `backend/app/alembic/versions/` go from `e2412789c190` (initialize models) through `9c0a54914c78` (string lengths), `d98dd8ec85a3` (integer ids replaced with UUIDs) and `1a31ce608336` (cascade delete) to `fe56fa70289e` (add `created_at` to user and item), each with a `down_revision` that the new file must extend. The agent runs `alembic heads` to confirm the pull request adds exactly one head, `alembic history` to see where it attaches, and reads the models in `backend/app/models.py` to compare what the migration does with what the `User` and `Item` tables now declare.

To review with evidence rather than opinion, the agent starts the `postgres:18` the repository's `compose.yml` names, applies the chain up to the pull request's `down_revision`, loads synthetic users and items, and only then applies the new revision with `alembic upgrade head` while a second session holds a transaction on the affected table, measuring whether the DDL waits. It reads the SQL with `alembic upgrade --sql` for the offline form, looks for the patterns that hurt on a live database: a `NOT NULL` added without a default, a column dropped or narrowed, a type change that rewrites the table, a backfill `UPDATE` inside the same transaction as the DDL (`d98dd8ec85a3` is the local example of that pattern, with its `UPDATE "user" SET new_id = uuid_generate_v4()`), and an index created without `CONCURRENTLY`.

Finally it runs `alembic downgrade -1` and `upgrade head` again to see whether the downgrade restores the schema and what data it discards, runs `alembic check` to see whether the models and the migrated schema still agree, and runs the test suite with `bash scripts/tests-start.sh`, which the `conftest.py` session fixture drives through `init_db` against the same database. Each observation becomes a comment on the changed line it concerns.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "Review the Alembic migrations added by the open pull request in resources. Read alembic.ini, alembic/env.py, the prestart script, pyproject.toml and the models first, and confirm the new revisions attach to the current head with alembic heads and alembic history. Start the Postgres version the repository's compose file names in the sandbox with a fresh database and synthetic data only, never a production connection. Apply the chain up to the pull request's down_revision, load representative rows, then apply the new revisions while a second session holds a transaction on the affected tables and report any measured wait. Read the generated SQL with alembic upgrade --sql and flag NOT NULL without a default, dropped or narrowed columns, type changes that rewrite tables, backfills inside the DDL transaction, and indexes created without CONCURRENTLY. Run alembic downgrade and upgrade again and say what the downgrade discards, run alembic check against the models, and run the test suite. Comment on the changed lines with evidence and a safer staged alternative where one exists, and return migration-review.md with every command, its output and the limits of what a fresh database can show. Do not edit the migrations or open another pull request.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app/pull/42"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

The pull request receives review comments on the specific lines of the new revision file: the `op.alter_column` that adds `NOT NULL` without a server default, the `op.execute` backfill that runs inside the DDL transaction, the `op.create_index` that would block writes, each with the SQL Alembic generated, what happened when the agent applied it with a concurrent transaction open, and a staged alternative in two or three migrations where that is safer.

`migration-review.md` comes back as an artifact on the session. It lists the revision ids and their `down_revision`, the `alembic heads` and `alembic history` output, the Postgres version used, the rows loaded, each command with its output in order, the measured waits, what `alembic downgrade -1` restored and what it discarded, the `alembic check` result, and the test run. A limits section says what a fresh database cannot show, such as how long a rewrite takes on a table with millions of rows.

A reviewer reads the comments on the diff, opens the report for the commands behind them, and decides whether to merge, split or rewrite. The migrations themselves are untouched; the agent opens no pull request of its own.

## Run it on every pull request that touches alembic/versions

In the console, add an automation to the Project with this prompt and a GitHub trigger on `pull_request.opened` and `pull_request.synchronize`. Only work by an owner, member or collaborator fires it, and pull requests from forks never do. Each push starts a run on the pull request's branch; a pull request that adds no file under `alembic/versions` ends with a short note and no comments.

Without the console, your own server can do the same: `POST /o/{org}/sessions` with the pull request URL in `resources`, one call per pull request, and a `webhook_url` on the key to learn when the review is posted.

## Why run it on Rush?

Migration review by eye finds the obvious `drop_column` and misses the lock. Applying the migration to a real Postgres with data in it, while another session holds a transaction, is what shows whether the `ALTER TABLE` waits, and that needs a database the reviewer does not want on their laptop. The sandbox starts the exact Postgres major the repository's compose file names, and the transcript keeps every command and wait.

The review is a comment on the line with the SQL and the measurement behind it, not a general warning about migrations. A reviewer who disagrees can replay the same commands from the report against a staging copy, and the pull request author gets a staged alternative they can apply instead of a request to be more careful.

## Frequently asked questions

### Does applying the migration to a fresh database prove it is safe in production?

No, and the report says so in its limits section. A fresh database shows the SQL, the lock behavior against a concurrent transaction, and whether the downgrade works; it cannot show how long a table rewrite takes on production-sized data. Comments that depend on size say so and name the table.

### Can it tell what a downgrade loses?

Yes, by running it. After `alembic downgrade -1` the agent compares the schema and the synthetic rows with what was there before the upgrade. The template's own `fe56fa70289e` downgrade drops the `created_at` columns, which is the kind of loss the comment names.

### Will it rewrite a dangerous migration for me?

No. The prompt forbids editing the migrations or opening another pull request. The comment proposes the staged alternative in words and SQL, and the author decides. A follow-up run with a different prompt can open a pull request with the staged version if you want that.

### The migrations are excluded from mypy and ruff. Does the review cover them?

Yes. In the template `pyproject.toml` excludes the `alembic` directory from both tools, which is one reason the review runs the migration rather than reading it. The agent also runs `alembic check` so a model change with no migration, or a migration that diverges from the models, is caught.
