Skip to content

Build1 publisher3 min readPublished

Drizzle-kit drafted a DROP TABLE for a live table after a developer's three schema copies drifted apart

Drizzle-kit generated a DROP TABLE ... CASCADE for a table still in production when a developer asked it for two new tables. The developer traces it to schema.ts, drizzle-kit's snapshot and the live database each describing a different schema.

The Engineer · Build desk

Illustration accompanying Drizzle-kit drafted a DROP TABLE for a live table after a developer's three schema copies drifted apart

What happened

  • The day after the end-of-July baseline, the developer deleted the waitlist_entries definition from schema.ts but left the empty, unreferenced table in production.
  • In the same commit, suppressed_count went to production through a one-off ADD COLUMN IF NOT EXISTS script that never touched drizzle-kit generate.
  • The developer applies production changes by hand inside a transaction after reading the SQL, and uses neither drizzle-kit push nor drizzle-kit migrate.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Every DDL statement run outside drizzle-kit generate leaves the snapshot behind the database, and the next unrelated migration will try to replay or reverse it.
  • exposure Had generated files gone to production unread through push or migrate, the CASCADE drop would have executed; the developer credits the hand-apply rule for a cleanup instead of a restore.
  • decision A project that mixes idempotent hand scripts with generated migrations has to choose one path for production DDL, or check each generated file against the live catalog before applying it.

The app runs Drizzle ORM on Postgres hosted by Neon. The schema lives in lib/db/schema.ts, and drizzle-kit writes a JSON snapshot next to each generated migration [6]. The two extra statements match the differences between two of those files [1]. The latest snapshot, 0000_snapshot.json, still held waitlist_entries and had no suppressed_count. schema.ts had the opposite. Production had both the table and the column [15]. A diff of snapshot against schema.ts gives a DROP for the first and an ADD for the second. The generated file contained exactly that pair [19].

The developer blames the process. "drizzle-kit did nothing wrong. It did exactly what it is designed to do," the developer wrote [4]. The snapshot changes only when generate runs, and the app reads only schema.ts [17]. The end-of-July baseline was followed the next day by one commit with two schema changes, and neither went through drizzle-kit generate [11][12]. "Nothing complained, because nothing compares those three," the developer wrote [16].

The column is the half that looked safe, and by every check the script ran, it was. The one-off script ran ALTER TABLE reviews ADD COLUMN IF NOT EXISTS suppressed_count integer DEFAULT 0, then checked information_schema [14]. It worked. The generated version of the same change would have failed with "column already exists". Depending on how the file was run, that would have aborted the migration or left it half-applied [3]. "That habit is half of the bug," the developer wrote about skipping the generator for small additive changes [9][10].

The drop is the dangerous half. DROP TABLE ... CASCADE removes the table and anything that depends on it [2]. According to the developer, waitlist_entries was empty and unreferenced by then [13]. This instance would have cost little data. The diff never reads production, though, so a table with rows in it would have produced the same statement [2]. Deleting a pgTable definition without running generate leaves a DROP waiting for the next generate run, inside whatever feature that run is for [2]. Here it was a referral feature adding referral_codes and referrals [18].

A rule caught it. The developer does not use drizzle-kit push, and the migration file headers say not to use drizzle-kit migrate either. Production changes go in by hand, inside a transaction, after the SQL has been read [7]. "That rule is the only reason this story ends with a cleanup instead of a restore," the developer wrote [8].

In my view, the check to add is cheap and already half-written. Before applying a generated file, compare every DROP and ADD in it against production's catalog. A DROP for a table the current feature never touched, or an ADD for a column that already exists, means the snapshot is behind the database [2]. The project already has the query, because the one-off script read information_schema after its change [14].

What to watch

  • Whether the pre-apply checks the developer now runs compare generated SQL with production's catalog, or only with schema.ts and the snapshot.
  • Whether waitlist_entries is eventually removed through its own reviewed migration, bringing schema.ts, the snapshot and production back into agreement.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories