I scanned 1,066 Prisma migrations. Here are the risky patterns that kept showing
up
I wanted to understand what risky database migrations look like in real-world
Prisma projects, not toy examples.
So I ran an open-source migration scanner across the full migration history of
three public Postgres + Prisma repositories:
- documenso
- trigger.dev
- formbricks
In total:
- 1,066 migration files
- 2,339 findings
- 21 rule types
- Postgres AST parsing, not regex
Important note: these findings do not mean the projects are broken. Many are
historical migrations, intentional refactors, or safe in context. The goal was
to identify recurring risk patterns that deserve review before merge.
## The most common risky patterns
Two patterns dominated the dataset.
### 1. Foreign keys added without NOT VALID
Postgres validates a new foreign key against existing rows when the constraint
is added.
On larger tables, that can create lock and rollout risk.
A safer pattern is often:
sql
ALTER TABLE orders
ADD CONSTRAINT orders_user_id_fkey
FOREIGN KEY (user_id)
REFERENCES users(id)
NOT VALID;
ALTER TABLE orders
VALIDATE CONSTRAINT orders_user_id_fkey;
This splits adding the constraint from validating historical data.
### 2. Indexes created without CONCURRENTLY
This one showed up constantly.
CREATE INDEX orders_user_id_idx ON orders(user_id);
For production tables, the safer version is usually:
CREATE INDEX CONCURRENTLY orders_user_id_idx ON orders(user_id);
The catch: CREATE INDEX CONCURRENTLY has transaction caveats, so migration tools
and ORMs do not always emit it automatically.
## Destructive changes were also common
The dataset also included many examples of:
- DROP COLUMN
- DROP TABLE
- RENAME COLUMN
- ALTER COLUMN TYPE
- DROP CONSTRAINT
- TRUNCATE
- UPDATE without WHERE
Again, these are not automatically bugs. Sometimes they are correct.
The dangerous part is deploy order.
For example:
ALTER TABLE users DROP COLUMN full_name;
This can be safe only if every deployed app version, background job, API
serializer, and raw SQL query has already stopped reading users.full_name.
That is where SQL-only linting stops being enough.
## Why SQL-only linting is not enough
A SQL linter can tell you:
> This migration drops a column.
But the question reviewers really need answered is:
> What application code will break if this merges?
For modern apps, the column name in SQL may not even match the field name in
code.
Example with Prisma:
model User {
fullName String @map("full_name")
}
The migration drops:
ALTER TABLE users DROP COLUMN full_name;
But the app reads:
user.fullName
A plain grep for full_name misses that. A schema-aware scanner needs to
understand the ORM mapping.
## The pattern I care about most
The highest-signal finding is not simply:
> This migration is risky.
It is:
> This migration drops users.full_name, and these exact TypeScript files still
> read it through fullName.
That is the difference between a generic warning and an actionable PR review.
## What I built
I built MergeBrake as an open-source GitHub Action/CLI to test this idea.
It scans Postgres migrations, maps Prisma/Drizzle schema symbols to application
code, and comments on the pull request with:
- the risky migration
- the app-code references it may break
- a SAFE / EXPAND_CONTRACT / BLOCK verdict
- a suggested rollout plan
It is not a migration executor and it never touches the database. It is a pre-
merge review guard.
The repo and case studies are here:
https://github.com/mergebrake/mergebrake
The dogfood results are here:
https://github.com/mergebrake/mergebrake/tree/main/examples/dogfood
I would be curious to hear from Prisma/Postgres teams: do you already review
migration deploy order manually, or do you rely on CI checks for this?
Top comments (0)