Schema Drift

Schema drift is the bug that survives code review.

What drizzle-kit push, drizzle-kit check, and Prisma Migrate actually detect, and what they do not.

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 important distinction is not “tool A detects drift, tool B does not.” Drizzle Kit push already introspects the live database and computes changes from your Drizzle schema; push --explain can show the planned SQL without applying it. But drizzle-kit check checks migration-history consistency, not live database drift. Prisma migrate dev detects drift in development with a shadow database, while Prisma's production migrate deploy explicitly does not look for drift. The gap depends on which workflow you actually run.

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

A database feature exists, but the ORM models it differently

Partial indexes are a concrete example. A Prisma issue filed against 7.4.0 documents manually-created partial indexes being detected as drift and a later migrate dev generating DROP INDEX because schema.prisma cannot represent the same declaration. The drift is real from Prisma's model point of view, even when the index is intentional in the database.

// migration.sql
CREATE INDEX active_orders_idx ON orders(user_id) WHERE status = 'active';

// later Prisma 7.4.0 migrate dev may propose:
DROP INDEX "active_orders_idx";
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

Development drift detection is real; production deploy is different

Prisma migrate dev replays migration history in a shadow database and compares that end state with the development database. That is a genuine drift detector. But Prisma documents migrate deploy differently: it applies pending migrations in production and does not look for database drift. There are also real edge cases in the issue tracker, including Prisma 7.4.0 treating intentionally-created partial indexes as drift and proposing DROP INDEX.

Drizzle

Live diff exists in push, but it is tied to a schema-change workflow

Drizzle Kit push reads the Drizzle schema, introspects the live database, computes the difference, and normally applies the resulting SQL. The newer --explain option gives you a dry-run preview. That is useful, but it is not the same workflow as opening a dedicated read-only drift view during normal database work. Meanwhile drizzle-kit check validates migration-history consistency and branch collisions, not the current live database.

05How Drift Mode in QueryDeck catches it

Drift Mode parses your ORM schema files directly (five ORMs at launch: Drizzle, Prisma, TypeORM, Django, ActiveRecord) 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.

Product preview

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

5 today

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?

They cover different parts of the problem. Drizzle Kit push introspects the live database and computes code-vs-database changes, and --explain can preview those changes without applying them. drizzle-kit check checks migration-history consistency, not the live database. Prisma migrate dev detects development drift with a shadow database, while Prisma explicitly documents that migrate deploy does not look for production drift. QueryDeck Drift Mode is a read-only ORM-vs-live-database view you can open while working, instead of making migration application the moment you discover the mismatch.

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. Five ORMs supported (Drizzle, Prisma, TypeORM, Django, ActiveRecord). 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?

Drift Mode is documented today as an interactive QueryDeck feature. When qdeck opens a recognized project, QueryDeck parses the ORM schema, introspects the connected database, and opens Drift Mode when it finds differences. A dedicated headless CI drift command is not documented today, so this page does not claim one.

What databases is Drift Mode available on?

PostgreSQL and MySQL have full introspectable-schema support. SQLite is supported with reduced fidelity. MongoDB does not use the same relational schema model, so Drift Mode does not apply there in the same way. All four databases still work with the rest of QueryDeck.

Catch schema drift before it ships.

Launch offer: Lifetime at $79 for the first 10 customers, then $149. Try it free for 14 days, no card required.

Launch offerLifetime at $79 for the first 10 customers, then $149.Try free for 14 days