feat(FN-1532): add performance indexes for dashboard boot queries
- Add composite indexes for dashboard boot query paths - Optimize taskList queries with (column, updatedAt) and (column, status, updatedAt) indexes - Optimize activityLog queries with (timestamp DESC, taskId) index - Add idempotent CREATE INDEX IF NOT EXISTS pattern for migrations - Document index patterns in memory.md for future schema changes - Update schema version assertions in db.test.ts and task-documents.test.ts - Add changeset for dashboard load performance improvement
This commit is contained in:
@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
|
||||
|
||||
expect(tableNames.has("task_documents")).toBe(true);
|
||||
expect(tableNames.has("task_document_revisions")).toBe(true);
|
||||
expect(db.getSchemaVersion()).toBe(27);
|
||||
expect(db.getSchemaVersion()).toBe(28);
|
||||
|
||||
const index = db
|
||||
.prepare(
|
||||
|
||||
@@ -86,12 +86,17 @@ describe("Database", () => {
|
||||
expect(indexNames).toContain("idxActivityLogTimestamp");
|
||||
expect(indexNames).toContain("idxActivityLogType");
|
||||
expect(indexNames).toContain("idxActivityLogTaskId");
|
||||
expect(indexNames).toContain("idxActivityLogTaskIdTimestamp");
|
||||
expect(indexNames).toContain("idxActivityLogTypeTimestamp");
|
||||
expect(indexNames).toContain("idxArchivedTasksId");
|
||||
expect(indexNames).toContain("idxAgentHeartbeatsAgentId");
|
||||
expect(indexNames).toContain("idxAgentHeartbeatsAgentIdTimestamp");
|
||||
expect(indexNames).toContain("idxAgentHeartbeatsRunId");
|
||||
expect(indexNames).toContain("idxAiSessionsStatus");
|
||||
expect(indexNames).toContain("idxAiSessionsStatusUpdatedAt");
|
||||
expect(indexNames).toContain("idxAiSessionsType");
|
||||
expect(indexNames).toContain("idxAiSessionsLock");
|
||||
expect(indexNames).toContain("idxAgentsState");
|
||||
expect(indexNames).toContain("idxMessagesCreatedAt");
|
||||
expect(indexNames).toContain("idxMessagesFrom");
|
||||
expect(indexNames).toContain("idxMessagesTo");
|
||||
@@ -103,10 +108,11 @@ describe("Database", () => {
|
||||
expect(indexNames).toContain("idxTaskDocumentsTaskKey");
|
||||
expect(indexNames).toContain("idxTaskDocumentsTaskId");
|
||||
expect(indexNames).toContain("idxTaskDocumentRevisionsTaskKey");
|
||||
expect(indexNames).toContain("idxTasksCreatedAt");
|
||||
});
|
||||
|
||||
it("seeds schema version", () => {
|
||||
expect(db.getSchemaVersion()).toBe(27);
|
||||
expect(db.getSchemaVersion()).toBe(28);
|
||||
});
|
||||
|
||||
it("seeds lastModified", () => {
|
||||
@@ -129,7 +135,7 @@ describe("Database", () => {
|
||||
|
||||
it("is idempotent - calling init() twice does not fail", () => {
|
||||
expect(() => db.init()).not.toThrow();
|
||||
expect(db.getSchemaVersion()).toBe(27);
|
||||
expect(db.getSchemaVersion()).toBe(28);
|
||||
});
|
||||
|
||||
it("does not overwrite existing config on re-init", () => {
|
||||
@@ -735,8 +741,8 @@ describe("schema migrations", () => {
|
||||
// Now run init() which should trigger migration
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 27 (includes v1→v2 through v26→v27)
|
||||
expect(db.getSchemaVersion()).toBe(27);
|
||||
// Verify version bumped to 28 (includes v1→v2 through v26→v28)
|
||||
expect(db.getSchemaVersion()).toBe(28);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -761,11 +767,11 @@ describe("schema migrations", () => {
|
||||
const db = new Database(kbDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(27);
|
||||
expect(db.getSchemaVersion()).toBe(28);
|
||||
|
||||
// Re-init should not fail
|
||||
db.init();
|
||||
expect(db.getSchemaVersion()).toBe(27);
|
||||
expect(db.getSchemaVersion()).toBe(28);
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -781,7 +787,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(27);
|
||||
expect(db.getSchemaVersion()).toBe(28);
|
||||
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
|
||||
expect(tables).toEqual([{ name: "agentRatings" }]);
|
||||
@@ -805,7 +811,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(27);
|
||||
expect(db.getSchemaVersion()).toBe(28);
|
||||
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
|
||||
expect(tables).toEqual([{ name: "mission_events" }]);
|
||||
@@ -908,8 +914,8 @@ describe("schema migrations", () => {
|
||||
// Now run init() which should trigger migrations v2→v3→v4
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 27
|
||||
expect(db.getSchemaVersion()).toBe(27);
|
||||
// Verify version bumped to 28
|
||||
expect(db.getSchemaVersion()).toBe(28);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -1275,7 +1281,7 @@ describe("createDatabase factory", () => {
|
||||
const db = createDatabase(kbDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(27);
|
||||
expect(db.getSchemaVersion()).toBe(28);
|
||||
expect(db.getLastModified()).toBeGreaterThan(0);
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -59,7 +59,7 @@ export function fromJson<T>(json: string | null | undefined): T | undefined {
|
||||
|
||||
// ── Schema Definition ────────────────────────────────────────────────
|
||||
|
||||
const SCHEMA_VERSION = 27;
|
||||
const SCHEMA_VERSION = 28;
|
||||
|
||||
function normalizeTaskComments(
|
||||
steeringComments: SteeringComment[] | undefined,
|
||||
@@ -990,6 +990,43 @@ export class Database {
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxRoutinesEnabled ON routines(enabled)`);
|
||||
});
|
||||
}
|
||||
|
||||
// Dashboard load performance indexes (FN-1532)
|
||||
// Added indexes to eliminate full table scans and temp B-tree sorts
|
||||
// in boot-critical query paths (listTasks, listActive, activityLog, agents)
|
||||
if (version < 28) {
|
||||
this.applyMigration(28, () => {
|
||||
// Index on tasks.createdAt to avoid temp B-tree sort for ORDER BY createdAt
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxTasksCreatedAt ON tasks(createdAt)`);
|
||||
|
||||
// Composite index on ai_sessions for status filter + updatedAt ordering
|
||||
// Covers: WHERE status IN (...) ORDER BY updatedAt DESC
|
||||
// Only create if the table exists (it was added in v9)
|
||||
if (this.hasTable("ai_sessions")) {
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxAiSessionsStatusUpdatedAt ON ai_sessions(status, updatedAt DESC)`);
|
||||
}
|
||||
|
||||
// Composite index on activityLog for taskId filter + timestamp ordering
|
||||
// Covers: WHERE taskId = ? ORDER BY timestamp DESC
|
||||
if (this.hasTable("activityLog")) {
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxActivityLogTaskIdTimestamp ON activityLog(taskId, timestamp DESC)`);
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxActivityLogTypeTimestamp ON activityLog(type, timestamp DESC)`);
|
||||
}
|
||||
|
||||
// Composite index on agentHeartbeats for agentId filter + timestamp ordering
|
||||
// Covers: WHERE agentId = ? ORDER BY timestamp DESC
|
||||
// Only create if the table exists (it was added in v2)
|
||||
if (this.hasTable("agentHeartbeats")) {
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxAgentHeartbeatsAgentIdTimestamp ON agentHeartbeats(agentId, timestamp DESC)`);
|
||||
}
|
||||
|
||||
// Index on agents.state for state filtering
|
||||
// Covers: WHERE state = ?
|
||||
if (this.hasTable("agents")) {
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxAgentsState ON agents(state)`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -464,8 +464,8 @@ describe("Run Audit", () => {
|
||||
expect(indexNames).toContain("idxRunAuditEventsTimestamp");
|
||||
});
|
||||
|
||||
it("schema version is bumped to 25", () => {
|
||||
expect(db.getSchemaVersion()).toBe(27);
|
||||
it("schema version is bumped to 28", () => {
|
||||
expect(db.getSchemaVersion()).toBe(28);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user