0010. Schema migrations versioned with PRAGMA user_version
- Status: Accepted
- Date: 2026-10-03
Context
ADR 0002 migrates the schema in place at startup with CREATE TABLE IF NOT EXISTS and addColumnIfMissing(). That covers new tables and columns, but not indexes or data migrations (rewriting existing rows), and nothing records which changes a given database already went through. The author page needs an index on credits(person_id), and an SQL-side search would want indexes on title and series.
Decision
src/main/db/schema.tsholds an orderedMIGRATIONSlist (version,run(db)).migrate()readsPRAGMA user_versionand applies, in order, every migration with a higher version.- Each migration runs through
withTransaction()together with itsPRAGMA user_version = N, so a failure leaves the database at the last fully applied version. - Migration 1 is everything that existed before versioning (the tables and the
addColumnIfMissing()columns), kept idempotent: an installed database is at version 0 and goes through it without harm. Migration 2 addsidx_credits_person_id. - A shipped migration is never edited or reordered; a schema change is a new entry at the end.
- A database whose version is ahead of the build (written by a newer release) is left untouched rather than refused, since migrations are additive.
Consequences
- Indexes and data fixes now have a place, and run exactly once.
scripts/gen-reference.mjsbuilds the schema reference by running the literal SQL ofschema.tsin file order, and lists the indexes.- Destructive changes (renaming or dropping a column) are still not covered by
addColumnIfMissing(), but a migration can now do them in a transaction. - Opening a database with an older build after a newer one migrated it is not guarded against.
Alternatives considered
- A
schema_migrationstable: more flexible (names, dates), but one integer is enough for a linear history in a single-user local file. - Keeping idempotent
IF NOT EXISTSsteps only: runs every step on every start and can't express a one-time data fix.