Resolving Drizzle Schema Drift Detection Errors

You run drizzle-kit generate expecting a migration and get No schema changes, nothing to migrate, even though the database visibly differs from your TypeScript schema. Or drizzle-kit check exits non-zero complaining about colliding snapshots after a branch merge. Or the build is green but production throws column "x" does not exist. These are all the same condition wearing different masks: drift between the three artifacts Drizzle keeps loosely coupled — your schema.ts file, the snapshot history under meta/, and the live database catalog. This page is for the engineer who lands here mid-deploy, needs to know which artifact moved, and needs the database and the schema reconciled without losing data.

The root almost always traces to how Drizzle detects changes. As covered in Drizzle ORM Type Sync, generate diffs your schema against the last snapshot, never the live database — so a push or a hand-applied hotfix moves the catalog out from under the snapshot, and every later command reasons from a stale baseline.

Symptom / Error Signatures

You are dealing with drift if you see any of these.

  • No schema changes, nothing to migrate from drizzle-kit generate when the schema and database clearly differ.
  • drizzle-kit check exits non-zero with a message about snapshot collisions, duplicate idx values in _journal, or non-sequential migration numbers.
  • A generated migration tries to ADD COLUMN something that already exists, producing ERROR: column "status_flag" of relation "users" already exists (PostgreSQL) or ERROR 1060 (42S21): Duplicate column name (MySQL) on apply.
  • Runtime column "x" does not exist (PostgreSQL 42703) or ERROR 1054 (42S22): Unknown column (MySQL) from queries that tsc accepted.
  • drizzle-kit migrate reports a migration as already applied, or refuses to apply because the __drizzle_migrations hash differs from the file on disk.

Root Cause Analysis

Drift has three distinct origins, and the fix differs for each, so identify which artifact moved before touching anything.

Origin What moved Telltale signal Reconcile toward
Stale snapshot push or manual DDL changed the database; snapshot did not generate says “no changes” but catalog differs Re-baseline snapshot from the live database
Divergent history Two branches generated from the same parent and merged drizzle-kit check collision; gaps in _journal Linearize the migration chain
Hash mismatch A committed migration file was edited after it ran __drizzle_migrations hash differs from file Restore the file or re-baseline the journal

The mechanism behind all three: Drizzle stores a JSON snapshot of the schema after each generate under drizzle/migrations/meta/, plus a _journal.json ordering them. generate compares your current schema.ts to the newest snapshot to compute the delta. It never introspects the database during generate. So whenever the database changes through any path other than applying a generated migration — a push, a psql hotfix, a restore from a divergent backup — the snapshot and the catalog disagree, and Drizzle’s diff is computed against a reality that no longer exists. On MySQL 8.0 this is more dangerous than on PostgreSQL: because DDL forces an implicit commit, a half-applied migration cannot roll back, so a failed apply can itself create drift, a hazard detailed in Transactional vs Non-Transactional Databases.

Immediate Mitigation

1. Establish ground truth — read the live catalog directly. Do not trust the types or the snapshot; ask the database what it holds.

-- PostgreSQL · read-only · safe on primary or replica at any time
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'users'
ORDER BY ordinal_position;

2. Compare ground truth against the schema file by introspecting into a scratch file. This shows exactly what Drizzle would write from the live database, which you diff against your committed schema.ts.

# Context: workstation; reads the live DB and writes a TS schema, applies nothing.
# Point introspect at a throwaway output so you can diff, not overwrite, your real schema.
drizzle-kit introspect --config=./drizzle.config.ts --out=./drizzle/_introspected
diff <(grep -v '^//' ./src/db/schema.ts) <(grep -v '^//' ./drizzle/_introspected/schema.ts)

3. If the snapshot is stale (the most common case), re-baseline it. Generate a fresh snapshot that matches the live database, so future diffs start from reality. Delete the divergent generated migration first if it was never applied.

# Context: workstation; regenerates the snapshot baseline. Commit the result.
# Removes an unapplied, drift-derived migration, then re-generates from current schema + live state.
rm ./drizzle/migrations/0007_drifted_migration.sql
drizzle-kit generate --schema=./src/db/schema.ts --out=./drizzle/migrations
drizzle-kit check --config=./drizzle.config.ts   # must exit 0 before you proceed

4. If the history diverged across branches, linearize it. Regenerate the later migration on top of the merged journal so the snapshot chain is sequential again, then re-run check.

Permanent Fix / Long-Term Pattern

Drift is preventable by removing the two paths that create it: push against shared environments, and hand-applied DDL. The durable fix is a required gate that fails any pull request whose committed migration history does not reproduce the schema file, so drift is caught before merge rather than at deploy.

# .github/workflows/drizzle-drift-gate.yml — required, blocking check
# Context: runs against a fresh throwaway database; a red result blocks the merge.
drizzle_drift_gate:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    # check the snapshot chain is internally consistent (no collisions, sequential journal)
    - run: npx drizzle-kit check --config=./drizzle.config.ts
    # apply every committed migration to a clean DB, then assert introspect shows zero diff
    - run: ./scripts/assert-migrations-reproduce-schema.sh

Pair the gate with a hard rule that production schema changes only ever arrive through a reviewed, committed generate migration applied by your runner — never push, never psql. That single discipline, part of the broader ORM & Framework Migration Workflows contract, eliminates the stale-snapshot origin entirely. For drift caused by a column rename being read as drop-plus-add, stage renames across two additive migrations following Expand and Contract Methodology.

Verification Checklist

  • drizzle-kit check exits 0 with no snapshot collisions or journal gaps.
  • schema.ts.
  • __drizzle_migrations (or _journal) hashes match the committed migration files exactly.
  • tsc --noEmit passes against the reconciled schema with no any casts masking missing columns.
  • drizzle-kit push.

Frequently Asked Questions

Why does drizzle-kit generate report no changes when my database is obviously different? Because generate diffs your schema file against the last stored snapshot, not against the live database. If the database moved through a push or a manual change without a corresponding generate, the snapshot is stale and the diff genuinely sees nothing new relative to it. Re-baseline by introspecting the live database into a fresh snapshot, then generate again.

How do I fix a drizzle-kit check snapshot collision after a branch merge? Two branches generated migrations from the same parent snapshot and both merged, leaving duplicate or non-sequential entries in the journal. Regenerate the later migration on top of the merged history so the snapshot chain is linear, delete the colliding snapshot, and re-run check until it exits zero before applying anything.

Can I just delete the migrations folder and start over to clear drift? Only if you also baseline the existing production schema as the new starting point — otherwise the next migration will try to create tables that already exist. Introspect the live database into a fresh schema and snapshot, mark that as applied in the migration history, and resume generating from there. Never drop the history without baselining, or the next apply will collide with the real catalog.