migrate existing sqlite dbs to the new columns, don't force a nuke on deploy
All checks were successful
Build and Push Docker Image / build (push) Successful in 52s

upgraded containers (like the unraid one with a persistent ./data volume)
kept their old media_items/media_streams tables, so upsertJellyfinItem hit
"table media_items has no column named original_title" on first scan after
the canonical-language rewrite. sqlite has no native add-column-if-not-exists
so we try/catch each alter, same pattern we had before i deleted the shims.

also backports the older alters (stream_decisions.custom_title, review_plans
subs_extracted/confidence/apple_compat/job_type) so pre-rewrite installs
still converge without a wipe.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 15:09:45 +02:00
parent b1b15924ec
commit c06172f412
3 changed files with 42 additions and 3 deletions

View File

@@ -51,11 +51,50 @@ export function getDb(): Database {
if (_db) return _db;
_db = new Database(dbPath, { create: true });
_db.exec(SCHEMA);
applyMigrations(_db);
seedDefaults(_db);
return _db;
}
/**
* Add columns that landed after the initial schema. `CREATE TABLE IF NOT
* EXISTS` above skips existing tables, so upgraded installs need per-column
* ALTERs to pick up new fields. Each call is wrapped in try/catch because
* SQLite has no native `ADD COLUMN IF NOT EXISTS` — the error is benign.
*
* Do NOT remove entries here just because the schema block also declares the
* column. Fresh installs get it from SCHEMA; existing deployments (e.g. the
* Unraid container with a persistent volume) only get it from this function.
*/
function applyMigrations(db: Database): void {
const addColumn = (table: string, column: string, type: string) => {
try {
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
} catch {
/* already exists */
}
};
// 2026-04-13: canonical-language + full-jellyfin-capture rewrite
addColumn("media_items", "original_title", "TEXT");
addColumn("media_items", "runtime_ticks", "INTEGER");
addColumn("media_items", "date_last_refreshed", "TEXT");
addColumn("media_items", "jellyfin_raw", "TEXT");
addColumn("media_items", "external_raw", "TEXT");
addColumn("media_items", "last_executed_at", "TEXT");
addColumn("media_streams", "profile", "TEXT");
addColumn("media_streams", "bit_depth", "INTEGER");
// Earlier migrations kept here so installs that predate the big rewrite
// still converge to the current schema.
addColumn("stream_decisions", "custom_title", "TEXT");
addColumn("stream_decisions", "transcode_codec", "TEXT");
addColumn("review_plans", "subs_extracted", "INTEGER NOT NULL DEFAULT 0");
addColumn("review_plans", "confidence", "TEXT NOT NULL DEFAULT 'low'");
addColumn("review_plans", "apple_compat", "TEXT");
addColumn("review_plans", "job_type", "TEXT NOT NULL DEFAULT 'copy'");
}
function seedDefaults(db: Database): void {
const insert = db.prepare("INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)");
for (const [key, value] of Object.entries(DEFAULT_CONFIG)) {