Skip to content

Commit 0604aef

Browse files
committed
feat(schema): add agent_threads and agent_messages tables
User-scoped chat threads with FK to user table and cascade delete. Handles existing tables from Go API by using ALTER TABLE ADD COLUMN IF NOT EXISTS for the new user_id column.
1 parent 9771ed2 commit 0604aef

3 files changed

Lines changed: 97 additions & 0 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-- Add user_id column to existing agent_threads table (created by Go API)
2+
-- For existing rows, we set a placeholder that must be backfilled
3+
ALTER TABLE "agent_threads" ADD COLUMN IF NOT EXISTS "user_id" uuid;
4+
--> statement-breakpoint
5+
-- Backfill existing threads: assign to the first user in the org from metadata
6+
UPDATE "agent_threads" SET "user_id" = (
7+
SELECT u.id FROM "user" u LIMIT 1
8+
) WHERE "user_id" IS NULL;
9+
--> statement-breakpoint
10+
-- Now make it NOT NULL
11+
ALTER TABLE "agent_threads" ALTER COLUMN "user_id" SET NOT NULL;
12+
--> statement-breakpoint
13+
DO $$ BEGIN
14+
ALTER TABLE "agent_threads" ADD CONSTRAINT "agent_threads_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
15+
EXCEPTION
16+
WHEN duplicate_object THEN null;
17+
END $$;
18+
--> statement-breakpoint
19+
CREATE INDEX IF NOT EXISTS "idx_agent_threads_user" ON "agent_threads" USING btree ("user_id");
20+
--> statement-breakpoint
21+
-- Clean orphaned messages whose thread no longer exists
22+
DELETE FROM "agent_messages" WHERE "thread_id" NOT IN (SELECT "id" FROM "agent_threads");
23+
--> statement-breakpoint
24+
-- Ensure agent_messages has FK to agent_threads
25+
DO $$ BEGIN
26+
ALTER TABLE "agent_messages" ADD CONSTRAINT "agent_messages_thread_id_agent_threads_id_fk" FOREIGN KEY ("thread_id") REFERENCES "public"."agent_threads"("id") ON DELETE cascade ON UPDATE no action;
27+
EXCEPTION
28+
WHEN duplicate_object THEN null;
29+
END $$;
30+
--> statement-breakpoint
31+
CREATE INDEX IF NOT EXISTS "idx_agent_messages_thread_seq" ON "agent_messages" USING btree ("thread_id","seq");

drizzle/meta/_journal.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,13 @@
309309
"when": 1776706898612,
310310
"tag": "0043_flippant_stark_industries",
311311
"breakpoints": true
312+
},
313+
{
314+
"idx": 44,
315+
"version": "7",
316+
"when": 1778436000000,
317+
"tag": "0044_agent_threads_messages",
318+
"breakpoints": true
312319
}
313320
]
314321
}

src/db/schema.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2308,3 +2308,62 @@ export const cliInstallations = pgTable(
23082308
index("idx_cli_installations_event_type").on(table.eventType),
23092309
],
23102310
);
2311+
2312+
// ─── Agent Chat Threads & Messages ──────────────────────────────────────────
2313+
2314+
export const agentThreads = pgTable(
2315+
"agent_threads",
2316+
{
2317+
id: text("id").primaryKey(),
2318+
userId: uuid("user_id")
2319+
.notNull()
2320+
.references(() => user.id, { onDelete: "cascade" }),
2321+
title: text("title").notNull().default(""),
2322+
metadata: text("metadata").default("{}"),
2323+
createdAt: timestamp("created_at", { withTimezone: true })
2324+
.defaultNow()
2325+
.notNull(),
2326+
updatedAt: timestamp("updated_at", { withTimezone: true })
2327+
.defaultNow()
2328+
.notNull(),
2329+
},
2330+
(table) => [
2331+
index("idx_agent_threads_user").on(table.userId),
2332+
],
2333+
);
2334+
2335+
export const agentThreadsRelations = relations(agentThreads, ({ one, many }) => ({
2336+
user: one(user, {
2337+
fields: [agentThreads.userId],
2338+
references: [user.id],
2339+
}),
2340+
messages: many(agentMessages),
2341+
}));
2342+
2343+
export const agentMessages = pgTable(
2344+
"agent_messages",
2345+
{
2346+
id: text("id").primaryKey(),
2347+
threadId: text("thread_id")
2348+
.notNull()
2349+
.references(() => agentThreads.id, { onDelete: "cascade" }),
2350+
role: text("role").notNull(),
2351+
content: text("content").notNull().default(""),
2352+
toolCalls: text("tool_calls").default("[]"),
2353+
toolCallId: text("tool_call_id").default(""),
2354+
createdAt: timestamp("created_at", { withTimezone: true })
2355+
.defaultNow()
2356+
.notNull(),
2357+
seq: integer("seq").notNull(),
2358+
},
2359+
(table) => [
2360+
index("idx_agent_messages_thread_seq").on(table.threadId, table.seq),
2361+
],
2362+
);
2363+
2364+
export const agentMessagesRelations = relations(agentMessages, ({ one }) => ({
2365+
thread: one(agentThreads, {
2366+
fields: [agentMessages.threadId],
2367+
references: [agentThreads.id],
2368+
}),
2369+
}));

0 commit comments

Comments
 (0)