# Review Database Migrations in Rails Pull Requests

Yes. An agent runs the pull request's Active Record migrations up and down on a fresh Postgres and comments on locks, table rewrites and data loss before merge.

[Run this on Rush](https://cloud.getrush.ai/run/review-database-migrations/rails/), 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 how the app already guards migrations. In a Rails app like rubygems/rubygems.org, the `Gemfile` pins `rails ~> 8.1.3`, `pg ~> 1.6` and `strong_migrations ~> 2.8`, and `config/initializers/strong_migrations.rb` sets `lock_timeout` to 10 seconds, `statement_timeout` to 1 hour, `auto_analyze = true`, `target_version = "18"`, and `start_after = 20_230_825_182_405`, so only migrations after that timestamp are checked. `docker-compose.yml` pins Postgres 18.6 by digest. The agent uses that same Postgres major in the sandbox so the lock behavior it observes is the one production has.

It reads the project's history for the patterns a new migration should follow. `db/migrate/` holds 245 migrations, and recent ones show the house style: `20261012233055_add_recently_published_index_to_versions.rb` uses `disable_ddl_transaction!`, `algorithm: :concurrently` and `if_not_exists: true`, removes a leftover invalid index first, and sets `lock_timeout = 0` inside `safety_assured` only for the concurrent build. `20260819160003_remove_old_version_unique_indexes.rb` drops unique indexes concurrently and documents in its `down` that it is irreversible once new rows land. A new migration that skips these patterns on the same tables is a comment.

Then it runs the migrations. It creates the database from `db/schema.rb` with `bin/rails db:prepare`, loads rows with `bin/rails db:seed` so tables are not empty, and runs `bin/rails db:migrate` for the new files while a second connection reads `pg_locks`. Strong Migrations raises on an unsafe operation before it runs, and the agent records each such error as a finding. It then runs `bin/rails db:rollback STEP=n` and `db:migrate` again to prove each migration reverses, and checks `git diff db/schema.rb` so the committed schema matches what the migrations produce.

Last, it reads what Strong Migrations cannot judge. Every `safety_assured` block in the new files gets a comment that says what it hides and whether that is safe on that table. A `remove_column` that a model still reads, with no `self.ignored_columns` in a prior deploy, is data the app will fail on. A `change_column` that narrows a type, a `change_column_null` without a backfill, a backfill inside the migration's transaction rather than in a GoodJob job, and an `add_reference` without `index: { algorithm: :concurrently }` each get a comment with the safe sequence.

## Start it with one API call

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

```json
{
  "agent": "claude",
  "input": "Review the migrations added in the open pull request. Read config/initializers/strong_migrations.rb and a few recent files in db/migrate for the project's conventions. Start a fresh Postgres of the version docker-compose.yml uses, run bin/rails db:prepare and db:seed, then bin/rails db:migrate for the new migrations while watching pg_locks, then db:rollback and db:migrate again, and check git diff db/schema.rb. Comment on any statement that rewrites or locks a whole table, drops or narrows a column with data, hides a risk inside safety_assured, or cannot be rolled back, with the safe sequence to use instead. Do not change the migration yourself.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-rails-app/pull/123"
    }
  ]
}
```

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

What comes back is a review on the pull request with comments on the lines of each new file in `db/migrate/`, and on `db/schema.rb` when it does not match what the migrations produce. Each comment names the operation, what it does to the table (an `AccessExclusiveLock`, a rewrite, a dropped column), what the sandbox run showed, and the sequence to use instead: ignore the column in one deploy and drop it in the next, add the index concurrently with `disable_ddl_transaction!`, add a check constraint `NOT VALID` and validate it separately.

The session keeps the evidence for each comment: the `db:migrate` output with any `StrongMigrations::UnsafeMigration` error, the lock rows read during each step, the rollback and re-run, and the schema diff. A reviewer can tell which comments come from a failed check and which from reading the code.

Before merging, a reviewer weighs each lock against the real size of the table, which a seeded sandbox does not have, and confirms any `safety_assured` the agent questioned. An irreversible `down`, or a column dropped while a model still reads it, should be resolved first.

## Run it on every pull request that adds a migration

An automation with a GitHub trigger on `pull_request.opened` and `pull_request.synchronize` can start this review on every pull request. To run it only for schema changes, receive GitHub's `pull_request` webhook on your server, check whether the changed files include `db/migrate/`, and send this body with the pull request URL in `resources` and the head SHA as the `Idempotency-Key`.

When the author pushes a fix, send a follow-up with `POST /o/{org}/sessions/{id}/messages` asking it to re-run the changed migration. The parked sandbox keeps its database and installed gems, so the second pass starts from the state the first one left.

## Why run it on Rush?

Strong Migrations stops many unsafe operations, but it cannot tell whether a `safety_assured` block is justified, whether a dropped column is still read, or whether a `down` really reverses. Answering those means running the migration up and down on a populated database of the right Postgres version, and on a laptop that means resetting the development database each time. On Rush each review gets its own sandbox with its own Postgres, built from `db/schema.rb` for that review alone.

Every comment is backed by output in the session's transcript, from the migrate and rollback runs to the lock rows, so the reviewer can check the claim rather than take it on trust. Reviews of pull requests on different branches run side by side, each with its own database, and none of them is given a production database URL.

## Frequently asked questions

### We already use Strong Migrations. What does this add?

Strong Migrations checks operations as they run and only after `start_after`. The agent runs the migration so those checks fire, and also reviews what they allow: each `safety_assured` block, columns dropped while a model still uses them, missing `ignored_columns`, and `down` methods that do not reverse.

### Does it run against my production database?

No. It creates a new Postgres inside its own sandbox, loads `db/schema.rb` and the seeds, and migrates that. The run has no database URL other than the sandbox's own unless you put one in a Project secret, and the prompt does not ask for one.

### Will it check that the migration can be rolled back?

Yes. It runs `bin/rails db:rollback` for the new migrations and then `db:migrate` again. A migration whose `down` raises `ActiveRecord::IrreversibleMigration`, or fails because rows now violate a restored unique index, gets a comment quoting the error.

### What about data backfills?

A backfill in a migration holds its transaction for the whole update. The agent flags one on a table that may be large and suggests moving it to a background job, such as a GoodJob job in an app that already uses GoodJob, with the migration only adding the column.
