From cd174d8cbaefe8c0821400c197f133c5af38f277 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 27 Jan 2026 16:57:51 -0500 Subject: [PATCH] sync --- packages/opencode/src/project/project.sql.ts | 4 +- packages/opencode/src/session/session.sql.ts | 12 +- packages/opencode/src/share/share.sql.ts | 4 +- packages/opencode/src/storage/db.ts | 11 +- packages/opencode/src/storage/schema.sql.ts | 10 ++ .../test/storage/json-migration.test.ts | 122 ++++++++++++++++++ 6 files changed, 143 insertions(+), 20 deletions(-) create mode 100644 packages/opencode/src/storage/schema.sql.ts diff --git a/packages/opencode/src/project/project.sql.ts b/packages/opencode/src/project/project.sql.ts index 76a8ecdc3b..7c4dc94cc4 100644 --- a/packages/opencode/src/project/project.sql.ts +++ b/packages/opencode/src/project/project.sql.ts @@ -1,5 +1,5 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" -import { Database } from "@/storage/db" +import { Timestamps } from "@/storage/schema.sql" export const ProjectTable = sqliteTable("project", { id: text().primaryKey(), @@ -8,7 +8,7 @@ export const ProjectTable = sqliteTable("project", { name: text(), icon_url: text(), icon_color: text(), - ...Database.Timestamps, + ...Timestamps, time_initialized: integer(), sandboxes: text({ mode: "json" }).notNull().$type(), }) diff --git a/packages/opencode/src/session/session.sql.ts b/packages/opencode/src/session/session.sql.ts index 1eab195922..ea7c1e32a3 100644 --- a/packages/opencode/src/session/session.sql.ts +++ b/packages/opencode/src/session/session.sql.ts @@ -3,7 +3,7 @@ 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" +import { Timestamps } from "@/storage/schema.sql" type PartData = Omit type InfoData = Omit @@ -27,7 +27,7 @@ export const SessionTable = sqliteTable( summary_diffs: text({ mode: "json" }).$type(), revert: text({ mode: "json" }).$type<{ messageID: string; partID?: string; snapshot?: string; diff?: string }>(), permission: text({ mode: "json" }).$type(), - ...Database.Timestamps, + ...Timestamps, time_compacting: integer(), time_archived: integer(), }, @@ -41,7 +41,7 @@ export const MessageTable = sqliteTable( session_id: text() .notNull() .references(() => SessionTable.id, { onDelete: "cascade" }), - ...Database.Timestamps, + ...Timestamps, data: text({ mode: "json" }).notNull().$type(), }, (table) => [index("message_session_idx").on(table.session_id)], @@ -55,7 +55,7 @@ export const PartTable = sqliteTable( .notNull() .references(() => MessageTable.id, { onDelete: "cascade" }), session_id: text().notNull(), - ...Database.Timestamps, + ...Timestamps, data: text({ mode: "json" }).notNull().$type(), }, (table) => [index("part_message_idx").on(table.message_id), index("part_session_idx").on(table.session_id)], @@ -72,7 +72,7 @@ export const TodoTable = sqliteTable( status: text().notNull(), priority: text().notNull(), position: integer().notNull(), - ...Database.Timestamps, + ...Timestamps, }, (table) => [primaryKey({ columns: [table.session_id, table.id] }), index("todo_session_idx").on(table.session_id)], ) @@ -81,6 +81,6 @@ export const PermissionTable = sqliteTable("permission", { project_id: text() .primaryKey() .references(() => ProjectTable.id, { onDelete: "cascade" }), - ...Database.Timestamps, + ...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 1dcab7b851..268d41a6f6 100644 --- a/packages/opencode/src/share/share.sql.ts +++ b/packages/opencode/src/share/share.sql.ts @@ -1,6 +1,6 @@ import { sqliteTable, text } from "drizzle-orm/sqlite-core" import { SessionTable } from "../session/session.sql" -import { Database } from "@/storage/db" +import { Timestamps } from "@/storage/schema.sql" export const SessionShareTable = sqliteTable("session_share", { session_id: text() @@ -9,5 +9,5 @@ export const SessionShareTable = sqliteTable("session_share", { id: text().notNull(), secret: text().notNull(), url: text().notNull(), - ...Database.Timestamps, + ...Timestamps, }) diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 54a8acc34d..738e7c6025 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 { integer, type SQLiteTransaction } from "drizzle-orm/sqlite-core" +import { type SQLiteTransaction } from "drizzle-orm/sqlite-core" export * from "drizzle-orm" import { Context } from "../util/context" import { lazy } from "../util/lazy" @@ -137,13 +137,4 @@ export namespace Database { throw err } } - - export const Timestamps = { - time_created: integer() - .notNull() - .$default(() => Date.now()), - time_updated: integer() - .notNull() - .$onUpdate(() => Date.now()), - } } diff --git a/packages/opencode/src/storage/schema.sql.ts b/packages/opencode/src/storage/schema.sql.ts new file mode 100644 index 0000000000..ead3518dee --- /dev/null +++ b/packages/opencode/src/storage/schema.sql.ts @@ -0,0 +1,10 @@ +import { integer } from "drizzle-orm/sqlite-core" + +export const Timestamps = { + time_created: integer() + .notNull() + .$default(() => Date.now()), + time_updated: integer() + .notNull() + .$onUpdate(() => Date.now()), +} diff --git a/packages/opencode/test/storage/json-migration.test.ts b/packages/opencode/test/storage/json-migration.test.ts index d72d442358..09578ebe8c 100644 --- a/packages/opencode/test/storage/json-migration.test.ts +++ b/packages/opencode/test/storage/json-migration.test.ts @@ -11,6 +11,7 @@ import { Global } from "../../src/global" import { ProjectTable } from "../../src/project/project.sql" import { Project } from "../../src/project/project" import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql" +import { SessionShareTable } from "../../src/share/share.sql" // Test fixtures const fixtures = { @@ -240,4 +241,125 @@ describe("JSON to SQLite migration", () => { const projects = db.select().from(ProjectTable).all() expect(projects.length).toBe(1) // Still only 1 due to onConflictDoNothing }) + + test("migrates todos", async () => { + // First create the project and session + await Bun.write( + path.join(storageDir, "project", "proj_test123abc.json"), + JSON.stringify({ + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }), + ) + await Bun.write( + path.join(storageDir, "session", "proj_test123abc", "ses_test456def.json"), + JSON.stringify({ ...fixtures.session }), + ) + + // Create todo file (named by sessionID, contains array of todos) + await Bun.write( + path.join(storageDir, "todo", "ses_test456def.json"), + JSON.stringify([ + { + id: "todo_1", + content: "First todo", + status: "pending", + priority: "high", + }, + { + id: "todo_2", + content: "Second todo", + status: "completed", + priority: "medium", + }, + ]), + ) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.todos).toBe(2) + + const db = drizzle({ client: sqlite }) + const todos = db.select().from(TodoTable).all() + expect(todos.length).toBe(2) + expect(todos[0].id).toBe("todo_1") + expect(todos[0].content).toBe("First todo") + expect(todos[0].status).toBe("pending") + expect(todos[0].priority).toBe("high") + expect(todos[0].position).toBe(0) + expect(todos[1].id).toBe("todo_2") + expect(todos[1].position).toBe(1) + }) + + test("migrates permissions", async () => { + // First create the project + await Bun.write( + path.join(storageDir, "project", "proj_test123abc.json"), + JSON.stringify({ + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }), + ) + + // Create permission file (named by projectID, contains array of rules) + const permissionData = [ + { permission: "file.read", pattern: "/test/file1.ts", action: "allow" as const }, + { permission: "file.write", pattern: "/test/file2.ts", action: "ask" as const }, + { permission: "command.run", pattern: "npm install", action: "deny" as const }, + ] + await Bun.write(path.join(storageDir, "permission", "proj_test123abc.json"), JSON.stringify(permissionData)) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.permissions).toBe(1) + + const db = drizzle({ client: sqlite }) + const permissions = db.select().from(PermissionTable).all() + expect(permissions.length).toBe(1) + expect(permissions[0].project_id).toBe("proj_test123abc") + expect(permissions[0].data).toEqual(permissionData) + }) + + test("migrates session shares", async () => { + // First create the project and session + await Bun.write( + path.join(storageDir, "project", "proj_test123abc.json"), + JSON.stringify({ + id: "proj_test123abc", + worktree: "/", + time: { created: Date.now(), updated: Date.now() }, + sandboxes: [], + }), + ) + await Bun.write( + path.join(storageDir, "session", "proj_test123abc", "ses_test456def.json"), + JSON.stringify({ ...fixtures.session }), + ) + + // Create session share file (named by sessionID) + await Bun.write( + path.join(storageDir, "session_share", "ses_test456def.json"), + JSON.stringify({ + id: "share_123", + secret: "supersecretkey", + url: "https://share.example.com/ses_test456def", + }), + ) + + const stats = await JsonMigration.run(sqlite) + + expect(stats?.shares).toBe(1) + + const db = drizzle({ client: sqlite }) + const shares = db.select().from(SessionShareTable).all() + expect(shares.length).toBe(1) + expect(shares[0].session_id).toBe("ses_test456def") + expect(shares[0].id).toBe("share_123") + expect(shares[0].secret).toBe("supersecretkey") + expect(shares[0].url).toBe("https://share.example.com/ses_test456def") + }) })