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