# Review Database Migrations in Next.js Pull Requests

Yes. An agent applies the pull request's Drizzle migration to a fresh Postgres and comments on any statement that locks a table, drops data or cannot be undone.

[Run this on Rush](https://cloud.getrush.ai/run/review-database-migrations/nextjs/), 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 finds where migrations live before reading any SQL. In a Next.js app like nextjs/saas-starter, `drizzle.config.ts` points `schema` at `./lib/db/schema.ts` and `out` at `./lib/db/migrations` with `dialect: 'postgresql'`, and `package.json` has `db:generate` (`drizzle-kit generate`) and `db:migrate` (`drizzle-kit migrate`). It lists the new `.sql` files in that folder and the new entries in `lib/db/migrations/meta/_journal.json`, which is what `drizzle-kit migrate` reads to decide what to apply. A migration file with no journal entry, or a journal entry with no file, is the first thing it flags, because one of them never runs.

Next it compares the migration with the schema it claims to produce. It runs `drizzle-kit generate` on the branch: if the generated output is not empty, the committed SQL and `schema.ts` disagree, and the comment says which table or column differs. It also reads how the project already writes migrations. The starter has a single migration, `0000_soft_the_anarchist.sql`, which wraps each foreign key in a `DO $$ BEGIN ... EXCEPTION WHEN duplicate_object THEN null; END $$;` block and creates tables with `IF NOT EXISTS`, and `schema.ts` defines no secondary indexes, so every foreign key on `activity_logs`, `invitations` and `team_members` is unindexed today.

Then it runs the migrations for real. It starts Postgres 16.4 in the sandbox, the version `lib/db/setup.ts` writes into its `docker-compose.yml`, applies the `main` branch's migrations, and inserts rows into `users`, `teams` and `activity_logs` with plain SQL so the tables are not empty. It does not use `pnpm db:seed`, because `lib/db/seed.ts` creates products and prices through the Stripe API. Then it applies the pull request's migration with `POSTGRES_URL` pointing at that database. While it runs, the agent checks `pg_locks` for `AccessExclusiveLock` on existing tables and times each statement, so a lock or a rewrite is observed rather than guessed.

With that output it reviews each statement. `ALTER TABLE ... ADD COLUMN ... NOT NULL` without a default on a populated table, `ALTER COLUMN ... TYPE` that rewrites the table, `CREATE INDEX` without `CONCURRENTLY` on a large table, a foreign key added without `NOT VALID`, a `DROP COLUMN` the app code in `lib/db/queries.ts` still selects, or a `varchar(255)` narrowed to `varchar(50)` all get a comment with the safe sequence instead: add the column nullable, backfill, then set `NOT NULL`; add the constraint `NOT VALID` and validate it in a second migration. Drizzle writes no down migrations, so for each destructive statement the comment also says how to undo it by hand.

## 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 drizzle.config.ts and lib/db/migrations/meta/_journal.json, check that each new SQL file has a journal entry, and run drizzle-kit generate to confirm the migrations match lib/db/schema.ts. Start a fresh Postgres in the sandbox, apply the main branch's migrations, insert sample rows with SQL (lib/db/seed.ts calls Stripe, so do not run it), then apply the new ones with drizzle-kit migrate while watching pg_locks. Comment on any statement that rewrites or locks a whole table, drops or narrows a column with data, adds a foreign key without an index, 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-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 the new `.sql` file under `lib/db/migrations/`. Each comment names the statement, what it does to an existing table (a full rewrite, an `AccessExclusiveLock`, a dropped column), the evidence from the sandbox run, and the replacement sequence, often split into two migrations so the slow step does not hold a lock. When the migration and `schema.ts` disagree, the comment quotes the extra SQL `drizzle-kit generate` produced.

The session's transcript keeps the evidence: the Postgres container start, the `main` migrations and the sample rows, `drizzle-kit migrate` output for the new file, and the `pg_locks` rows seen during each statement. If a migration fails to apply at all, the review leads with the error and the statement that raised it.

Before merging, a reviewer weighs each comment against the real table sizes in production, which the sandbox does not have, and decides whether a lock on a small table is acceptable. A dropped or narrowed column that code still reads is the comment that should block the merge.

## Run it on every pull request that touches lib/db/migrations

An automation with a GitHub trigger on `pull_request.opened` and `pull_request.synchronize` starts this review; to keep it to schema changes, have your server receive GitHub's `pull_request` webhook, check whether the changed files include `lib/db/migrations/`, and only then send this body with the pull request URL in `resources`. Use the head SHA as the `Idempotency-Key` so a redelivery does not start a second review.

When the author pushes a fix, send a follow-up message to the same session with `POST /o/{org}/sessions/{id}/messages`. The parked sandbox wakes with its Postgres data directory and the `main` migrations already applied, so the second pass only applies the changed file.

## Why run it on Rush?

Most migration reviews read the SQL and guess. Knowing whether a statement takes an `AccessExclusiveLock` or rewrites the table depends on the Postgres version and the column types, and the only sure way to know is to run it against a database with rows in it. Doing that for every pull request on a laptop means resetting a local database each time and risking the one you develop against. On Rush the agent gets its own sandbox with its own Postgres, built from `main` for each review, and throws it away afterwards.

The comments come with the run that produced them. A reviewer can read the lock rows and the timings for each statement in the session, rather than trusting an opinion about locks. Reviews for pull requests on different branches run at once, each against its own database, so two schema changes in flight never interfere.

## Frequently asked questions

### Does it run migrations against my real database?

No. It starts a new Postgres in its own sandbox, applies the `main` branch's migrations and sample rows, and applies the pull request's migration there. Your `POSTGRES_URL` is never given to it unless you add it, and the prompt does not ask for it.

### How does it know a statement locks the table?

It applies the migration while querying `pg_locks` from a second connection and reports the lock mode it saw on each table. It also reads the statement against Postgres's documented behavior, such as `CREATE INDEX` without `CONCURRENTLY` blocking writes, and says which of the two a comment is based on.

### Will it catch drift between schema.ts and the migration?

Yes. It runs `drizzle-kit generate` on the branch, and any SQL that command produces is drift: the schema says one thing and the committed migration another. The comment quotes the generated statements so the author can regenerate rather than hand-edit.

### Does this work with Prisma instead of Drizzle?

The same review works for Prisma migrations under `prisma/migrations/`. Change the prompt to apply them with `prisma migrate deploy` and to check drift with `prisma migrate diff`, and the rest of the review is the same.
