All jobs

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 RushIn 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 with this body:

{
  "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.

The use case this job follows, with its automation and trigger: Review every pull request.

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.