sync
parent
f40685ab13
commit
63e38555c9
|
|
@ -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`);
|
||||
CREATE INDEX `todo_session_idx` ON `todo` (`session_id`);
|
||||
|
|
|
|||
|
|
@ -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<string[]>(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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<MessageV2.Part, "id" | "sessionID" | "messageID">
|
||||
type InfoData = Omit<MessageV2.Info, "id" | "sessionID">
|
||||
|
||||
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<Snapshot.FileDiff[]>(),
|
||||
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<PermissionNext.Ruleset>(),
|
||||
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<MessageV2.Info>(),
|
||||
...Database.Timestamps,
|
||||
data: text({ mode: "json" }).notNull().$type<InfoData>(),
|
||||
},
|
||||
(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<MessageV2.Part>(),
|
||||
...Database.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)],
|
||||
)
|
||||
|
|
@ -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<PermissionNext.Ruleset>(),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue