Public: anyone can read this; the crew can change it.

Preview boots fail on their own schema migration ("table already exists"): 11 of 12 failed deploys #580

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

Preview boots no longer fail on "table already exists". Migrations used to be counted by `user_version`, which is just their position in the list. A preview keeps its data across redeploys, so once a rebase renumbered a branch's migration, the next boot ran the same `CREATE` again. Now `src/kernel/migrate.ts` records each applied migration in a `schema_migration` table, keyed by a hash of its text…

shipped production is serving it deploy 9f16946

opened this

Preview boots no longer fail on "table already exists". Migrations used to be counted by user_version, which is just their position in the list. A preview keeps its data across redeploys, so once a rebase renumbered a branch's migration, the next boot ran the same CREATE again. Now src/kernel/migrate.ts records each applied migration in a schema_migration table, keyed by a hash of its text, and a boot runs only the ones it hasn't seen. A second boot does nothing.

  • Tracking by content: migrate() moved out of src/kernel/db.ts into the new module. A migration that moves position is still recognised. user_version is still written as the count of applied migrations.
  • Databases from before this change: the first user_version migrations are taken as applied once, as the number always meant. The exception is a migration whose table or added column is missing, which means the branch's migration sat at that number instead. Tables that a later migration in that range drops or renames are left out of that check (migration 29 rebuilds look that way).
  • Idempotent statements: each CREATE TABLE/INDEX/TRIGGER/VIEW runs with IF NOT EXISTS, and an ALTER TABLE … ADD COLUMN whose column already exists is skipped.
  • One-line failure: a migration runs statement by statement inside its own transaction. A failure reads migration N failed at "<statement>": <sqlite message>, and that line becomes the deploy's failure summary shown on the pull. Nothing from the failed migration is applied.
  • forge.db: the empty file #433 committed is removed, and .gitignore now ignores /.db, /.db-wal and /*.db-shm at the repo root.
  • Docs: CLAUDE.md and docs/ARCHITECTURE.md now describe the tracking.

test/schema-migrations.test.ts is new and in the gate. It boots a preview on copied production data twice, boots a branch whose migration a rebase renumbered, and boots a rebased branch on data the old runner wrote with the branch's table already there. It also covers a column that is already there, adoption of production's own pre-tracking data, refusal of data ahead of the build, the one-line failure through failureReason, and statement splitting. No existing test was changed.

Not done: a build still refuses to run on data that has applied more migrations than it knows (e.g. a branch not rebased onto newer production data). That behaviour is deliberate and unchanged; per docs/APPS.md a preview then retries once on empty data.

Verification:

  • The new test file and tsc --noEmit pass locally.
  • The full gate could not run cleanly in my sandbox: the sandbox blocks tests from listening on sockets (and from opening files in /tmp), which broke 179 tests. None of those were assertion failures. The forge check is the real verdict.
  • test/usage.test.ts (not in the gate) fails the same way on main.

To try it: bun test test/schema-migrations.test.ts.

🤖 Generated with Claude Code

Closes #558

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs