Skip to content

drizzle-kit generate or drizzle-kit push, and when each is safe

push diffs your schema straight onto the database with no file to review; generate writes SQL you commit. Use push only on a database you can throw away.

Drizzle ORM4 min readships at docs/solutions/drizzle/generate-vs-push.md

Tags: drizzle · migrations · postgres · schema · deployment

Drizzle offers two ways to get your schema (src/db/tables.ts, re-exported from src/db/schema.ts) into Postgres, and the docs present them side by side without saying loudly enough that one of them will delete your data.

bun run db:push       # diff the schema against the live database and apply it
bun run db:generate   # write a .sql migration file
bun run db:migrate    # apply pending migration files

What push does

drizzle-kit push introspects the live database, diffs it against your schema file, and executes whatever statements it thinks will close the gap. There is no file. There is no record. There is no review step.

That last point is the one that matters, because Drizzle (like every schema-diffing tool) cannot see a rename. You change this:

export const users = pgTable("users", {
  id: uuid("id").primaryKey().defaultRandom(),
  name: text("name").notNull(),          // was: text("full_name")
});

and the diff it computes is "there is a column full_name that should not exist, and a column name that does not exist yet". So it drops full_name (with every value in it) and adds an empty name. Interactively it will ask; with --force, in a script, or when the prompt scrolls past, it will not.

The second problem is invisibility. A push happened on one machine, against one database. Nothing in git changed. A colleague pulls your schema file, runs the app, and gets errors about a column that exists only on your laptop. Production never gets the change at all, because there is nothing for a deploy to apply.

What generate does

drizzle-kit generate diffs your schema against the migration history in drizzle/ and writes a new SQL file plus a snapshot in drizzle/meta/.

-- drizzle/0003_slim_wasp.sql
ALTER TABLE "users" ADD COLUMN "name" text NOT NULL;--> statement-breakpoint
DROP COLUMN "full_name";

Now the data loss is visible, in a diff, before it happens, and you can fix it by hand before the first apply:

ALTER TABLE "users" RENAME COLUMN "full_name" TO "name";

bun run db:migrate applies pending files and records each one's hash in drizzle.__drizzle_migrations, so every environment converges on the same schema by replaying the same files in the same order.

The rule

Use push only on a database whose entire contents you are willing to lose.

That is: your local Docker Postgres, or a Neon/Supabase branch created for this feature. Nothing else. Not staging, not a shared dev database, not "production but it's only a new table".

Use generate + migrate everywhere else, including on your own machine once you are past the shape-it-out phase.

The workflow that uses both honestly

Push is genuinely useful for the first hour of a new model, when you are changing schema.ts every few minutes and do not want twelve migration files describing your indecision.

# 1. Explore. Local or a throwaway branch only.
bun run db:push
# ...edit schema.ts, push again, repeat...

# 2. Settle. Then throw the scratch database away and do it properly:
bun run db:reset          # or: drop and recreate the local database
bun run db:generate       # one clean migration for the final shape
bun run db:migrate
git add src/db/tables.ts drizzle/

The reset in step 2 is the part people skip, and skipping it is what produces the "drift" errors later: your local database has a history of pushes, the migration files describe a different path, and generate starts emitting statements that make no sense.

Guardrails worth having

This project blocks drizzle-kit push from the agent by default: the guard-neon-sql hook denies it unless the command is prefixed with ALLOW_DB_PUSH=1. That is not paranoia about the tool; it is that an agent running push against whatever DATABASE_URL happens to be exported is a category of accident with no undo.

Two more habits:

  • Never put push in a build command, a deploy script, or a postinstall. A deploy that "syncs the schema" is a deploy that can drop a column to match a branch someone force-pushed.
  • Keep strict: true and verbose: true in drizzle.config.ts (they already are) so push at least prints the statements and asks.

Reading the generated SQL is not optional

Even with generate, the file is only as safe as the person who read it. Look for exactly four things:

  1. DROP COLUMN / DROP TABLE: real data, gone.
  2. A drop-plus-add pair on the same table: that is a rename in disguise.
  3. SET NOT NULL on a column with no default, on a table that has rows: it will fail on any populated database, and it will fail in production, not locally.
  4. CREATE UNIQUE INDEX on a column that may already contain duplicates: same story.

Each of those has a hand-edit that makes it safe, and the window for hand-editing is before the first apply anywhere. After that, fix forward with a new migration: editing an applied file makes the file disagree with its recorded hash, and environments that already ran it will never pick up your change.

Summary

pushgenerate + migrate
Leaves a file to reviewnoyes
Survives in gitnoyes
Can rename without data lossnoyes, by hand-editing before apply
Safe on productionneveryes
Good forthe first hour of a new modeleverything else