Split The code is public; the app answers only to the crew.

The first paint groups every match ever played: `standings` costs 42 ms at 50k rows and 171 ms at 200k #218

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

`standings` was a `GROUP BY` over the whole `match` table, paid on every `/`, every `POST /api/match`, and every terminal frame — 171ms at 200k rows and growing without bound.

shipped merged

opened this

standings was a GROUP BY over the whole match table, paid on every /, every POST /api/match, and every terminal frame — 171ms at 200k rows and growing without bound.

What changed: a standing table, one row per identity, kept current by recomputeStanding — called from saveMatch and deleteMatch — which recomputes just that one identity's row from its own matches (WHERE identity = ?1, using the match_by_identity index), never the whole table. standings() is now a plain sorted read of that table, backed by a (kills DESC, damage DESC) index. I chose recompute-from-source over delta-tracking: the SQL is a direct copy of the old GROUP BY narrowed to one identity, so it can't drift from what saveMatch's MAX(...) conflict logic actually stored — no separate delta math to get wrong on the late-post races the issue calls out. Both saveMatch (periodic + final + late-periodic posts) and deleteMatch (the terminal's x) recompute; the aggregate lives in the same SQLite file both the server and src/tty.ts read, so there's no in-process cache to go stale between the two.

Migration: openDatabase creates standing once, populated with the old GROUP BY query, alongside the existing identity/walked_out/map migrations — a database from before this change works on first open, paying the O(n) cost exactly once.

Proof: the same db.test.ts case for "standings group by identity, not name" now reads from standing. A new test seeds 20,000 matches through saveMatch and asserts standings() answers in under 10ms; run against the pre-change code it takes ~53ms, so it's an honest regression test. A second migration test covers an old database with a full match table and no standing table yet.

Left out: matchCount (a plain COUNT(*)) isn't in the issue's scope and I left it alone. No SQL moved out of src/db.ts; no caller (game.ts, tty.ts, server.ts) changed.

bun run check (types + full suite, 773 tests) passes.

Closes #184

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs