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 "); + }); +});