Skip to content

drizzle-kit migrate runs full commutativity check on every invocation — ~115s on a 70-migration repo, even when no migrations are pending #5777

@JeremyCraven

Description

@JeremyCraven

Summary

Since drizzle-kit@1.0.0-beta.18 introduced commutative-migrations checking (per discussion #5005), the migrate command calls checkHandler unconditionally before applying any migrations. On a project with ~70 migrations and ~28 MB of snapshot.json files, this adds ~115 seconds of single-threaded CPU work to every drizzle-kit migrate invocation — including no-op runs where the DB is already up to date.

The --ignore-conflicts flag suppresses the report but does not skip the analysis, so it provides no perf relief.

Verified affected versions (this is not fixed in the latest published release):

  • 1.0.0-beta.22 npm tarball: ~115 s
  • 1.0.0-rc.2 npm tarball: ~117 s
  • 1.0.0-rc.3 source (beta branch HEAD): same code path present — see drizzle-kit/src/cli/schema.ts:147await checkHandler(out, dialect, ignoreConflicts);

Repro

A repo with N migrations where each snapshot.json is ~500 KB (representative of a mature Postgres schema: ~120 tables, indexes, FKs, RLS policies, etc.). At N = 71, total snapshot footprint ≈ 28 MB.

$ time pnpm drizzle-kit migrate --config drizzle.config.ts
Reading config file 'drizzle.config.ts'
Using 'pg' driver for database querying
[✓] migrations applied successfully!
pnpm drizzle-kit migrate ...   115.01s user 0.90s system 101% cpu 1:54.55 total

DB is already migrated; zero migrations applied. All ~115 s is the pre-flight commutativity check. Cost scales roughly linear-in-snapshot-bytes plus a multiplier per branching point in the migration DAG; adding a migration today costs ~+1.5 s on subsequent runs.

Root cause (verified on beta branch HEAD = rc.3 commit)

  1. drizzle-kit/src/cli/schema.ts:147 — the migrate command's handler calls await checkHandler(out, dialect, ignoreConflicts); before any DB connection is opened.

  2. drizzle-kit/src/cli/commands/check.tscheckHandler:

    • Reads + JSON.parses every snapshot.json (pass 1, validation).
    • Runs Zod .strict() over every snapshot — by far the most expensive line item; the schemas are deeply nested (schemaInternal, tables, columns, indexes, fks, checks, policies, …) and .strict() walks every field.
    • Calls commutativity.detectNonCommutative(snapshots)buildSnapshotGraph(snapshots) which reads + parses every snapshot again (pass 2) to build the DAG. Net: every snapshot is JSON-parsed twice and Zod-validated once per invocation.
    • For each parent with multiple children, runs full diffSnapshots between parent and each leaf, then pairwise across leaves. Linear chains skip this, but a repo that has gone through rebases/rechains has branching points that pay this per-pair cost.
  3. --ignore-conflicts (flag declared in migrate and check commands) only short-circuits the report + process.exit in checkHandler after detectNonCommutative has already returned. The expensive work is unconditional.

Why this matters

  • migrate is the most frequently invoked drizzle-kit command in a normal dev loop (every time you switch branches, reset your DB, run integration tests against a fresh schema, etc.). Adding ~2 minutes to a command that used to take under a second is a significant regression for local DX.
  • By the time migrate runs, the migration chain has already been merged. Commutativity warnings at apply-time are advisory at best — the conflicting migrations are already in the tree and will be applied in whatever order the journal records. The check belongs in generate (where it can prevent the problem) and in the dedicated drizzle-kit check command (where users opt into it), not as an unconditional pre-flight on migrate.

Suggested fixes

In rough preference order:

  1. Remove checkHandler from migrate's handler entirely. Keep it on generate (where it catches the issue at the right time) and on the dedicated drizzle-kit check command. CI can run drizzle-kit check as a separate step.
  2. If you want to keep a safety net on migrate, gate it behind an opt-in flag (--check / --strict) rather than running by default.
  3. At minimum, make --ignore-conflicts actually short-circuit the analysisif (ignoreConflicts) return emptyResult(); before detectNonCommutative — so users have an escape hatch today without pinning back to beta.17. The flag's name already implies this is what it does.
  4. Independently of the gating decision, the snapshot-load path could be substantially faster: deduplicate the two read+parse passes, and skip Zod .strict() for snapshots written by drizzle-kit itself (writer-trusted). Issues Feature: delta-encoded snapshot.json to eliminate redundant storage #5635 and [FEATURE]: Minify Snapshot Files #5133 are also touching the snapshot-bloat problem from the storage side.

Happy to send a PR for (1) or (3) if there's interest.

Side note: v1.0.0-rc.2 git tag

While investigating this we noticed the v1.0.0-rc.2 git tag points at commit 48e54060… ("Netlify-DB for main (#5663)"), which is on the main branch (the legacy 0.31.x line), not on beta. That commit predates the commutativity feature entirely, so source-code searches against the tag read like "feature fixed in rc.2" when in reality the npm tarball published under that version is built from a different (1.0-line) commit and does have the feature. Not a perf issue; just a heads-up that it makes "what shipped in rc.2?" hard to answer from source. v1.0.0-rc.3 is on beta and looks correct.

Environment

  • drizzle-kit@1.0.0-rc.2 (also verified beta.22 and inspected rc.3 source)
  • drizzle-orm@1.0.0-rc.2
  • Node 24, macOS, Postgres 16
  • 71 migration files, ~28 MB total snapshot footprint
  • Migration chain has a handful of branching points from cross-branch rebases

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions