From a5b9733bd06fccf121222c77e36a0a13535147c7 Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 12 Sep 2026 11:03:50 -0400 Subject: [PATCH] Auto-migrate any missing servers column, not just recent additions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Legacy cherry.db files created before commit 854f553 lack the game_port column (it was added via CREATE TABLE which is a no-op on existing tables). Users upgrading through several versions were left with a partial schema, and setServerField('game_port', ...) crashed with "no such column: game_port" — which the worker's generic catch mislabeled as an RCON send-error in the UI. Replaces the hardcoded post-v0.1 migration list with an introspection- based one: PRAGMA table_info(servers), diff against the desired column set, ADD COLUMN for anything missing. Covers base columns too (komodo_ server, game_port, rcon_host, last_players, last_checked_at) so legacy DBs from any past version heal themselves on boot. Verified manually: created a 3-column legacy servers table, booted the app, all 13 desired columns present after PRAGMA re-check. Co-Authored-By: Claude Opus 4.7 --- src/lib/server/db/index.ts | 39 +++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts index e3e63c7..ff8fc5d 100644 --- a/src/lib/server/db/index.ts +++ b/src/lib/server/db/index.ts @@ -6,24 +6,33 @@ import schemaSql from './schema.sql?raw'; let _db: Database.Database | null = null; -// Columns that were added to `servers` after v0.1 shipped. Applied on every -// boot; better-sqlite3 has no `ADD COLUMN IF NOT EXISTS`, so we run each and -// swallow the "duplicate column" error. -const SERVERS_MIGRATIONS: Array<{ column: string; ddl: string }> = [ - { column: 'last_map', ddl: 'ALTER TABLE servers ADD COLUMN last_map TEXT' }, - { column: 'last_gametype', ddl: 'ALTER TABLE servers ADD COLUMN last_gametype TEXT' }, - { column: 'last_roster', ddl: 'ALTER TABLE servers ADD COLUMN last_roster TEXT' }, - { column: 'last_seen_at', ddl: 'ALTER TABLE servers ADD COLUMN last_seen_at TEXT' }, - { column: 'last_error', ddl: 'ALTER TABLE servers ADD COLUMN last_error TEXT' } +// Desired shape of the `servers` table. Every column here is added to the +// table on boot if missing — introspection via PRAGMA table_info handles +// both fresh DBs and legacy DBs that predate any given column. CREATE TABLE +// IF NOT EXISTS in schema.sql is a no-op on existing tables, so it can't +// backfill columns; this can. +const SERVERS_COLUMNS: Array<{ name: string; ddl: string }> = [ + // Base columns that shipped in the very first schema stay here for parity + // with fresh installs — no ALTER is issued if they're already present. + { name: 'komodo_server', ddl: 'ALTER TABLE servers ADD COLUMN komodo_server TEXT' }, + { name: 'game_port', ddl: 'ALTER TABLE servers ADD COLUMN game_port INTEGER' }, + { name: 'rcon_host', ddl: 'ALTER TABLE servers ADD COLUMN rcon_host TEXT' }, + { name: 'last_players', ddl: 'ALTER TABLE servers ADD COLUMN last_players INTEGER' }, + { name: 'last_checked_at', ddl: 'ALTER TABLE servers ADD COLUMN last_checked_at TEXT' }, + // Post-v0.1 additions from the perk-a-cola/diagnostics work + { name: 'last_map', ddl: 'ALTER TABLE servers ADD COLUMN last_map TEXT' }, + { name: 'last_gametype', ddl: 'ALTER TABLE servers ADD COLUMN last_gametype TEXT' }, + { name: 'last_roster', ddl: 'ALTER TABLE servers ADD COLUMN last_roster TEXT' }, + { name: 'last_seen_at', ddl: 'ALTER TABLE servers ADD COLUMN last_seen_at TEXT' }, + { name: 'last_error', ddl: 'ALTER TABLE servers ADD COLUMN last_error TEXT' } ]; function migrateServers(handle: Database.Database): void { - for (const m of SERVERS_MIGRATIONS) { - try { - handle.exec(m.ddl); - } catch (err) { - const msg = (err as Error).message ?? ''; - if (!/duplicate column name/i.test(msg)) throw err; + const rows = handle.prepare('PRAGMA table_info(servers)').all() as Array<{ name: string }>; + const existing = new Set(rows.map((r) => r.name)); + for (const col of SERVERS_COLUMNS) { + if (!existing.has(col.name)) { + handle.exec(col.ddl); } } }