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 { LocalContext } from "../util" import { lazy } from "../util/lazy" import { Global } from "@opencode-ai/core/global" import { Log } from "../util" import { NamedError } from "@opencode-ai/core/util/error" import z from "zod" import path from "path" import { readFileSync, readdirSync, existsSync } from "fs" import { Flag } from "@opencode-ai/core/flag/flag" import { InstallationChannel } from "@opencode-ai/core/installation/version" import { InstanceState } from "@/effect" 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 function getChannelPath() { if (["latest", "beta", "prod"].includes(InstallationChannel) || Flag.OPENCODE_DISABLE_CHANNEL_DB) return path.join(Global.Path.data, "opencode.db") const safe = InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-") return path.join(Global.Path.data, `opencode-${safe}.db`) } export const Path = iife(() => { if (Flag.OPENCODE_DB) { if (Flag.OPENCODE_DB === ":memory:" || path.isAbsolute(Flag.OPENCODE_DB)) return Flag.OPENCODE_DB return path.join(Global.Path.data, Flag.OPENCODE_DB) } return getChannelPath() }) 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 = LocalContext.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 LocalContext.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) { const bound = InstanceState.bind(fn) try { ctx.use().effects.push(bound) } catch { bound() } } type NotPromise = T extends Promise ? never : T export function transaction( callback: (tx: TxOrDb) => NotPromise, options?: { behavior?: "deferred" | "immediate" | "exclusive" }, ): NotPromise { try { return callback(ctx.use().tx) } catch (err) { if (err instanceof LocalContext.NotFound) { const effects: (() => void | Promise)[] = [] const txCallback = InstanceState.bind((tx: TxOrDb) => ctx.provide({ tx, effects }, () => callback(tx))) const result = Client().transaction(txCallback, { behavior: options?.behavior }) for (const effect of effects) effect() return result as NotPromise } throw err } }