Schema Drift

Schema drift is the bug that survives code review.

Why drizzle-kit has no drift detector and Prisma 7’s migrate diff produces false positives.

It is the gap between what your Prisma or Drizzle schema says and what your live database actually has. ORMs miss it. Migrations silently paper over it. Drift Mode in QueryDeck catches it before it ships.

Real examples below. Real postmortems linked. About 7 min read.

01What schema drift actually is

Schema drift is when your application's idea of the database and the actual live database disagree. The idea lives in your schema file (schema.prisma, src/schema.ts), your migration history, and your ORM-generated types. The reality lives in the database. The two should be identical. They almost never are.

Drift is dangerous because it is silent. Code keeps compiling. Queries keep running. Tests keep passing. The first time you find out is when a migration silently rewrites one side to match the other in production, or when a query returns wrong data because the model thinks a column exists with one shape and the row has another.

The Prisma team has been talking about this on their issue tracker for years. Issue #19100 and Discussion #24886 are both about the limits of their drift detector. Drizzle does not have a drift detector at all as of mid 2026. There is a real industry pattern here, and it is not solved by the migration tools that caused it.

02Five places schema drift hides
Place 1

A DBA fixes prod, no one updates the schema file

The most classic drift. An on-call engineer runs ALTER TABLE in psql at 2am to unblock a deploy. The fix works. Nobody opens a PR to mirror the change in schema.prisma or src/schema.ts. The next migration the team writes, three weeks later, generates a diff that silently reverts the prod fix.

psql=# ALTER TABLE events ALTER COLUMN created_at TYPE timestamptz;
ALTER TABLE
# (schema.prisma still says: createdAt DateTime @default(now()))
Place 2

Two developers, two migration timelines

Alice writes a migration on her branch that adds a column. Bob writes a different migration on his branch that adds a different column. Both branches merge to main but the migrations apply in different order on different environments. Staging ends up with one column order, prod with another. Indexes that depend on column order get rebuilt differently.

// migrations/0042_alice.sql: ALTER TABLE users ADD COLUMN tier TEXT;
// migrations/0042_bob.sql:   ALTER TABLE users ADD COLUMN region TEXT;
// (same number, different content, different envs)
Place 3

ORM defaults that quietly differ from the DB

Prisma's DateTime maps to TIMESTAMP(3) by default. If your DB has TIMESTAMPTZ (which the PostgreSQL wiki recommends), there is drift. Your code reads UTC, the DB stores 'whatever the session timezone is'. Test data lines up because both write at midnight UTC. Real users in Tokyo break.

// schema.prisma
model Event {
  id        Int      @id
  createdAt DateTime @default(now())  // TIMESTAMP(3), not TIMESTAMPTZ
}
Place 4

An index added in prod for performance, never in the file

Slow query in prod, SRE adds CREATE INDEX CONCURRENTLY. Performance issue fixed. The index never makes it into the schema file. Six months later a refactor regenerates the migrations and the index disappears on the next deploy. The slow query comes back, but now it is your problem at 3am.

psql=# CREATE INDEX CONCURRENTLY idx_orders_user_status ON orders(user_id, status);
# (drift: schema file knows nothing about this index)
Place 5

Schema-name qualifiers and partial indexes the tools ignore

Prisma migrate diff treats some Postgres features as out of scope: WHERE clauses on partial indexes, multi-schema setups, custom collations. The tool reports 'in sync' when the schemas are not in sync, just on dimensions it does not look at. The team learns to trust 'in sync' even when it is wrong.

// Index in DB:    CREATE INDEX ... ON orders(user_id) WHERE status = 'active';
// Schema file:    @@index([userId])
// prisma migrate diff says: 'No differences'
03Canonical case: TIMESTAMP vs TIMESTAMPTZ

The cleanest drift incident to study is the one that shows up around Postgres timestamp types. Two letters of difference between TIMESTAMP and TIMESTAMPTZ. The first stores the wall clock with no timezone metadata. The second stores an instant and converts at read time. Mixing them in a Prisma project with a manual DBA fix is a five-step path to silent data corruption.

Day 0

schema.prisma defines `createdAt DateTime @default(now())`. Prisma generates TIMESTAMP(3) in PostgreSQL.

Day 30

Engineering notices timestamps display in the wrong timezone for non-UTC users. DBA runs `ALTER TABLE events ALTER COLUMN created_at TYPE timestamptz`. Symptom fixed.

Day 60

A new model is added. `npx prisma migrate dev --name add_orders`. Prisma reads the schema (still TIMESTAMP), reads the DB (TIMESTAMPTZ), notices drift, generates an ALTER to downgrade events.created_at back to TIMESTAMP.

Day 60 + 1 hour

Migration applied to staging. All historical timestamps reinterpreted as server local time. Now reading wrong. Tests pass because they write and read in the same session.

Day 60 + 1 day

Deploy to prod. Same migration runs. Historical events corrupted. 87% of timestamps are now wrong (see the Medium postmortem linked below). Recovery requires the last clean backup.

The PostgreSQL wiki lists TIMESTAMP without timezone in its “Don't Do This” page. There is a Medium postmortem documenting an incident where 87% of records were affected. The root cause every time is the same: drift between what schema.prisma said and what the live DB actually had, plus a migration that silently chose the wrong side.

04Why ORMs do not catch this on their own
Prisma

Drift detector exists, but false positives killed trust

Prisma migrate has a drift detector. It flags differences between the migration history and the live DB. The problem is its blind spots: partial index WHERE clauses, multi-schema setups, custom collations, PostGIS types. When the detector flags them as drift in cases where teams know it's fine, teams learn to ignore the warning. The detector becomes noise. Real drift slips through with the noise.

Drizzle

No drift detector at all

Drizzle Kit can generate migrations from schema diffs (schema file vs current introspected DB) but it does not have a continuous drift check. If your live DB drifts from your schema file between migrations (because of a manual fix, a different env, a half-applied migration), Drizzle Kit will only notice the next time you ask it to generate a new migration, at which point it will silently include the “fix” in your next ALTER.

05How Drift Mode in QueryDeck catches it

Drift Mode parses your ORM schema files directly (six ORMs at launch: Drizzle, Prisma, TypeORM, Django, ActiveRecord, Eloquent) and compares them against the live database column by column, index by index, constraint by constraint. Every divergence shows up in the panel with a side-by-side diff, a severity tag, and a one-click Fix action that either patches the schema file or generates an ALTER for the DB, whichever side you choose as truth.

Concept preview · ships with v1.0 launch on 2026-07-21

Drift Mode side-by-side: schema.prisma left, live PostgreSQL right, every drifted column highlighted with TIMESTAMP vs TIMESTAMPTZ called out in red.

ORMs parsed

6 at launch

Cases caught

8 categories

CI integration

qdeck CLI

06Sources and further reading
Related

Postgres MCP server: let Claude Code see your live schema

The same parser that powers Drift Mode also feeds the MCP server, so the model gets your real shape, not its guess at your shape.

See the setup →
Read next

Why most database MCP servers are wrong

The schema-aware tooling philosophy applied to LLM tool surfaces. Why one-tool run_sql is a footgun.

Read the essay →
07Frequently asked questions

What is schema drift in plain terms?

Schema drift is when your application's idea of the database (your Prisma or Drizzle schema file, your migration history, your ORM model definitions) and the actual live database disagree. The drift is usually invisible until a deploy or a migration silently rewrites one side to match the other and breaks production. It is one of the most common 'how did this even happen' bugs in modern app stacks.

How does schema drift differ from a migration conflict?

A migration conflict happens at apply time and the migration tool refuses to run. Drift is the opposite: nothing fails. The DB is in state A, the code thinks it is in state B, and queries run, sometimes returning wrong data, sometimes corrupting on write. Drift survives PR review because it is not in the diff. You only see it in the next incident.

Why do Prisma migrate and Drizzle Kit miss drift?

Prisma migrate has a drift detector but it produces false positives on edge cases like partial indexes, custom collations, and PostGIS types, so teams often ignore its warnings. Drizzle Kit does not have a built-in drift detector at all as of mid 2026. Both tools are migration-first: they assume the schema file is the source of truth and rewrite the DB to match, which makes drift dangerous (the rewrite is silent) rather than visible.

What kinds of drift does QueryDeck Drift Mode catch?

Today: missing or extra columns, column type drift (including TIMESTAMP vs TIMESTAMPTZ), nullable mismatches, default value drift, missing or extra simple indexes, missing or extra foreign keys, enum type drift. Six ORMs supported (Drizzle, Prisma, TypeORM, Django, ActiveRecord, Eloquent). Not yet: functional indexes, check constraints, triggers, generated columns, custom types like PostGIS. The CLI version covers a wider tool surface than Drift Mode.

Does Drift Mode run in CI or only locally?

Both. The qdeck CLI ships with the QueryDeck app and can run in CI (GitHub Actions, GitLab CI, anywhere bash runs) to fail a PR on drift. The Drift Mode panel in the app gives you the same checks interactively while you work. Most teams run both: CI as the gate, app as the daily catch.

What databases is Drift Mode available on?

PostgreSQL, MySQL, and SQLite at launch. MongoDB and Redis do not have a traditional schema, so Drift Mode does not apply (QueryDeck still gives you collection and key introspection for those). All five engines work with the rest of QueryDeck: the SQL editor, MCP server, ERD generator.

Catch schema drift before it ships.

The database client that knows your project. $79 one-time. All your Macs.

Pre-launch offerLifetime ($149 value) for $79. Removed at launch.