From b157fc7dfafd4f2e46bafc2d069179012f382325 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:54:38 -0700 Subject: [PATCH] perf(db): index ai_usage_events(status, created_at) for the daily-budget query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sumAiEstimatedNeuronsSince runs SELECT sum(estimated_neurons) WHERE status='ok' AND created_at >= ? — verified via EXPLAIN QUERY PLAN on prod to be a full `SCAN ai_usage_events` (no covering index; existing indexes are on feature/actor). It runs on every AI review/summary (the daily-budget check) and the table grows with each AI call, so the scan cost grows unbounded once AI is enabled. Add the covering index (status equality + created_at range). EXPLAIN confirms the query now SEARCHes via it. Investigation found this is the ONLY un-indexed read in the codebase — the per-webhook/gate hot path (issues/pull_requests/ files/bounties by repo, official_miner_detections by login, settings/repos by PK) is already fully indexed, so the recent prod D1 overload is capacity/DB-size related, not a missing-index problem on the hot path. Migration numbered 0028 to avoid colliding with 0026/0027 reserved by #652. --- migrations/0028_ai_usage_status_index.sql | 6 ++++++ src/db/schema.ts | 3 +++ test/unit/ai-usage-index.test.ts | 23 +++++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 migrations/0028_ai_usage_status_index.sql create mode 100644 test/unit/ai-usage-index.test.ts diff --git a/migrations/0028_ai_usage_status_index.sql b/migrations/0028_ai_usage_status_index.sql new file mode 100644 index 0000000000..8d9a114bfb --- /dev/null +++ b/migrations/0028_ai_usage_status_index.sql @@ -0,0 +1,6 @@ +-- Covers the daily AI-neuron budget query (sumAiEstimatedNeuronsSince): +-- SELECT sum(estimated_neurons) FROM ai_usage_events WHERE status = 'ok' AND created_at >= ? +-- Without this index that aggregate full-scans ai_usage_events, and it runs on every AI review/summary. +-- Numbered 0028 to avoid colliding with migrations 0026/0027 reserved by the AI-review PR (#652); the +-- D1 migration runner applies un-applied files in order and tolerates gaps. +CREATE INDEX IF NOT EXISTS ai_usage_events_status_created_idx ON ai_usage_events (status, created_at); diff --git a/src/db/schema.ts b/src/db/schema.ts index 326a4f028d..9d0baa6827 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -923,5 +923,8 @@ export const aiUsageEvents = sqliteTable( (table) => ({ featureCreated: index("ai_usage_events_feature_created_idx").on(table.feature, table.createdAt), actorCreated: index("ai_usage_events_actor_created_idx").on(table.actor, table.createdAt), + // Covers the daily-budget query (sumAiEstimatedNeuronsSince): WHERE status='ok' AND created_at >= ?. + // Without it that aggregate full-scans ai_usage_events, which runs on every AI review/summary. + statusCreated: index("ai_usage_events_status_created_idx").on(table.status, table.createdAt), }), ); diff --git a/test/unit/ai-usage-index.test.ts b/test/unit/ai-usage-index.test.ts new file mode 100644 index 0000000000..61de4f065f --- /dev/null +++ b/test/unit/ai-usage-index.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { createTestEnv } from "../helpers/d1"; + +describe("ai_usage_events budget index", () => { + it("creates the (status, created_at) index and the budget query uses it (SEARCH, not SCAN)", async () => { + const env = createTestEnv(); + + const idx = await env.DB.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name = ?") + .bind("ai_usage_events_status_created_idx") + .first<{ name: string }>(); + expect(idx?.name).toBe("ai_usage_events_status_created_idx"); + + // sumAiEstimatedNeuronsSince: WHERE status='ok' AND created_at >= ? — must SEARCH via the new index. + const plan = await env.DB.prepare( + "EXPLAIN QUERY PLAN SELECT coalesce(sum(estimated_neurons),0) FROM ai_usage_events WHERE created_at >= ? AND status = 'ok'", + ) + .bind("2026-01-01T00:00:00.000Z") + .all<{ detail: string }>(); + const detail = (plan.results ?? []).map((row) => row.detail).join(" "); + expect(detail).toContain("ai_usage_events_status_created_idx"); + expect(detail).not.toContain("SCAN ai_usage_events "); + }); +});