From 37a36e287bb33c6480df57a4e2f8611951f93f34 Mon Sep 17 00:00:00 2001 From: foxtacles Date: Sat, 2 May 2026 14:15:28 -0700 Subject: [PATCH] Optimize latest memory queries (#35) --- server/migrations/0007_memory_events.sql | 46 ++++++++++++++++++++++++ server/src/account.ts | 13 +++++++ server/src/index.ts | 16 ++------- 3 files changed, 62 insertions(+), 13 deletions(-) create mode 100644 server/migrations/0007_memory_events.sql diff --git a/server/migrations/0007_memory_events.sql b/server/migrations/0007_memory_events.sql new file mode 100644 index 0000000..7910985 --- /dev/null +++ b/server/migrations/0007_memory_events.sql @@ -0,0 +1,46 @@ +-- Denormalized latest-per-event view of memory_completions. +-- Powers /api/memories/latest, /api/memory/:eventId, /api/sitemap. +-- Update memory_events alongside any schema change to memory_completions. +CREATE TABLE IF NOT EXISTS memory_events ( + event_id TEXT PRIMARY KEY, + anim_index INTEGER NOT NULL, + completed_at INTEGER NOT NULL, + participants TEXT NOT NULL, + language TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_memory_events_completed_at + ON memory_events(completed_at DESC); + +-- AFTER INSERT does not fire when INSERT OR IGNORE hits the UNIQUE conflict, +-- so duplicate completions correctly do not update memory_events. +CREATE TRIGGER IF NOT EXISTS memory_events_after_insert +AFTER INSERT ON memory_completions +BEGIN + INSERT INTO memory_events (event_id, anim_index, completed_at, participants, language) + VALUES (NEW.event_id, NEW.anim_index, NEW.completed_at, NEW.participants, NEW.language) + ON CONFLICT(event_id) DO UPDATE SET + anim_index = excluded.anim_index, + completed_at = excluded.completed_at, + participants = excluded.participants, + language = excluded.language + WHERE excluded.completed_at > memory_events.completed_at; +END; + +-- Backfill: pick the latest row per event_id deterministically by (completed_at DESC, id DESC). +-- UPSERT semantics so writes captured by the trigger between CREATE TRIGGER and now are not clobbered. +INSERT INTO memory_events (event_id, anim_index, completed_at, participants, language) +SELECT event_id, anim_index, completed_at, participants, language +FROM memory_completions mc +WHERE id = ( + SELECT id FROM memory_completions mc2 + WHERE mc2.event_id = mc.event_id + ORDER BY completed_at DESC, id DESC + LIMIT 1 +) +ON CONFLICT(event_id) DO UPDATE SET + anim_index = excluded.anim_index, + completed_at = excluded.completed_at, + participants = excluded.participants, + language = excluded.language +WHERE excluded.completed_at >= memory_events.completed_at; diff --git a/server/src/account.ts b/server/src/account.ts index 2175ebf..5b188ed 100644 --- a/server/src/account.ts +++ b/server/src/account.ts @@ -13,6 +13,19 @@ account.delete("/", async (c) => { c.env.DB.prepare("DELETE FROM user_saves WHERE user_id = ?").bind(userId), c.env.DB.prepare("DELETE FROM user_config WHERE user_id = ?").bind(userId), c.env.DB.prepare("DELETE FROM memory_completions WHERE user_id = ?").bind(userId), + c.env.DB.prepare( + "DELETE FROM memory_events WHERE event_id NOT IN (SELECT event_id FROM memory_completions)" + ), + c.env.DB.prepare( + "INSERT INTO memory_events (event_id, anim_index, completed_at, participants, language) " + + "SELECT event_id, anim_index, completed_at, participants, language FROM memory_completions mc " + + "WHERE id = (SELECT id FROM memory_completions mc2 WHERE mc2.event_id = mc.event_id " + + "ORDER BY completed_at DESC, id DESC LIMIT 1) " + + "ON CONFLICT(event_id) DO UPDATE SET " + + "anim_index = excluded.anim_index, completed_at = excluded.completed_at, " + + "participants = excluded.participants, language = excluded.language " + + "WHERE excluded.completed_at >= memory_events.completed_at" + ), c.env.DB.prepare("UPDATE crash_reports SET user_id = NULL WHERE user_id = ?").bind(userId), c.env.DB.prepare('DELETE FROM "session" WHERE "userId" = ?').bind(userId), c.env.DB.prepare('DELETE FROM "account" WHERE "userId" = ?').bind(userId), diff --git a/server/src/index.ts b/server/src/index.ts index 4ea5034..3c3e577 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -29,7 +29,7 @@ app.all("/api/auth/*", async (c) => { // Public endpoint: all memory event IDs for sitemap generation app.get("/api/sitemap", async (c) => { const results = await c.env.DB.prepare( - "SELECT event_id, MAX(completed_at) AS completed_at FROM memory_completions GROUP BY event_id ORDER BY completed_at DESC" + "SELECT event_id, completed_at FROM memory_events ORDER BY completed_at DESC" ).all<{ event_id: string; completed_at: number }>(); return c.json({ entries: results.results }); @@ -43,7 +43,7 @@ app.get("/api/memory/:eventId", async (c) => { } const result = await c.env.DB.prepare( - "SELECT anim_index, event_id, completed_at, participants, language FROM memory_completions WHERE event_id = ? LIMIT 1" + "SELECT anim_index, event_id, completed_at, participants, language FROM memory_events WHERE event_id = ? LIMIT 1" ) .bind(eventId) .first<{ @@ -77,17 +77,7 @@ app.get("/api/memory/:eventId", async (c) => { // Public endpoint: latest 10 unique memories for the global feed app.get("/api/memories/latest", async (c) => { const results = await c.env.DB.prepare( - `SELECT mc.anim_index, mc.event_id, mc.completed_at, mc.participants, mc.language - FROM memory_completions mc - INNER JOIN ( - SELECT event_id, MAX(completed_at) AS max_completed_at - FROM memory_completions - GROUP BY event_id - ORDER BY max_completed_at DESC - LIMIT 10 - ) latest ON mc.event_id = latest.event_id AND mc.completed_at = latest.max_completed_at - GROUP BY mc.event_id - ORDER BY mc.completed_at DESC` + "SELECT anim_index, event_id, completed_at, participants, language FROM memory_events ORDER BY completed_at DESC, event_id DESC LIMIT 10" ).all<{ anim_index: number; event_id: string; completed_at: number; participants: string; language: string }>(); const entries = results.results.map((r) => {