Drizzle ORM Type Sync

Drizzle’s defining feature is that your schema lives in TypeScript and the types your application compiles against are inferred directly from it — no codegen step, no separate client. InferSelectModel<typeof users> is the shape the compiler enforces, and it is derived from the same pgTable definition that drizzle-kit turns into migration SQL. The power and the hazard are the same fact: there is no machine forcing the inferred types and the live database to agree. The TypeScript schema is what your code believes; the database catalog is what is true. When they diverge, tsc stays green and the gap surfaces as a runtime column "x" does not exist on whichever replica got the read.

This section is for engineers running Drizzle against PostgreSQL or MySQL who need to deploy schema changes without a maintenance window. It sits inside the broader ORM & Framework Migration Workflows discipline and assumes the additive, forward-only contract from Expand and Contract Methodology. The two operations that govern drift are drizzle-kit generate, which emits reviewable SQL from your schema, and drizzle-kit push, which reconciles the database to your schema directly — and choosing wrongly between them is where most Drizzle production incidents begin.

generate versus push The TypeScript schema feeds two paths: generate emits SQL that is reviewed, committed, and applied through a runner; push mutates the database directly, leaving no artifact and a drift gap against the migration history. generate (review) vs push (direct) schema.ts inferred types generate → SQL reviewed, committed runner applies push → database no artifact drift gap vs history Use generate in production; reserve push for the local dev loop.
generate keeps a reviewable, replayable artifact and a clean history; push mutates the database directly and opens the drift gap that later breaks deploys.

Concept & Mechanism

Drizzle does not run a code generator. The pgTable (or mysqlTable) object you write is the type source: InferSelectModel and InferInsertModel read its column definitions at compile time, so the row type your queries return is whatever your schema file says — never what the database actually holds. There is no introspection at build time and no runtime validation of the catalog. This is fast and ergonomic, but it means the database is an unchecked external dependency from the compiler’s point of view.

drizzle-kit generate closes the loop by diffing your schema file against the snapshot of the previous generated migration (stored under meta/ in the migrations folder) and emitting the SQL delta. It does not connect to your database to do this; it compares schema-to-snapshot. drizzle-kit push, by contrast, connects to the database, introspects its real catalog, diffs that against your schema, and applies the difference immediately — no SQL file, no history entry. drizzle-kit check validates that the generated migration snapshots are internally consistent and free of collisions, and drizzle-kit introspect reverses the flow, reading a live database and writing a TypeScript schema from it.

The drift surface follows directly. Because generate diffs against a snapshot rather than the live database, a push to production — or any hand-applied change — leaves the database ahead of the snapshot history, and the next generate produces SQL that assumes the old state. On PostgreSQL, DDL is transactional (except CREATE INDEX CONCURRENTLY), so a failed multi-statement migration rolls back cleanly; on MySQL 8.0, DDL forces an implicit commit, so a half-applied migration leaves the database in a state no longer matching any snapshot. That engine difference, detailed in Transactional vs Non-Transactional Databases, changes how recoverable your drift is.

Prerequisites & Decision Criteria

Before applying any Drizzle migration to a shared environment, confirm the following. The first three are about which command to run; the rest are about safety.

  • drizzle.config.ts declares the correct dialect, schema path, and migrations out directory, identical across environments.
  • generate (not push) for anything beyond a local database you can throw away.
  • DROP COLUMN or ALTER COLUMN ... TYPE.
  • Backfill Optimization, rather than the migration step.

The decision rule is short: push is for the local dev loop only. Every environment that another person or service depends on gets a reviewed, committed, generate-d migration applied through your runner.

Step-by-Step Procedure

1. Edit the schema and generate the migration. Make the schema change additive first, then emit the SQL delta.

# Context: local workstation; produces a reviewable SQL file, applies nothing.
# Drizzle reads ./src/db/schema.ts and writes a numbered migration under ./drizzle/migrations.
drizzle-kit generate --schema=./src/db/schema.ts --out=./drizzle/migrations

Verify before proceeding: a new NNNN_*.sql file exists and a snapshot was written under meta/.

2. Review the generated SQL for safety. Read the file; never apply it unseen.

# Context: read-only inspection on the workstation; fails loudly on destructive DDL.
grep -iE 'DROP COLUMN|DROP TABLE|ALTER COLUMN .* (TYPE|SET NOT NULL)' \
  ./drizzle/migrations/*.sql && echo "REVIEW: destructive or rewriting DDL present"

A rename will show as a DROP plus an ADD — if you see that pair, rewrite the migration by hand to preserve the column. Verify before proceeding: the SQL is additive and any index uses CONCURRENTLY.

3. Confirm types compile against the new schema. The inferred types only matter if the build agrees with them.

# Context: CI or workstation; zero exit means the inferred types resolve against the new schema.
tsc --noEmit --project tsconfig.build.json

4. Apply through your runner, not push. Run the committed SQL as a discrete, forward-only deploy step.

-- PostgreSQL · run as the migration role · CREATE INDEX CONCURRENTLY must run OUTSIDE a transaction
SET lock_timeout = '3s';
ALTER TABLE users ADD COLUMN IF NOT EXISTS status_flag VARCHAR(32);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_status ON users (status_flag);

Verify before proceeding: the step exits cleanly and the column is visible in the catalog before the new application image rolls.

Verification & Observability

After applying, prove the three artifacts agree. First, confirm the migration history is self-consistent.

# Context: CI or workstation; validates snapshot integrity, no database connection required.
drizzle-kit check --config=./drizzle.config.ts   # non-zero on snapshot collisions or corruption

Then confirm the live catalog actually carries the change, querying the database directly rather than trusting the types.

-- PostgreSQL · read-only · safe to run any time against primary or replica
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'status_flag';

Watch for sessions stuck mid-migration, which on MySQL can leave DDL half-applied and the catalog out of sync with every snapshot.

-- PostgreSQL · read-only · detect a migration session holding locks or idle in transaction
SELECT pid, state, wait_event_type, query
FROM pg_stat_activity
WHERE state <> 'idle' AND query ILIKE '%alter table%';

If drizzle-kit check reports an inconsistency or the catalog query returns nothing you expected, you are in drift — the schema drift detection guide walks through reconciling it.

Rollback Path

Reversal is forward-compatible: restore the previous application image and stop writing the new column rather than dropping it. Because the deploy was additive, the old code already runs against the expanded schema.

-- PostgreSQL · run as the migration role · safe at any time; non-destructive
-- Stop populating the new column; routing reverts via the application image, not a DROP.
ALTER TABLE users ALTER COLUMN status_flag DROP DEFAULT;

After rolling the application back, regenerate types from the live schema so the build matches reality again.

# Context: workstation; reads the live (rolled-back) database and rewrites the TS schema.
drizzle-kit introspect --config=./drizzle.config.ts
tsc --noEmit   # confirm inferred types compile against the restored schema

A destructive DROP COLUMN is only safe once the backfill is provably complete, no running version reads the column, and a verified point-in-time backup exists — schedule it as a reviewed contract step, never as an automatic rollback.

Common Errors & Fixes

drizzle-kit check reports snapshot collisions or non-sequential migrations. Root cause: two branches generated migrations from the same parent snapshot and both merged. Fix: regenerate the conflicting migration on top of the merged history so the snapshot chain is linear again.

Generated SQL contains a DROP COLUMN you did not intend. Root cause: a column rename, which the diff engine reads as delete-plus-create because it has no rename signal. Fix: hand-edit the migration to ALTER TABLE ... RENAME COLUMN and update the snapshot, or stage the rename across two additive migrations.

Runtime column "x" does not exist while tsc passes. Root cause: types inferred from a schema file ahead of the live database, typically after a push to one environment but not another. Fix: drizzle-kit introspect the live database, reconcile the schema file, and regenerate.

No schema changes, nothing to migrate when you expect a migration. Root cause: generate diffs against the last snapshot, and a prior push already moved the database, so the snapshot is stale. Fix: re-baseline by introspecting the live database into a fresh snapshot before generating.

Child Page Index

This section’s deep-dive guides cover the two failure surfaces that most often page a Drizzle engineer at deploy time. Start with resolving Drizzle schema drift detection errors when drizzle-kit check or generate reports the database out of sync with your TypeScript schema, generated migrations no longer match the catalog, or a push has left the snapshot history stale. Then read fixing Drizzle connection pool configuration errors when postgres-js, node-postgres, or a serverless driver throws sorry, too many clients already, exhausts the pool during a migration, or breaks prepared statements under a PgBouncer transaction-mode pooler. For the companion framework, compare with Prisma Migration Strategies.

Frequently Asked Questions

When is drizzle-kit push actually safe to use? Only against a database you can recreate from scratch — your local development instance. push applies changes with no SQL artifact and no history entry, so it cannot be reviewed, replayed, or rolled back, and using it on a shared environment is the most common way Drizzle databases drift from their migration history. For anything anyone else depends on, use generate and apply the committed SQL through your runner.

Why does drizzle-kit generate say there are no changes when the database is clearly different? generate diffs your schema file against the last snapshot it stored, not against the live database. If a push or a manual change moved the database without a corresponding generate, the snapshot is stale and the diff sees nothing new. Re-baseline by introspecting the live database into a fresh snapshot, then generate.

Does Drizzle wrap migrations in a transaction so a failure rolls back? It depends on the engine, not Drizzle. On PostgreSQL, DDL is transactional and a failed multi-statement migration rolls back — except CREATE INDEX CONCURRENTLY, which must run outside a transaction. On MySQL 8.0, DDL forces an implicit commit, so a partially applied migration stays applied and you must make each step independently re-runnable.