From f003d570f6529b183f990d81fd67a958f372846a Mon Sep 17 00:00:00 2001 From: literally-dan <9935415+literally-dan@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:59:49 +0100 Subject: [PATCH] fix(storage): index session list queries --- packages/core/schema.json | 221 ++++++++++++++---- packages/core/src/database/migration.gen.ts | 1 + .../20260724103001_session_list_indexes.ts | 16 ++ packages/core/src/database/schema.gen.ts | 5 +- packages/core/src/session/sql.ts | 5 +- packages/core/test/database-migration.test.ts | 50 ++++ 6 files changed, 246 insertions(+), 52 deletions(-) create mode 100644 packages/core/src/database/migration/20260724103001_session_list_indexes.ts diff --git a/packages/core/schema.json b/packages/core/schema.json index d0eeeebd5c41..97a63764aa73 100644 --- a/packages/core/schema.json +++ b/packages/core/schema.json @@ -1,8 +1,10 @@ { "version": "7", "dialect": "sqlite", - "id": "f14a9b18-8207-487e-a3d3-227e629ba9ad", - "prevIds": ["169a0f0f-d58f-479f-b024-fa1c7b9a09db"], + "id": "40d5fbe0-94c3-434a-8751-a3e796d88d57", + "prevIds": [ + "f14a9b18-8207-487e-a3d3-227e629ba9ad" + ], "ddl": [ { "name": "workspace", @@ -1481,9 +1483,13 @@ "table": "session_share" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1492,9 +1498,13 @@ "table": "workspace" }, { - "columns": ["active_account_id"], + "columns": [ + "active_account_id" + ], "tableTo": "account", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "SET NULL", "nameExplicit": false, @@ -1503,9 +1513,13 @@ "table": "account_state" }, { - "columns": ["aggregate_id"], + "columns": [ + "aggregate_id" + ], "tableTo": "event_sequence", - "columnsTo": ["aggregate_id"], + "columnsTo": [ + "aggregate_id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1514,9 +1528,13 @@ "table": "event" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1525,9 +1543,13 @@ "table": "permission" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1536,9 +1558,13 @@ "table": "project_directory" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1547,9 +1573,13 @@ "table": "message" }, { - "columns": ["message_id"], + "columns": [ + "message_id" + ], "tableTo": "message", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1558,9 +1588,13 @@ "table": "part" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1569,9 +1603,13 @@ "table": "session_context_epoch" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1580,9 +1618,13 @@ "table": "session_input" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1591,9 +1633,13 @@ "table": "session_message" }, { - "columns": ["project_id"], + "columns": [ + "project_id" + ], "tableTo": "project", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1602,9 +1648,13 @@ "table": "session" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1613,9 +1663,13 @@ "table": "todo" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "tableTo": "session", - "columnsTo": ["id"], + "columnsTo": [ + "id" + ], "onUpdate": "NO ACTION", "onDelete": "CASCADE", "nameExplicit": false, @@ -1624,133 +1678,174 @@ "table": "session_share" }, { - "columns": ["email", "url"], + "columns": [ + "email", + "url" + ], "nameExplicit": false, "name": "control_account_pk", "entityType": "pks", "table": "control_account" }, { - "columns": ["project_id", "directory"], + "columns": [ + "project_id", + "directory" + ], "nameExplicit": false, "name": "project_directory_pk", "entityType": "pks", "table": "project_directory" }, { - "columns": ["session_id", "position"], + "columns": [ + "session_id", + "position" + ], "nameExplicit": false, "name": "todo_pk", "entityType": "pks", "table": "todo" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "workspace_pk", "table": "workspace", "entityType": "pks" }, { - "columns": ["name"], + "columns": [ + "name" + ], "nameExplicit": false, "name": "data_migration_pk", "table": "data_migration", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "account_state_pk", "table": "account_state", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "account_pk", "table": "account", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "credential_pk", "table": "credential", "entityType": "pks" }, { - "columns": ["aggregate_id"], + "columns": [ + "aggregate_id" + ], "nameExplicit": false, "name": "event_sequence_pk", "table": "event_sequence", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "event_pk", "table": "event", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "permission_pk", "table": "permission", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "project_pk", "table": "project", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "message_pk", "table": "message", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "part_pk", "table": "part", "entityType": "pks" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "nameExplicit": false, "name": "session_context_epoch_pk", "table": "session_context_epoch", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "session_input_pk", "table": "session_input", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "session_message_pk", "table": "session_message", "entityType": "pks" }, { - "columns": ["id"], + "columns": [ + "id" + ], "nameExplicit": false, "name": "session_pk", "table": "session", "entityType": "pks" }, { - "columns": ["session_id"], + "columns": [ + "session_id" + ], "nameExplicit": false, "name": "session_share_pk", "table": "session_share", @@ -2015,6 +2110,10 @@ { "value": "project_id", "isExpression": false + }, + { + "value": "time_updated", + "isExpression": false } ], "isUnique": false, @@ -2043,6 +2142,14 @@ { "value": "parent_id", "isExpression": false + }, + { + "value": "time_updated", + "isExpression": false + }, + { + "value": "id", + "isExpression": false } ], "isUnique": false, @@ -2052,6 +2159,24 @@ "entityType": "indexes", "table": "session" }, + { + "columns": [ + { + "value": "time_updated", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_time_updated_idx", + "entityType": "indexes", + "table": "session" + }, { "columns": [ { @@ -2068,4 +2193,4 @@ } ], "renames": [] -} +} \ No newline at end of file diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts index e6ea4eaa1477..ef1592339487 100644 --- a/packages/core/src/database/migration.gen.ts +++ b/packages/core/src/database/migration.gen.ts @@ -40,5 +40,6 @@ export const migrations = ( import("./migration/20260622142730_simplify_session_context_epoch"), import("./migration/20260622170816_reset_v2_session_state"), import("./migration/20260622202450_simplify_session_input"), + import("./migration/20260724103001_session_list_indexes"), ]) ).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration/20260724103001_session_list_indexes.ts b/packages/core/src/database/migration/20260724103001_session_list_indexes.ts new file mode 100644 index 000000000000..02f07f38cd45 --- /dev/null +++ b/packages/core/src/database/migration/20260724103001_session_list_indexes.ts @@ -0,0 +1,16 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +export default { + id: "20260724103001_session_list_indexes", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP INDEX IF EXISTS "session_project_idx";`) + yield* tx.run(`DROP INDEX IF EXISTS "session_parent_idx";`) + yield* tx.run(`DROP INDEX IF EXISTS "session_time_updated_idx";`) + yield* tx.run(`CREATE INDEX "session_project_idx" ON "session" ("project_id","time_updated");`) + yield* tx.run(`CREATE INDEX "session_parent_idx" ON "session" ("parent_id","time_updated","id");`) + yield* tx.run(`CREATE INDEX "session_time_updated_idx" ON "session" ("time_updated","id");`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/schema.gen.ts b/packages/core/src/database/schema.gen.ts index ed60fde6c55f..3e0372ba8aaf 100644 --- a/packages/core/src/database/schema.gen.ts +++ b/packages/core/src/database/schema.gen.ts @@ -265,9 +265,10 @@ export default { `CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`, ) yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`) - yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`) + yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`,\`time_updated\`);`) yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`) - yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`);`) + yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`,\`time_updated\`,\`id\`);`) + yield* tx.run(`CREATE INDEX \`session_time_updated_idx\` ON \`session\` (\`time_updated\`,\`id\`);`) yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`) }) }, diff --git a/packages/core/src/session/sql.ts b/packages/core/src/session/sql.ts index 264a1d2cca0a..4d8c8f169464 100644 --- a/packages/core/src/session/sql.ts +++ b/packages/core/src/session/sql.ts @@ -59,9 +59,10 @@ export const SessionTable = sqliteTable( time_archived: integer(), }, (table) => [ - index("session_project_idx").on(table.project_id), + index("session_project_idx").on(table.project_id, table.time_updated), index("session_workspace_idx").on(table.workspace_id), - index("session_parent_idx").on(table.parent_id), + index("session_parent_idx").on(table.parent_id, table.time_updated, table.id), + index("session_time_updated_idx").on(table.time_updated, table.id), ], ) diff --git a/packages/core/test/database-migration.test.ts b/packages/core/test/database-migration.test.ts index b381cc7418a3..86164b57a158 100644 --- a/packages/core/test/database-migration.test.ts +++ b/packages/core/test/database-migration.test.ts @@ -8,6 +8,7 @@ import { Effect, Layer } from "effect" import { eq, inArray, sql } from "drizzle-orm" import { DatabaseMigration } from "@opencode-ai/core/database/migration" import { migrations } from "@opencode-ai/core/database/migration.gen" +import sessionListIndexesMigration from "@opencode-ai/core/database/migration/20260724103001_session_list_indexes" import sessionUsageMigration from "@opencode-ai/core/database/migration/20260510033149_session_usage" import normalizeStoragePathsMigration from "@opencode-ai/core/database/migration/20260601010001_normalize_storage_paths" import sessionMessageProjectionOrderMigration from "@opencode-ai/core/database/migration/20260603040000_session_message_projection_order" @@ -60,6 +61,55 @@ describe("DatabaseMigration", () => { }, 30_000) } + test("updates session list indexes", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.run( + sql`CREATE TABLE session (id text PRIMARY KEY, project_id text NOT NULL, parent_id text, time_updated integer NOT NULL)`, + ) + yield* db.run(sql`CREATE INDEX session_project_idx ON session (project_id)`) + yield* db.run(sql`CREATE INDEX session_parent_idx ON session (parent_id)`) + yield* db.run(sql`CREATE INDEX session_time_updated_idx ON session (time_updated)`) + + yield* DatabaseMigration.applyOnly(db, [sessionListIndexesMigration]) + + expect( + (yield* db.all<{ name: string }>(sql`PRAGMA index_info(session_project_idx)`)).map((column) => column.name), + ).toEqual(["project_id", "time_updated"]) + expect( + (yield* db.all<{ name: string }>(sql`PRAGMA index_info(session_parent_idx)`)).map((column) => column.name), + ).toEqual(["parent_id", "time_updated", "id"]) + expect( + (yield* db.all<{ name: string }>(sql`PRAGMA index_info(session_time_updated_idx)`)).map( + (column) => column.name, + ), + ).toEqual(["time_updated", "id"]) + }), + ) + }) + + test("creates the session update index when absent", async () => { + await run( + Effect.gen(function* () { + const db = yield* makeDb + yield* db.run( + sql`CREATE TABLE session (id text PRIMARY KEY, project_id text NOT NULL, parent_id text, time_updated integer NOT NULL)`, + ) + yield* db.run(sql`CREATE INDEX session_project_idx ON session (project_id)`) + yield* db.run(sql`CREATE INDEX session_parent_idx ON session (parent_id)`) + + yield* DatabaseMigration.applyOnly(db, [sessionListIndexesMigration]) + + expect( + (yield* db.all<{ name: string }>(sql`PRAGMA index_info(session_time_updated_idx)`)).map( + (column) => column.name, + ), + ).toEqual(["time_updated", "id"]) + }), + ) + }) + test("applies tracked migrations to an empty database", async () => { await run( Effect.gen(function* () {