fix: proper migration version tracking via user_version PRAGMA

- Migrations now run exactly once per DB lifetime, tracked via
  SQLite's user_version PRAGMA
- Re-runs become no-ops after schema reaches target version
- v0.2 also made defensive — skips if template_id already dropped

Fixes "no such column: layouts.template_id" on second startup after
v0.5 rebuild dropped the legacy columns.
This commit is contained in:
Mitchell R 2026-05-10 22:18:03 +02:00
parent 16ab165b06
commit 374a2e091b
2 changed files with 30 additions and 6 deletions

View file

@ -120,14 +120,29 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
this.db.exec("PRAGMA foreign_keys = ON"); this.db.exec("PRAGMA foreign_keys = ON");
this.db.exec("PRAGMA busy_timeout = 10000"); this.db.exec("PRAGMA busy_timeout = 10000");
obs.log.info("running {n} migrations", { n: MIGRATIONS.length }); // Track schema version via SQLite's built-in user_version PRAGMA.
for (const entry of MIGRATIONS) { // Each migration entry runs exactly once across all server boots.
const row = this.db.prepare("PRAGMA user_version").get() as { user_version: number };
const currentVersion = row.user_version;
const targetVersion = MIGRATIONS.length;
if (currentVersion < targetVersion) {
obs.log.info("running migrations from {from} to {to}", {
from: currentVersion,
to: targetVersion,
});
for (let i = currentVersion; i < targetVersion; i++) {
const entry = MIGRATIONS[i];
if (typeof entry === "string") { if (typeof entry === "string") {
this.db.exec(entry); this.db.exec(entry);
} else { } else if (typeof entry === "function") {
entry(this.db); entry(this.db);
} }
} }
this.db.exec(`PRAGMA user_version = ${targetVersion}`);
} else {
obs.log.info("schema up to date (version {v})", { v: currentVersion });
}
this._repo = new Repository(this.db, async (table, op, id) => { this._repo = new Repository(this.db, async (table, op, id) => {
// Best-effort broadcast — never let a failed event-bus call fail a write. // Best-effort broadcast — never let a failed event-bus call fail a write.

View file

@ -270,6 +270,15 @@ export const MIGRATIONS: readonly MigrationEntry[] = [
// ---- v0.2: flatten layout_templates into layouts, display→kiosk inversion --- // ---- v0.2: flatten layout_templates into layouts, display→kiosk inversion ---
(db: DatabaseSync) => { (db: DatabaseSync) => {
// Skip entirely if v0.5 rebuild already dropped template_id (idempotent re-run)
const cols = db.prepare(`PRAGMA table_info("layouts")`).all() as Array<{ name: string }>;
const hasTemplateId = cols.some((c) => c.name === "template_id");
if (!hasTemplateId) {
// Just ensure displays.kiosk_id exists for fresh-but-post-v0.5 DBs
addColumnIfNotExists(db, "displays", "kiosk_id", "INTEGER REFERENCES kiosks(id) ON DELETE SET NULL");
return;
}
addColumnIfNotExists(db, "layouts", "regions", "TEXT NOT NULL DEFAULT '[]'"); addColumnIfNotExists(db, "layouts", "regions", "TEXT NOT NULL DEFAULT '[]'");
addColumnIfNotExists(db, "layouts", "grid_cols", "INTEGER NOT NULL DEFAULT 1"); addColumnIfNotExists(db, "layouts", "grid_cols", "INTEGER NOT NULL DEFAULT 1");
addColumnIfNotExists(db, "layouts", "grid_rows", "INTEGER NOT NULL DEFAULT 1"); addColumnIfNotExists(db, "layouts", "grid_rows", "INTEGER NOT NULL DEFAULT 1");