diff --git a/turbo/apps/web/src/db/migrations/0051_add_performance_indexes.sql b/turbo/apps/web/src/db/migrations/0051_add_performance_indexes.sql new file mode 100644 index 000000000..dbe010a54 --- /dev/null +++ b/turbo/apps/web/src/db/migrations/0051_add_performance_indexes.sql @@ -0,0 +1,24 @@ +-- Migration: Add performance indexes for high-frequency queries +-- Issue: #1284 - Add missing database indexes for high-frequency query columns + +-- agent_runs: Composite index for user listing with time-based sorting +-- Optimizes: GET /v1/runs, user run listing queries +CREATE INDEX "idx_agent_runs_user_created" ON "agent_runs"("user_id", "created_at" DESC); + +-- agent_runs: Partial index for cron cleanup job +-- Optimizes: /api/cron/cleanup-sandboxes (finds running sandboxes with stale heartbeats) +-- Only indexes rows where status='running', keeping index small +CREATE INDEX "idx_agent_runs_running_heartbeat" ON "agent_runs"("last_heartbeat_at") + WHERE "status" = 'running'; + +-- agent_runs: Partial index for schedule run history +-- Optimizes: schedule-service.ts getRecentRuns query +-- Only indexes scheduled runs (schedule_id IS NOT NULL) +CREATE INDEX "idx_agent_runs_schedule_created" ON "agent_runs"("schedule_id", "created_at" DESC) + WHERE "schedule_id" IS NOT NULL; + +-- agent_sessions: Composite index for findOrCreate pattern +-- Optimizes: agent-session-service.ts findOrCreate() method +-- Covers queries with (userId, agentComposeId, artifactName) combinations +CREATE INDEX "idx_agent_sessions_user_compose_artifact" + ON "agent_sessions"("user_id", "agent_compose_id", "artifact_name"); diff --git a/turbo/apps/web/src/db/schema/agent-run.ts b/turbo/apps/web/src/db/schema/agent-run.ts index c44637c74..662721643 100644 --- a/turbo/apps/web/src/db/schema/agent-run.ts +++ b/turbo/apps/web/src/db/schema/agent-run.ts @@ -5,6 +5,7 @@ import { text, jsonb, timestamp, + index, } from "drizzle-orm/pg-core"; import { agentComposeVersions } from "./agent-compose"; @@ -13,26 +14,41 @@ import { agentComposeVersions } from "./agent-compose"; * Created when developer executes agent via SDK * References immutable compose version for reproducibility */ -export const agentRuns = pgTable("agent_runs", { - id: uuid("id").defaultRandom().primaryKey(), - userId: text("user_id").notNull(), // Clerk user ID - owner of this run - agentComposeVersionId: varchar("agent_compose_version_id", { length: 64 }) - .references(() => agentComposeVersions.id) - .notNull(), - resumedFromCheckpointId: uuid("resumed_from_checkpoint_id"), - // References agent_schedules.id if this run was triggered by a schedule - // No FK constraint to avoid circular dependency with agent_schedules - scheduleId: uuid("schedule_id"), - status: varchar("status", { length: 20 }).notNull(), - prompt: text("prompt").notNull(), - vars: jsonb("vars"), - // Secret names for validation (values never stored - must be provided at runtime) - secretNames: jsonb("secret_names").$type(), - sandboxId: varchar("sandbox_id", { length: 255 }), - result: jsonb("result"), - error: text("error"), - createdAt: timestamp("created_at").defaultNow().notNull(), - startedAt: timestamp("started_at"), - completedAt: timestamp("completed_at"), - lastHeartbeatAt: timestamp("last_heartbeat_at"), -}); +export const agentRuns = pgTable( + "agent_runs", + { + id: uuid("id").defaultRandom().primaryKey(), + userId: text("user_id").notNull(), // Clerk user ID - owner of this run + agentComposeVersionId: varchar("agent_compose_version_id", { length: 64 }) + .references(() => agentComposeVersions.id) + .notNull(), + resumedFromCheckpointId: uuid("resumed_from_checkpoint_id"), + // References agent_schedules.id if this run was triggered by a schedule + // No FK constraint to avoid circular dependency with agent_schedules + scheduleId: uuid("schedule_id"), + status: varchar("status", { length: 20 }).notNull(), + prompt: text("prompt").notNull(), + vars: jsonb("vars"), + // Secret names for validation (values never stored - must be provided at runtime) + secretNames: jsonb("secret_names").$type(), + sandboxId: varchar("sandbox_id", { length: 255 }), + result: jsonb("result"), + error: text("error"), + createdAt: timestamp("created_at").defaultNow().notNull(), + startedAt: timestamp("started_at"), + completedAt: timestamp("completed_at"), + lastHeartbeatAt: timestamp("last_heartbeat_at"), + }, + (table) => [ + // Composite index for user listing with time-based sorting + index("idx_agent_runs_user_created").on(table.userId, table.createdAt), + // Partial index for cron cleanup (only running status) + index("idx_agent_runs_running_heartbeat") + .on(table.lastHeartbeatAt) + .where("status = 'running'" as never), + // Partial index for schedule history (only scheduled runs) + index("idx_agent_runs_schedule_created") + .on(table.scheduleId, table.createdAt) + .where("schedule_id IS NOT NULL" as never), + ], +); diff --git a/turbo/apps/web/src/db/schema/agent-session.ts b/turbo/apps/web/src/db/schema/agent-session.ts index 77da91f93..6c1df4f12 100644 --- a/turbo/apps/web/src/db/schema/agent-session.ts +++ b/turbo/apps/web/src/db/schema/agent-session.ts @@ -5,6 +5,7 @@ import { text, timestamp, jsonb, + index, } from "drizzle-orm/pg-core"; import { agentComposes } from "./agent-compose"; import { conversations } from "./conversation"; @@ -20,24 +21,35 @@ import { conversations } from "./conversation"; * - vars: Template variables for compose expansion * - secretNames: Secret names for validation (values never stored) */ -export const agentSessions = pgTable("agent_sessions", { - id: uuid("id").defaultRandom().primaryKey(), - userId: text("user_id").notNull(), - agentComposeId: uuid("agent_compose_id") - .references(() => agentComposes.id, { onDelete: "cascade" }) - .notNull(), - // Immutable compose version ID (SHA-256 hash) fixed at session creation - // If null (legacy sessions), resolveSession falls back to HEAD version - agentComposeVersionId: varchar("agent_compose_version_id", { length: 255 }), - conversationId: uuid("conversation_id").references(() => conversations.id, { - onDelete: "set null", - }), - artifactName: varchar("artifact_name", { length: 255 }), - vars: jsonb("vars").$type>(), - // Secret names for validation (values never stored - must be provided at runtime) - secretNames: jsonb("secret_names").$type(), - // Volume versions snapshot at session creation for reproducibility - volumeVersions: jsonb("volume_versions").$type>(), - createdAt: timestamp("created_at").defaultNow().notNull(), - updatedAt: timestamp("updated_at").defaultNow().notNull(), -}); +export const agentSessions = pgTable( + "agent_sessions", + { + id: uuid("id").defaultRandom().primaryKey(), + userId: text("user_id").notNull(), + agentComposeId: uuid("agent_compose_id") + .references(() => agentComposes.id, { onDelete: "cascade" }) + .notNull(), + // Immutable compose version ID (SHA-256 hash) fixed at session creation + // If null (legacy sessions), resolveSession falls back to HEAD version + agentComposeVersionId: varchar("agent_compose_version_id", { length: 255 }), + conversationId: uuid("conversation_id").references(() => conversations.id, { + onDelete: "set null", + }), + artifactName: varchar("artifact_name", { length: 255 }), + vars: jsonb("vars").$type>(), + // Secret names for validation (values never stored - must be provided at runtime) + secretNames: jsonb("secret_names").$type(), + // Volume versions snapshot at session creation for reproducibility + volumeVersions: jsonb("volume_versions").$type>(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), + }, + (table) => [ + // Composite index for findOrCreate pattern + index("idx_agent_sessions_user_compose_artifact").on( + table.userId, + table.agentComposeId, + table.artifactName, + ), + ], +);