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

Schema migrations are identified by name, so two pulls can each add one without colliding #2560

Merged opened this in answer to

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

Merged

Migrations are now recorded by name in the database, and a copied database that has one more or one fewer migration than its code still boots. Released migrations are pinned so an edit fails a test. Named migration files and either-order apply were already on main (1918be80, f6510826, 31c4791e); this pull fills the remaining gaps.

shipped production is serving it deploy 93e874d

opened this

Migrations are now recorded by name in the database, and a copied database that has one more or one fewer migration than its code still boots. Released migrations are pinned so an edit fails a test. Named migration files and either-order apply were already on main (1918be80, f6510826, 31c4791e); this pull fills the remaining gaps.

What failed last time: test/kernel.test.ts "future schema refuses to run" still expected the refusal that #783 asks to remove. That test was wrong for the new requirement, so I rewrote it. It now checks that a database ahead of its build boots and names the migration it lacks. It also checks that a database from before the tracking table, which has only a migration count, is still refused when ahead.

Changes

  • Names recorded: schema_migration gains a name column (src/kernel/migrate.ts). It is filled for every migration applied, and backfilled for rows written before this change, leaving ids, positions and timestamps as they were. Migrations are still identified by a hash of their text, so a renumbered or moved migration is still recognised.
  • Ahead or behind, it boots: missing migrations are applied. Migrations the build doesn't know are left in place and named instead of raising db.future_schema. openDb logs a warning, and --preflight lists them in its "preflight ok" line. app.ts, main.ts and the chaos worker now pass the named migrationFiles.
  • Pins: src/core/migration-pins.json records each migration's text hash by name; all 117 current ones are pinned. bun scripts/pin-migrations.ts adds pins for new migrations and never rewrites one. The release agent runs it, so pulls don't share the file. test/schema-migrations.test.ts fails, naming the migration, when a pinned one is edited or removed.
  • Tests and docs: the old refusal test in test/schema-migrations.test.ts is replaced too. New tests cover:

- a copied database with one more, and one fewer, migration than its code; - names written into a database from before names were kept; - both migrations recorded by name in the either-order merge test; - pin breaks.

CLAUDE.md, docs/ARCHITECTURE.md, the releases.json note and the rollback.ts header are updated to match.

Not changed: the rollback gate still compares migration counts, and still refuses a release older than the live database.

Testing: the type check is clean. Every gated test file that imports kernel/db, kernel/migrate or kernel/preflight passes (17 files, 311 tests, 1 skipped). I didn't run the full check locally.

To try it: boot a build with an extra migration file on a copy of the data, then boot one without it on the same data. It starts and logs database has migrations this build does not know with the file's name.

🤖 Generated with Claude Code

Closes #783

commented

Migrations are now recorded by name in the database, and a copied database that has one more or one fewer migration than its code still boots. Released migrations are pinned so an edit fails a test. Named migration files and either-order apply were already on main (1918be80, f6510826, 31c4791e); this pull fills the remaining gaps.

What failed last time: test/kernel.test.ts "future schema refuses to run" still expected the refusal that #783 asks to remove. That test was wrong for the new requirement, so I rewrote it. It now checks that a database ahead of its build boots and names the migration it lacks. It also checks that a database from before the tracking table, which has only a migration count, is still refused when ahead.

Changes

  • Names recorded: schema_migration gains a name column (src/kernel/migrate.ts). It is filled for every migration applied, and backfilled for rows written before this change, leaving ids, positions and timestamps as they were. Migrations are still identified by a hash of their text, so a renumbered or moved migration is still recognised.
  • Ahead or behind, it boots: missing migrations are applied. Migrations the build doesn't know are left in place and named instead of raising db.future_schema. openDb logs a warning, and --preflight lists them in its "preflight ok" line. app.ts, main.ts and the chaos worker now pass the named migrationFiles.
  • Pins: src/core/migration-pins.json records each migration's text hash by name; all 117 current ones are pinned. bun scripts/pin-migrations.ts adds pins for new migrations and never rewrites one. The release agent runs it, so pulls don't share the file. test/schema-migrations.test.ts fails, naming the migration, when a pinned one is edited or removed.
  • Tests and docs: the old refusal test in test/schema-migrations.test.ts is replaced too. New tests cover:

- a copied database with one more, and one fewer, migration than its code; - names written into a database from before names were kept; - both migrations recorded by name in the either-order merge test; - pin breaks.

CLAUDE.md, docs/ARCHITECTURE.md, the releases.json note and the rollback.ts header are updated to match.

Not changed: the rollback gate still compares migration counts, and still refuses a release older than the live database.

Testing: the type check is clean. Every gated test file that imports kernel/db, kernel/migrate or kernel/preflight passes (17 files, 311 tests, 1 skipped). I didn't run the full check locally.

To try it: boot a build with an extra migration file on a copy of the data, then boot one without it on the same data. It starts and logs database has migrations this build does not know with the file's name.

🤖 Generated with Claude Code

Approved

Approved. Migrations are now recorded by name, a database ahead of or behind its build boots (the ahead case is logged and listed in preflight), and released migrations are pinned so an edit fails a test. The legacy count-only database is still refused when ahead, and that case is tested. Every behaviour added has a test, and there is no new dependency, SQL built from user text, or UI surface. This change has no page or terminal surface, so both doors are n/a. I could not reach the preview from my sandbox (the proxy denied the host), so I judged the diff only.

Two things I would do next:

  1. Make the rollback gate in src/release/rollback.ts compare migration names rather than counts, now that names are recorded. The gate now refuses a release that is behind the live database for a reason the database no longer shares.
  2. Either use Migrated.applied outside tests or drop it, and wrap the over-long CLAUDE.md migration paragraph.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs