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.

How a push leaves the snapshot stale Two tracks over time: the snapshot history advances only when generate runs, while the live database jumps ahead on a push. The next generate diffs the schema against the stale snapshot, so it emits SQL that assumes the pre-push state. A push leaves the snapshot behind the database snapshot history snap v1 snap v1 still stale live database db v1 db v2 ahead push next generate diffs schema vs snap v1 Emitted SQL assumes db v1 — the wrong starting state.
Because generate diffs the schema against the stored snapshot, a push that moves the database forward leaves the snapshot stale, so the next generate produces SQL for a state the database has already left.

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.

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.

Three-way agreement: schema, snapshot, catalog Three sources form a triangle — the TypeScript schema that types are inferred from, the migration snapshot validated by drizzle-kit check, and the live catalog read from information_schema. Each edge is a check that must pass; any mismatched pair is drift. All three must agree, or it is drift schema.ts what types believe migration snapshot meta/ history live catalog information_schema generate diff catalog query must match drizzle-kit check Any one edge that fails to match is the drift you must reconcile.
The schema file, the validated migration snapshot, and the live catalog form a triangle of checks; drizzle-kit check guards the snapshot, an information_schema query proves the catalog, and any mismatched pair is drift.

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.

Keeping Drizzle’s Three Artifacts Aligned

Drizzle’s model is that your TypeScript schema is the source of truth, drizzle-kit generates SQL migrations by diffing that schema against the recorded migration state, and the live database is what those migrations produce. Type sync fails whenever those three fall out of step, and each divergence has a distinct symptom. If you edit schema.ts and forget to run drizzle-kit generate, your application’s inferred types describe a shape the database does not have, so a query compiles against a column that will not exist at runtime — a green build and a red production. If you run generate but hand-edit the emitted SQL without regenerating, the migration journal’s snapshot no longer matches your schema, and the next generate produces a confused diff on top of a state it does not recognize. If someone changes the database directly, both the schema and the journal are now fiction, and drizzle-kit’s next diff either tries to re-create an object that exists or misses one it cannot see.

The discipline that keeps the three aligned is to make the schema the only place changes originate and to verify the alignment mechanically. Generate migrations from schema edits, never hand-write structural changes into the database; commit the generated SQL and the journal together with the schema change in the same pull request, so history and source move as one unit; and run a drift check in CI — generate against a fresh database built from the committed migrations and fail if the diff is non-empty, which proves the migrations reproduce the schema exactly. Connection-pool configuration is the other recurring Drizzle footgun, separate from type sync but often blamed on it: a driver pool sized without regard for the database’s connection ceiling produces timeouts under load that look like schema errors but are exhaustion. Keeping the schema authoritative, the migrations regenerated, and the pool sized to the database’s real limit is what turns Drizzle’s type inference from a source of runtime surprises into the compile-time guarantee it is meant to be.

The single most valuable guard is a CI job that builds a throwaway database from the committed migrations, runs drizzle-kit generate against it, and fails if the resulting diff is non-empty. A clean diff proves the three artifacts agree: the migrations reproduce exactly the schema your types are inferred from, so a query that compiles is a query the database can serve. A non-empty diff means someone edited the schema without regenerating, hand-edited the SQL, or changed the database out of band — and you have caught it at build time instead of at runtime. Pair that with the rule that structural changes only ever originate in schema.ts, and Drizzle’s type inference becomes the compile-time contract it is designed to be rather than a promise that quietly breaks whenever the artifacts drift.

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.