Files
yellowjacket/.pi/skills/yellowjacket-dev/references/schema-change.md
T
logan d33dfb2264
Build & publish Arch package / arch-package (push) Successful in 2m2s
CI / check (push) Successful in 2m47s
Search index maintenance / maintain-index (push) Successful in 8s
CI / e2e (push) Successful in 6m11s
docs: record phase 4, and the counts a new guard has to agree with
Plan 008 is complete and moves to completed/. The two findings worth
carrying forward are that a new table needs one schema file rather than
two (and a datamap entry, which is a gate nobody remembers), and that
excluding a path has to reach every place that counts what is in the
library — the soft scan's disk-vs-database comparison above all, which
would otherwise have rescanned the whole library on every launch with
nothing failing anywhere.
2026-08-13 13:30:37 -04:00

3.0 KiB

Changing the database schema

The reasoning — why there are two files, what the old 48-step migration chain got wrong, and when squashing is legitimate — is in CLAUDE.md under Backend packages → database. Read it once. This is the checklist.

A brand-new table needs one file, not two. The rule below is about a column added to a table that already exists. applySchema runs every file in sql/schemas/ on every open, so a CREATE TABLE IF NOT EXISTS reaches an existing install verbatim and a migration for it would be a second description of the same table — the thing the third rule forbids. Its indexes go in the schema file too, because the column and the index arrive together.

A new table has a second gate: backend/datamap. Add an entry stating its Kind and Lifetime, or TestCatalogCoversSchema fails — and if it is Authored and cascades, TestAuthoredCascadesAreDeliberate wants an explicit exemption with a note, because authored data is what a user cannot get back.

Adding a column to an existing table needs two files, not one:

  1. backend/database/sql/schemas/*.sqlCREATE TABLE ... IF NOT EXISTS, the literal target shape, what sqlc reads and what a fresh install gets verbatim. Add the new column last in the CREATE TABLE.
  2. backend/database/sql/migrations/NNNN_description.sql — the ALTER TABLE ... ADD COLUMN (and any index on it) that gets an existing database to the same shape. Schema files are a no-op against a table that already exists, so without this an upgrade never gets the column.

Then:

make generate                                        # sqlc + templ
go test -tags webkit2_41 ./backend/database/         # migration + column-order tests
make test

Rebuild any seed you rely on (make sandbox-seed NAME=default) and delete your own dev YJ_HOME if you want to see the fresh-install path rather than the migrated one.

The three ways this goes wrong

  • Column order must match between the two paths. ADD COLUMN always appends, so a migrated column declared anywhere but last in CREATE TABLE leaves fresh and upgraded installs disagreeing on order — and sqlc binds SELECT * positionally, so one of them silently reads the wrong field. TestMigrations_ColumnOrderMatchesFreshInstall is the regression test.
  • Do not put an index on a migrated column in sql/schemas/. Schema files run before migrations, against a database that may not have the column yet, and the predicate fails. Declare the index in the migration, after the ALTER TABLE.
  • Do not add a third description of the schema anywhere. A migration's ADD COLUMN failing with "duplicate column name" against an already-current database is expected and tolerated, not an error to route around.

New queries go in backend/database/sql/queries/; generated Go lands in backend/database/sql/sqlcgen/, which is never edited by hand. Tests use database.NewTestDB(t), built by the same applySchema production uses, so the two cannot diverge.