import { 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 { Installation } from "../installation" import { Flag } from "../flag/flag" import { iife } from "@/util/iife" import { init } from "#db" 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 const Path = iife(() => { const channel = Installation.CHANNEL if (["latest", "beta"].includes(channel) || Flag.OPENCODE_DISABLE_CHANNEL_DB) return path.join(Global.Path.data, "opencode.db") const safe = channel.replace(/[^a-zA-Z0-9._-]/g, "-") return path.join(Global.Path.data, `opencode-${safe}.db`) }) export type Transaction = SQLiteTransaction<"sync", void> type Client = SQLiteBunDatabase type Journal = { sql: string; timestamp: number; name: string }[] 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 db = init(Path) db.run("PRAGMA journal_mode = WAL") db.run("PRAGMA synchronous = NORMAL") db.run("PRAGMA busy_timeout = 5000") db.run("PRAGMA cache_size = -64000") db.run("PRAGMA foreign_keys = ON") db.run("PRAGMA wal_checkpoint(PASSIVE)") // 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() { Client().$client.close() Client.reset() } export type TxOrDb = Transaction | Client const ctx = Context.create<{ tx: TxOrDb effects: (() => void | Promise)[] }>("database") export function use(callback: (trx: TxOrDb) => T): T { try { return callback(ctx.use().tx) } catch (err) { if (err instanceof Context.NotFound) { const effects: (() => void | Promise)[] = [] 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) { try { ctx.use().effects.push(fn) } catch { fn() } } export function transaction(callback: (tx: TxOrDb) => T): T { try { return callback(ctx.use().tx) } catch (err) { if (err instanceof Context.NotFound) { const effects: (() => void | Promise)[] = [] 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 } } }