This commit is contained in:
Dax Raad
2026-01-27 16:57:51 -05:00
parent 246e901e42
commit cd174d8cba
6 changed files with 143 additions and 20 deletions

View File

@@ -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<string[]>(),
})

View File

@@ -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<MessageV2.Part, "id" | "sessionID" | "messageID">
type InfoData = Omit<MessageV2.Info, "id" | "sessionID">
@@ -27,7 +27,7 @@ export const SessionTable = sqliteTable(
summary_diffs: text({ mode: "json" }).$type<Snapshot.FileDiff[]>(),
revert: text({ mode: "json" }).$type<{ messageID: string; partID?: string; snapshot?: string; diff?: string }>(),
permission: text({ mode: "json" }).$type<PermissionNext.Ruleset>(),
...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<InfoData>(),
},
(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<PartData>(),
},
(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<PermissionNext.Ruleset>(),
})

View File

@@ -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,
})

View File

@@ -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()),
}
}

View File

@@ -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()),
}

View File

@@ -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")
})
})