The commit before this removed the tag from the Makefile, lefthook, both packaging recipes and CI, but left it in CLAUDE.md's "Running tests" section and the yellowjacket-dev skill — which are the copies a coding agent actually runs, so a stale tag there is worse than one in prose. skill-check does not catch this: it verifies that documented make targets exist, not that documented go commands do. The historical mentions in .planning/ and .pi/journal.md are left alone; they are records of what was true then. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UDCbcCZQepnpSQYJ6SxxZm
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:
backend/database/sql/schemas/*.sql—CREATE TABLE ... IF NOT EXISTS, the literal target shape, what sqlc reads and what a fresh install gets verbatim. Add the new column last in theCREATE TABLE.backend/database/sql/migrations/NNNN_description.sql— theALTER 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 ./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 COLUMNalways appends, so a migrated column declared anywhere but last inCREATE TABLEleaves fresh and upgraded installs disagreeing on order — and sqlc bindsSELECT *positionally, so one of them silently reads the wrong field.TestMigrations_ColumnOrderMatchesFreshInstallis 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 theALTER TABLE. - Do not add a third description of the schema anywhere. A
migration's
ADD COLUMNfailing 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.