fix(database): retire a table whose shape the schema moved past
`applySchema` is CREATE ... IF NOT EXISTS and there is no migration chain, so a *changed* table never migrates: the statement silently no-ops against the old shape. Two plans had already landed on that, and neither showed up in a test because a fresh install is perfectly healthy. - 014 added `total_tracks` to explore_index and to `indexRowFields`, the projection every explore read uses, so every search, browse, artist page and album page failed with "no such column: total_tracks" on any database that already had a catalog. - 013 reshaped audio_files, so applySchema could not run at all and the app did not open. staleshape.go runs before applySchema and drops what disagrees, so the create is a create. It parses sql/schemas/ for the expectation rather than writing the column list down a second time, and it notices a changed *type* as well as a missing column — 013 moved mbid TEXT to BLOB, which no ALTER could express and which SQLite will not coerce, so a query against 16 raw bytes returns no rows rather than an error. Only Authored tables are exempt. Cache is rebuildable by definition, Owned is what a rescan rebuilds (plan 013's stated "delete and rescan"), and a table the schema no longer describes at all goes too -- 013 left seven behind plus schema_migrations. Three things in it are load-bearing, and each was a bug first: - The parser read `UNIQUE(mbid)` as a column, which made a healthy catalog look stale. That would have retired it on every launch and cost every user an artifact download per start. - The drops are one transaction with defer_foreign_keys. Those legacy tables reference each other, so any order fails on whichever goes first; turning foreign keys off instead would suppress playlist_tracks.audio_file_id's ON DELETE SET NULL and leave entries pointing at ids a rescan reissues to *different songs*. Nulled entries are empty; stale ones are wrong, and wrong quietly. - The order is sorted, so a failure reproduces. Map order is random, and the foreign-key bug passed its own regression test on two runs in three until the order was fixed. Verified against a real pre-013 install: it opens, its 22 playlists survive, 1,887 linked playlist entries become 0 rather than dangling, and the legacy tables are swept. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfVYUVExXsx1nSWrXN8mAh
This commit is contained in:
@@ -233,6 +233,51 @@ rather than renaming them.
|
||||
the drift it caused before — `sql/schemas/` and the migrations
|
||||
disagreed, and sqlc generated against the stale one.
|
||||
|
||||
**What that costs an existing database is repaired once, at open.**
|
||||
`CREATE ... IF NOT EXISTS` reaches an existing table only if its shape
|
||||
already matches and otherwise silently no-ops, so a *changed* table
|
||||
never migrates. Plan 014 added `total_tracks` to `explore_index` and
|
||||
to `indexRowFields` — the projection every explore read uses — and no
|
||||
database that already existed grew the column: **every** Explore
|
||||
search, browse, artist and album page on such an install failed with
|
||||
`no such column: total_tracks`, while a fresh install was perfectly
|
||||
healthy, which is exactly why no test saw it. Plan 013 was worse on
|
||||
the same install: `applySchema` could not be applied at all over a
|
||||
pre-013 `audio_files`, so the app did not open.
|
||||
|
||||
`backend/database/staleshape.go` runs before `applySchema` and
|
||||
retires what is stale, so the create is a create. Five things about
|
||||
it are load-bearing:
|
||||
- **It parses `sql/schemas/` for the expectation** rather than
|
||||
writing the column list down a second time, because a second list
|
||||
is a second thing to forget — the fault it exists to repair.
|
||||
- **It notices a changed *type*, not just a missing column.** 013
|
||||
moved `mbid` from TEXT to BLOB, and SQLite does not coerce between
|
||||
them: a comparison against 16 raw bytes returns no rows rather than
|
||||
an error. `ALTER TABLE ADD COLUMN` would have handled
|
||||
`total_tracks` alone and cannot express this at all, which is why
|
||||
the repair drops rather than migrates.
|
||||
- **`Authored` is never retired**, and that boundary is a test
|
||||
(`TestAuthoredTablesAreNeverRetired`), not a comment. Everything
|
||||
else is rebuildable: `Cache` by definition, `Owned` by a rescan —
|
||||
plan 013's stated "delete and rescan" — and `Derived` from Owned.
|
||||
A table the schema no longer describes at all goes too; 013 left
|
||||
seven behind plus `schema_migrations`.
|
||||
- **The drops are one transaction with `defer_foreign_keys`.** Those
|
||||
legacy tables reference each other, so dropping them in any order
|
||||
fails on whichever goes first, and turning foreign keys *off*
|
||||
instead would silently take `playlist_tracks.audio_file_id`'s
|
||||
ON DELETE SET NULL with it — leaving playlist entries pointing at
|
||||
ids a rescan reissues to *different songs*. Nulled entries are
|
||||
empty; stale ones are wrong, and wrong quietly.
|
||||
- **The order is sorted, so a failure reproduces.** Map order is
|
||||
random, and the foreign-key bug above passed its own regression
|
||||
test on two runs in three until the order was fixed.
|
||||
|
||||
Retiring `explore_index` takes its FTS and its meta with it, because
|
||||
the `dump_import_done` marker is what would otherwise stop the
|
||||
artifact ever being fetched again.
|
||||
|
||||
**What that costs an existing database is that it does not open**, and
|
||||
"delete and rescan" is the answer (plan 013, open question 1) — free
|
||||
for everyone except one machine. The index job's `/cache` volume is a
|
||||
|
||||
Reference in New Issue
Block a user