Auto-migrate any missing servers column, not just recent additions

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 11:03:50 -04:00
co-authored by Claude Opus 4.7
parent 15ea2ddfce
commit a5b9733bd0
+24 -15
View File
@@ -6,24 +6,33 @@ import schemaSql from './schema.sql?raw';
let _db: Database.Database | null = null; let _db: Database.Database | null = null;
// Columns that were added to `servers` after v0.1 shipped. Applied on every // Desired shape of the `servers` table. Every column here is added to the
// boot; better-sqlite3 has no `ADD COLUMN IF NOT EXISTS`, so we run each and // table on boot if missing — introspection via PRAGMA table_info handles
// swallow the "duplicate column" error. // both fresh DBs and legacy DBs that predate any given column. CREATE TABLE
const SERVERS_MIGRATIONS: Array<{ column: string; ddl: string }> = [ // IF NOT EXISTS in schema.sql is a no-op on existing tables, so it can't
{ column: 'last_map', ddl: 'ALTER TABLE servers ADD COLUMN last_map TEXT' }, // backfill columns; this can.
{ column: 'last_gametype', ddl: 'ALTER TABLE servers ADD COLUMN last_gametype TEXT' }, const SERVERS_COLUMNS: Array<{ name: string; ddl: string }> = [
{ column: 'last_roster', ddl: 'ALTER TABLE servers ADD COLUMN last_roster TEXT' }, // Base columns that shipped in the very first schema stay here for parity
{ column: 'last_seen_at', ddl: 'ALTER TABLE servers ADD COLUMN last_seen_at TEXT' }, // with fresh installs — no ALTER is issued if they're already present.
{ column: 'last_error', ddl: 'ALTER TABLE servers ADD COLUMN last_error TEXT' } { 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 { function migrateServers(handle: Database.Database): void {
for (const m of SERVERS_MIGRATIONS) { const rows = handle.prepare('PRAGMA table_info(servers)').all() as Array<{ name: string }>;
try { const existing = new Set(rows.map((r) => r.name));
handle.exec(m.ddl); for (const col of SERVERS_COLUMNS) {
} catch (err) { if (!existing.has(col.name)) {
const msg = (err as Error).message ?? ''; handle.exec(col.ddl);
if (!/duplicate column name/i.test(msg)) throw err;
} }
} }
} }