From 63e38555c97e743cefc6c8f8a977a6bd5e40c1b4 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 27 Jan 2026 15:33:44 -0500 Subject: [PATCH] sync --- .../migration.sql | 18 ++++--- packages/opencode/src/project/project.sql.ts | 6 +-- packages/opencode/src/session/index.ts | 51 +++++++------------ packages/opencode/src/session/message-v2.ts | 22 +++++--- packages/opencode/src/session/session.sql.ts | 21 ++++---- packages/opencode/src/share/share.sql.ts | 2 + packages/opencode/src/storage/db.ts | 9 +++- .../opencode/src/storage/json-migration.ts | 16 +++--- .../test/storage/json-migration.test.ts | 4 +- 9 files changed, 83 insertions(+), 66 deletions(-) diff --git a/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql b/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql index bc17ef4938..db59eb0213 100644 --- a/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql +++ b/packages/opencode/migration/20260127173238_melted_union_jack/migration.sql @@ -14,7 +14,8 @@ CREATE TABLE `project` ( CREATE TABLE `message` ( `id` text PRIMARY KEY, `session_id` text NOT NULL, - `created_at` integer NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, `data` text NOT NULL, CONSTRAINT `fk_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE ); @@ -23,12 +24,16 @@ CREATE TABLE `part` ( `id` text PRIMARY KEY, `message_id` text NOT NULL, `session_id` text NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, `data` text NOT NULL, CONSTRAINT `fk_part_message_id_message_id_fk` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE ); --> statement-breakpoint CREATE TABLE `permission` ( `project_id` text PRIMARY KEY, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, `data` text NOT NULL, CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE ); @@ -46,10 +51,7 @@ CREATE TABLE `session` ( `summary_deletions` integer, `summary_files` integer, `summary_diffs` text, - `revert_message_id` text, - `revert_part_id` text, - `revert_snapshot` text, - `revert_diff` text, + `revert` text, `permission` text, `time_created` integer NOT NULL, `time_updated` integer NOT NULL, @@ -65,6 +67,8 @@ CREATE TABLE `todo` ( `status` text NOT NULL, `priority` text NOT NULL, `position` integer NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `id`), CONSTRAINT `fk_todo_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE ); @@ -74,6 +78,8 @@ CREATE TABLE `session_share` ( `id` text NOT NULL, `secret` text NOT NULL, `url` text NOT NULL, + `time_created` integer NOT NULL, + `time_updated` integer NOT NULL, CONSTRAINT `fk_session_share_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE ); --> statement-breakpoint @@ -82,4 +88,4 @@ CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoin CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint -CREATE INDEX `todo_session_idx` ON `todo` (`session_id`); \ No newline at end of file +CREATE INDEX `todo_session_idx` ON `todo` (`session_id`); diff --git a/packages/opencode/src/project/project.sql.ts b/packages/opencode/src/project/project.sql.ts index 0f5a856e51..76a8ecdc3b 100644 --- a/packages/opencode/src/project/project.sql.ts +++ b/packages/opencode/src/project/project.sql.ts @@ -1,4 +1,5 @@ -import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core" +import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" +import { Database } from "@/storage/db" export const ProjectTable = sqliteTable("project", { id: text().primaryKey(), @@ -7,8 +8,7 @@ export const ProjectTable = sqliteTable("project", { name: text(), icon_url: text(), icon_color: text(), - time_created: integer().notNull(), - time_updated: integer().notNull(), + ...Database.Timestamps, time_initialized: integer(), sandboxes: text({ mode: "json" }).notNull().$type(), }) diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 4085d99a37..3a18735416 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -54,15 +54,7 @@ export namespace Session { } : undefined const share = row.share_url ? { url: row.share_url } : undefined - const revert = - row.revert_message_id !== null - ? { - messageID: row.revert_message_id, - partID: row.revert_part_id ?? undefined, - snapshot: row.revert_snapshot ?? undefined, - diff: row.revert_diff ?? undefined, - } - : undefined + const revert = row.revert ?? undefined return { id: row.id, slug: row.slug, @@ -98,10 +90,7 @@ export namespace Session { summary_deletions: info.summary?.deletions, summary_files: info.summary?.files, summary_diffs: info.summary?.diffs, - revert_message_id: info.revert?.messageID ?? null, - revert_part_id: info.revert?.partID ?? null, - revert_snapshot: info.revert?.snapshot ?? null, - revert_diff: info.revert?.diff ?? null, + revert: info.revert ?? null, permission: info.permission, time_created: info.time.created, time_updated: info.time.updated, @@ -415,10 +404,7 @@ export namespace Session { const row = db .update(SessionTable) .set({ - revert_message_id: input.revert?.messageID ?? null, - revert_part_id: input.revert?.partID ?? null, - revert_snapshot: input.revert?.snapshot ?? null, - revert_diff: input.revert?.diff ?? null, + revert: input.revert ?? null, summary_additions: input.summary?.additions, summary_deletions: input.summary?.deletions, summary_files: input.summary?.files, @@ -440,10 +426,7 @@ export namespace Session { const row = db .update(SessionTable) .set({ - revert_message_id: null, - revert_part_id: null, - revert_snapshot: null, - revert_diff: null, + revert: null, time_updated: Date.now(), }) .where(eq(SessionTable.id, sessionID)) @@ -544,16 +527,17 @@ export namespace Session { }) export const updateMessage = fn(MessageV2.Info, async (msg) => { - const created_at = msg.role === "user" ? msg.time.created : msg.time.created + const time_created = msg.role === "user" ? msg.time.created : msg.time.created + const { id, sessionID, ...data } = msg Database.use((db) => { db.insert(MessageTable) .values({ - id: msg.id, - session_id: msg.sessionID, - created_at, - data: msg, + id, + session_id: sessionID, + time_created, + data, }) - .onConflictDoUpdate({ target: MessageTable.id, set: { data: msg } }) + .onConflictDoUpdate({ target: MessageTable.id, set: { data } }) .run() Database.effect(() => Bus.publish(MessageV2.Event.Updated, { @@ -620,15 +604,18 @@ export namespace Session { export const updatePart = fn(UpdatePartInput, async (input) => { const part = "delta" in input ? input.part : input const delta = "delta" in input ? input.delta : undefined + const { id, messageID, sessionID, ...data } = part + const time = Date.now() Database.use((db) => { db.insert(PartTable) .values({ - id: part.id, - message_id: part.messageID, - session_id: part.sessionID, - data: part, + id, + message_id: messageID, + session_id: sessionID, + time_created: time, + data, }) - .onConflictDoUpdate({ target: PartTable.id, set: { data: part } }) + .onConflictDoUpdate({ target: PartTable.id, set: { data } }) .run() Database.effect(() => Bus.publish(MessageV2.Event.PartUpdated, { diff --git a/packages/opencode/src/session/message-v2.ts b/packages/opencode/src/session/message-v2.ts index 7d28b912c5..d5a5d17622 100644 --- a/packages/opencode/src/session/message-v2.ts +++ b/packages/opencode/src/session/message-v2.ts @@ -616,7 +616,7 @@ export namespace MessageV2 { .select() .from(MessageTable) .where(eq(MessageTable.session_id, sessionID)) - .orderBy(desc(MessageTable.created_at)) + .orderBy(desc(MessageTable.time_created)) .limit(size) .offset(offset) .all(), @@ -635,15 +635,22 @@ export namespace MessageV2 { .all(), ) for (const row of partRows) { + const part = { + ...row.data, + id: row.id, + sessionID: row.session_id, + messageID: row.message_id, + } as MessageV2.Part const list = partsByMessage.get(row.message_id) - if (list) list.push(row.data) - else partsByMessage.set(row.message_id, [row.data]) + if (list) list.push(part) + else partsByMessage.set(row.message_id, [part]) } } for (const row of rows) { + const info = { ...row.data, id: row.id, sessionID: row.session_id } as MessageV2.Info yield { - info: row.data, + info, parts: partsByMessage.get(row.id) ?? [], } } @@ -657,7 +664,9 @@ export namespace MessageV2 { const rows = Database.use((db) => db.select().from(PartTable).where(eq(PartTable.message_id, message_id)).orderBy(PartTable.id).all(), ) - return rows.map((row) => row.data) + return rows.map( + (row) => ({ ...row.data, id: row.id, sessionID: row.session_id, messageID: row.message_id }) as MessageV2.Part, + ) }) export const get = fn( @@ -668,8 +677,9 @@ export namespace MessageV2 { async (input) => { const row = Database.use((db) => db.select().from(MessageTable).where(eq(MessageTable.id, input.messageID)).get()) if (!row) throw new Error(`Message not found: ${input.messageID}`) + const info = { ...row.data, id: row.id, sessionID: row.session_id } as MessageV2.Info return { - info: row.data, + info, parts: await parts(input.messageID), } }, diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts index 2afaef5aa0..1eab195922 100644 --- a/packages/opencode/src/session/session.sql.ts +++ b/packages/opencode/src/session/session.sql.ts @@ -3,6 +3,10 @@ import { ProjectTable } from "../project/project.sql" import type { MessageV2 } from "./message-v2" import type { Snapshot } from "@/snapshot" import type { PermissionNext } from "@/permission/next" +import { Database } from "@/storage/db" + +type PartData = Omit +type InfoData = Omit export const SessionTable = sqliteTable( "session", @@ -21,13 +25,9 @@ export const SessionTable = sqliteTable( summary_deletions: integer(), summary_files: integer(), summary_diffs: text({ mode: "json" }).$type(), - revert_message_id: text(), - revert_part_id: text(), - revert_snapshot: text(), - revert_diff: text(), + revert: text({ mode: "json" }).$type<{ messageID: string; partID?: string; snapshot?: string; diff?: string }>(), permission: text({ mode: "json" }).$type(), - time_created: integer().notNull(), - time_updated: integer().notNull(), + ...Database.Timestamps, time_compacting: integer(), time_archived: integer(), }, @@ -41,8 +41,8 @@ export const MessageTable = sqliteTable( session_id: text() .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), - created_at: integer().notNull(), - data: text({ mode: "json" }).notNull().$type(), + ...Database.Timestamps, + data: text({ mode: "json" }).notNull().$type(), }, (table) => [index("message_session_idx").on(table.session_id)], ) @@ -55,7 +55,8 @@ export const PartTable = sqliteTable( .notNull() .references(() => MessageTable.id, { onDelete: "cascade" }), session_id: text().notNull(), - data: text({ mode: "json" }).notNull().$type(), + ...Database.Timestamps, + data: text({ mode: "json" }).notNull().$type(), }, (table) => [index("part_message_idx").on(table.message_id), index("part_session_idx").on(table.session_id)], ) @@ -71,6 +72,7 @@ export const TodoTable = sqliteTable( status: text().notNull(), priority: text().notNull(), position: integer().notNull(), + ...Database.Timestamps, }, (table) => [primaryKey({ columns: [table.session_id, table.id] }), index("todo_session_idx").on(table.session_id)], ) @@ -79,5 +81,6 @@ export const PermissionTable = sqliteTable("permission", { project_id: text() .primaryKey() .references(() => ProjectTable.id, { onDelete: "cascade" }), + ...Database.Timestamps, data: text({ mode: "json" }).notNull().$type(), }) diff --git a/packages/opencode/src/share/share.sql.ts b/packages/opencode/src/share/share.sql.ts index 4d9c9290a5..1dcab7b851 100644 --- a/packages/opencode/src/share/share.sql.ts +++ b/packages/opencode/src/share/share.sql.ts @@ -1,5 +1,6 @@ import { sqliteTable, text } from "drizzle-orm/sqlite-core" import { SessionTable } from "../session/session.sql" +import { Database } from "@/storage/db" export const SessionShareTable = sqliteTable("session_share", { session_id: text() @@ -8,4 +9,5 @@ export const SessionShareTable = sqliteTable("session_share", { id: text().notNull(), secret: text().notNull(), url: text().notNull(), + ...Database.Timestamps, }) diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 0beddca8f2..370fa846c2 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -1,7 +1,7 @@ import { Database as BunDatabase } from "bun:sqlite" import { drizzle, type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite" import { migrate } from "drizzle-orm/bun-sqlite/migrator" -import type { SQLiteTransaction } from "drizzle-orm/sqlite-core" +import { integer, type SQLiteTransaction } from "drizzle-orm/sqlite-core" export * from "drizzle-orm" import { Context } from "../util/context" import { lazy } from "../util/lazy" @@ -137,4 +137,11 @@ export namespace Database { throw err } } + + export const Timestamps = { + time_created: integer().notNull(), + time_updated: integer() + .notNull() + .$onUpdate(() => Date.now()), + } } diff --git a/packages/opencode/src/storage/json-migration.ts b/packages/opencode/src/storage/json-migration.ts index 4b235a9dc0..c1236a0f76 100644 --- a/packages/opencode/src/storage/json-migration.ts +++ b/packages/opencode/src/storage/json-migration.ts @@ -118,10 +118,7 @@ export namespace JsonMigration { summary_deletions: data.summary?.deletions ?? null, summary_files: data.summary?.files ?? null, summary_diffs: data.summary?.diffs ?? null, - revert_message_id: data.revert?.messageID ?? null, - revert_part_id: data.revert?.partID ?? null, - revert_snapshot: data.revert?.snapshot ?? null, - revert_diff: data.revert?.diff ?? null, + revert: data.revert ?? null, permission: data.permission ?? null, time_created: data.time?.created ?? Date.now(), time_updated: data.time?.updated ?? Date.now(), @@ -159,11 +156,13 @@ export namespace JsonMigration { stats.errors.push(`message missing id: ${item.file}`) continue } + const { id, sessionID: _, ...rest } = data values.push({ id: data.id, session_id: sessionID, - created_at: data.time?.created ?? Date.now(), - data, + time_created: data.time?.created ?? Date.now(), + time_updated: data.time?.updated ?? Date.now(), + data: rest, }) messageIds.add(data.id) } @@ -191,11 +190,14 @@ export namespace JsonMigration { stats.errors.push(`part missing id or messageID: ${item.file}`) continue } + const { id, messageID, sessionID: _, ...rest } = data values.push({ id: data.id, message_id: data.messageID, session_id: sessionID, - data, + time_created: data.time?.created ?? Date.now(), + time_updated: data.time?.updated ?? Date.now(), + data: rest, }) } if (values.length === 0) continue diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index bc55124002..d72d442358 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -196,11 +196,11 @@ describe("JSON to SQLite migration", () => { const db = drizzle({ client: sqlite }) const messages = db.select().from(MessageTable).all() expect(messages.length).toBe(1) - expect(messages[0].data.id).toBe("msg_test789ghi") + expect(messages[0].id).toBe("msg_test789ghi") const parts = db.select().from(PartTable).all() expect(parts.length).toBe(1) - expect(parts[0].data.id).toBe("prt_testabc123") + expect(parts[0].id).toBe("prt_testabc123") }) test("skips orphaned sessions (no parent project)", async () => {