mirror of
https://gitea.toothfairyai.com/ToothFairyAI/tf_code.git
synced 2026-04-01 14:52:25 +00:00
Allows users to skip automatic database migrations by setting the OPENCODE_SKIP_MIGRATIONS environment variable. Useful for testing scenarios or when manually managing database state.
173 lines
4.8 KiB
TypeScript
173 lines
4.8 KiB
TypeScript
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"
|
|
export * from "drizzle-orm"
|
|
import { Context } from "../util/context"
|
|
import { lazy } from "../util/lazy"
|
|
import { Global } from "../global"
|
|
import { Log } from "../util/log"
|
|
import { NamedError } from "@opencode-ai/util/error"
|
|
import z from "zod"
|
|
import path from "path"
|
|
import { readFileSync, readdirSync, existsSync } from "fs"
|
|
import * as schema from "./schema"
|
|
import { Installation } from "../installation"
|
|
import { Flag } from "../flag/flag"
|
|
|
|
declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number; name: string }[] | undefined
|
|
|
|
export const NotFoundError = NamedError.create(
|
|
"NotFoundError",
|
|
z.object({
|
|
message: z.string(),
|
|
}),
|
|
)
|
|
|
|
const log = Log.create({ service: "db" })
|
|
|
|
export namespace Database {
|
|
export function file(channel: string) {
|
|
if (channel === "latest" || Flag.OPENCODE_DISABLE_CHANNEL_DB) return "opencode.db"
|
|
const safe = channel.replace(/[^a-zA-Z0-9._-]/g, "-")
|
|
return `opencode-${safe}.db`
|
|
}
|
|
|
|
export const Path = (() => {
|
|
return path.join(Global.Path.data, file(Installation.CHANNEL))
|
|
})()
|
|
|
|
type Schema = typeof schema
|
|
export type Transaction = SQLiteTransaction<"sync", void, Schema>
|
|
|
|
type Client = SQLiteBunDatabase<Schema>
|
|
|
|
type Journal = { sql: string; timestamp: number; name: string }[]
|
|
|
|
const state = {
|
|
sqlite: undefined as BunDatabase | undefined,
|
|
}
|
|
|
|
function time(tag: string) {
|
|
const match = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(tag)
|
|
if (!match) return 0
|
|
return Date.UTC(
|
|
Number(match[1]),
|
|
Number(match[2]) - 1,
|
|
Number(match[3]),
|
|
Number(match[4]),
|
|
Number(match[5]),
|
|
Number(match[6]),
|
|
)
|
|
}
|
|
|
|
function migrations(dir: string): Journal {
|
|
const dirs = readdirSync(dir, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
|
|
const sql = dirs
|
|
.map((name) => {
|
|
const file = path.join(dir, name, "migration.sql")
|
|
if (!existsSync(file)) return
|
|
return {
|
|
sql: readFileSync(file, "utf-8"),
|
|
timestamp: time(name),
|
|
name,
|
|
}
|
|
})
|
|
.filter(Boolean) as Journal
|
|
|
|
return sql.sort((a, b) => a.timestamp - b.timestamp)
|
|
}
|
|
|
|
export const Client = lazy(() => {
|
|
log.info("opening database", { path: Path })
|
|
|
|
const sqlite = new BunDatabase(Path, { create: true })
|
|
state.sqlite = sqlite
|
|
|
|
sqlite.run("PRAGMA journal_mode = WAL")
|
|
sqlite.run("PRAGMA synchronous = NORMAL")
|
|
sqlite.run("PRAGMA busy_timeout = 5000")
|
|
sqlite.run("PRAGMA cache_size = -64000")
|
|
sqlite.run("PRAGMA foreign_keys = ON")
|
|
sqlite.run("PRAGMA wal_checkpoint(PASSIVE)")
|
|
|
|
const db = drizzle({ client: sqlite, schema })
|
|
|
|
// Apply schema migrations
|
|
const entries =
|
|
typeof OPENCODE_MIGRATIONS !== "undefined"
|
|
? OPENCODE_MIGRATIONS
|
|
: migrations(path.join(import.meta.dirname, "../../migration"))
|
|
if (entries.length > 0) {
|
|
log.info("applying migrations", {
|
|
count: entries.length,
|
|
mode: typeof OPENCODE_MIGRATIONS !== "undefined" ? "bundled" : "dev",
|
|
})
|
|
if (Flag.OPENCODE_SKIP_MIGRATIONS) {
|
|
for (const item of entries) {
|
|
item.sql = "select 1;"
|
|
}
|
|
}
|
|
migrate(db, entries)
|
|
}
|
|
|
|
return db
|
|
})
|
|
|
|
export function close() {
|
|
const sqlite = state.sqlite
|
|
if (!sqlite) return
|
|
sqlite.close()
|
|
state.sqlite = undefined
|
|
Client.reset()
|
|
}
|
|
|
|
export type TxOrDb = Transaction | Client
|
|
|
|
const ctx = Context.create<{
|
|
tx: TxOrDb
|
|
effects: (() => void | Promise<void>)[]
|
|
}>("database")
|
|
|
|
export function use<T>(callback: (trx: TxOrDb) => T): T {
|
|
try {
|
|
return callback(ctx.use().tx)
|
|
} catch (err) {
|
|
if (err instanceof Context.NotFound) {
|
|
const effects: (() => void | Promise<void>)[] = []
|
|
const result = ctx.provide({ effects, tx: Client() }, () => callback(Client()))
|
|
for (const effect of effects) effect()
|
|
return result
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
|
|
export function effect(fn: () => any | Promise<any>) {
|
|
try {
|
|
ctx.use().effects.push(fn)
|
|
} catch {
|
|
fn()
|
|
}
|
|
}
|
|
|
|
export function transaction<T>(callback: (tx: TxOrDb) => T): T {
|
|
try {
|
|
return callback(ctx.use().tx)
|
|
} catch (err) {
|
|
if (err instanceof Context.NotFound) {
|
|
const effects: (() => void | Promise<void>)[] = []
|
|
const result = (Client().transaction as any)((tx: TxOrDb) => {
|
|
return ctx.provide({ tx, effects }, () => callback(tx))
|
|
})
|
|
for (const effect of effects) effect()
|
|
return result
|
|
}
|
|
throw err
|
|
}
|
|
}
|
|
}
|