From 0e98cc8fbb0e4afab48fb3d49199fa363bd5cc56 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Sat, 12 Sep 2026 11:53:32 +0000 Subject: [PATCH] 0.17.1: /api/v1/events answers again on a database that predates concerts nixamp.com had live_events from before 0.17.0. The schema in live-events.ts built the index on kind before the ALTER TABLE that adds kind to an existing table, so the whole statement failed with "column kind does not exist", the store cached that rejected promise, and every /api/v1/events request answered 500 "the live event service failed" until the process restarted. A fresh database (tests, CI) creates the table with kind already and never saw it. - the ADD COLUMN statements now run before the kind index - a failed schema run is retried on the next request instead of remembered - the 500 path logs the underlying error - a test pins the statement order and the retry - 0.17.1 in package.json, desktop and web Verified by running the fixed schema against the production database in a rolled-back transaction. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f --- desktop/package.json | 2 +- package.json | 2 +- src/live-api.ts | 1 + src/live-events.ts | 17 ++++++++++++++--- test/live-events.test.ts | 23 +++++++++++++++++++++++ web/package.json | 2 +- 6 files changed, 41 insertions(+), 6 deletions(-) diff --git a/desktop/package.json b/desktop/package.json index bbe7f8a..5bf0ae6 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@nixamp/desktop", "productName": "nixamp", - "version": "0.17.0", + "version": "0.17.1", "private": true, "description": "nixamp as a desktop app: the PWA in a window, with the CLI bundled in.", "author": "Profullstack, Inc. ", diff --git a/package.json b/package.json index c4f4e54..d51e6ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "nixamp", - "version": "0.17.0", + "version": "0.17.1", "description": "It really whips the terminal's ass. A Winamp-shaped audio player for your terminal.", "license": "MIT", "type": "module", diff --git a/src/live-api.ts b/src/live-api.ts index 3477a4f..26cc0ec 100644 --- a/src/live-api.ts +++ b/src/live-api.ts @@ -628,6 +628,7 @@ export async function handleLiveApi( json(response, error.status, { error: error.message }); return true; } + console.error(`live api ${request.method} ${path}:`, error instanceof Error ? error.message : error); json(response, 500, { error: "the live event service failed" }); return true; } diff --git a/src/live-events.ts b/src/live-events.ts index 41685e9..d43d6a6 100644 --- a/src/live-events.ts +++ b/src/live-events.ts @@ -173,19 +173,22 @@ const EVENT_SCHEMA = ` ON live_events (visibility, status, starts_at, updated_at DESC); CREATE INDEX IF NOT EXISTS live_events_owner ON live_events (owner_id, updated_at DESC); - CREATE INDEX IF NOT EXISTS live_events_kind - ON live_events (kind, status, starts_at); -- The repo has no migration runner, so a table that already exists is -- brought forward here. Every statement is idempotent, and the CHECKs are -- replaced by name rather than added twice: the originals were unnamed, so -- the ones carrying the old status list are found by what they say. + -- The columns come before any index on them: nixamp.com had live_events + -- from before 0.17.0, and an index on kind ahead of ADD COLUMN kind failed + -- there while every fresh database (tests, CI) sailed through. ALTER TABLE live_events ADD COLUMN IF NOT EXISTS kind TEXT NOT NULL DEFAULT 'talk'; ALTER TABLE live_events ADD COLUMN IF NOT EXISTS doors_open_at TIMESTAMPTZ; ALTER TABLE live_events ADD COLUMN IF NOT EXISTS ticket_price_cents INTEGER NOT NULL DEFAULT 0; ALTER TABLE live_events ADD COLUMN IF NOT EXISTS ticket_currency TEXT NOT NULL DEFAULT 'USD'; ALTER TABLE live_events ADD COLUMN IF NOT EXISTS ticket_minutes INTEGER NOT NULL DEFAULT 1440; ALTER TABLE live_events ADD COLUMN IF NOT EXISTS pay_to TEXT; + CREATE INDEX IF NOT EXISTS live_events_kind + ON live_events (kind, status, starts_at); DO $$ DECLARE stale record; @@ -511,7 +514,15 @@ export class LiveEvents { constructor(private readonly db: Queryable) {} private async ensure(): Promise { - this.ready ??= this.db.query(EVENT_SCHEMA).then(() => undefined); + // A failed schema run is not remembered: the next request tries again + // rather than answering 500 until the process restarts. + this.ready ??= this.db.query(EVENT_SCHEMA).then( + () => undefined, + (error: unknown) => { + this.ready = null; + throw error; + }, + ); await this.ready; } diff --git a/test/live-events.test.ts b/test/live-events.test.ts index 576b4f9..5fdedf2 100644 --- a/test/live-events.test.ts +++ b/test/live-events.test.ts @@ -232,3 +232,26 @@ test("an artist invitation is a role the store accepts", async () => { }); assert.equal(invitation.role, "artist"); }); + +test("the schema adds every new column before it indexes one, and a failed run is retried", async () => { + const seen: string[] = []; + let failures = 1; + const database: Queryable = { + async query(text) { + seen.push(text); + if (text.includes("CREATE TABLE") && failures-- > 0) throw new Error("column \"kind\" does not exist"); + return { rows: [] }; + }, + }; + const events = new LiveEvents(database); + await assert.rejects(events.list(), /kind/); + await events.list(); + assert.equal(seen.filter((text) => text.includes("CREATE TABLE")).length, 2, "the schema runs again after a failure"); + const schema = seen[0]!; + for (const column of ["kind", "doors_open_at", "ticket_price_cents", "ticket_currency", "ticket_minutes", "pay_to"]) { + const added = schema.indexOf(`ADD COLUMN IF NOT EXISTS ${column}`); + assert.ok(added > 0, `${column} is brought forward`); + const indexed = schema.indexOf(`CREATE INDEX IF NOT EXISTS live_events_kind`); + assert.ok(indexed > added, `${column} exists before the kind index is built`); + } +}); diff --git a/web/package.json b/web/package.json index f504b15..5d813fd 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@nixamp/web", - "version": "0.17.0", + "version": "0.17.1", "private": true, "description": "The nixamp PWA: a browser player, and a remote for nixamp serve.", "license": "MIT",