From fc34c7456748e99f6105ed16ddac025d17cabafd Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Fri, 15 May 2026 04:09:10 +0530 Subject: [PATCH] refactor(flags): move channel db flag to runtime flags (#27615) --- packages/core/src/flag/flag.ts | 1 - packages/desktop/src/main/env.d.ts | 2 +- packages/opencode/src/cli/cmd/db.ts | 8 +- packages/opencode/src/effect/runtime-flags.ts | 1 + packages/opencode/src/storage/db.ts | 91 ++++++++++++------- .../test/effect/runtime-flags.test.ts | 4 + packages/opencode/test/fixture/db.ts | 7 +- packages/opencode/test/storage/db.test.ts | 31 +++++-- 8 files changed, 93 insertions(+), 52 deletions(-) diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index 1822d8f8da..d1480179a1 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -62,7 +62,6 @@ export const Flag = { OPENCODE_MODELS_URL: process.env["OPENCODE_MODELS_URL"], OPENCODE_MODELS_PATH: process.env["OPENCODE_MODELS_PATH"], OPENCODE_DB: process.env["OPENCODE_DB"], - OPENCODE_DISABLE_CHANNEL_DB: truthy("OPENCODE_DISABLE_CHANNEL_DB"), OPENCODE_SKIP_MIGRATIONS: truthy("OPENCODE_SKIP_MIGRATIONS"), OPENCODE_STRICT_CONFIG_DEPS: truthy("OPENCODE_STRICT_CONFIG_DEPS"), diff --git a/packages/desktop/src/main/env.d.ts b/packages/desktop/src/main/env.d.ts index eee21e48cb..1d03be5055 100644 --- a/packages/desktop/src/main/env.d.ts +++ b/packages/desktop/src/main/env.d.ts @@ -19,7 +19,7 @@ declare module "virtual:opencode-server" { export const init: typeof import("../../../opencode/dist/types/src/node").Log.init } export namespace Database { - export const Path: typeof import("../../../opencode/dist/types/src/node").Database.Path + export const getPath: typeof import("../../../opencode/dist/types/src/node").Database.getPath export const Client: typeof import("../../../opencode/dist/types/src/node").Database.Client } export namespace JsonMigration { diff --git a/packages/opencode/src/cli/cmd/db.ts b/packages/opencode/src/cli/cmd/db.ts index 2aa5caf10a..b113455f3b 100644 --- a/packages/opencode/src/cli/cmd/db.ts +++ b/packages/opencode/src/cli/cmd/db.ts @@ -28,7 +28,7 @@ const QueryCommand = cmd({ handler: async (args: { query?: string; format: string }) => { const query = args.query as string | undefined if (query) { - const db = new BunDatabase(Database.Path, { readonly: true }) + const db = new BunDatabase(Database.getPath(), { readonly: true }) try { const result = db.query(query).all() as Record[] if (args.format === "json") { @@ -47,7 +47,7 @@ const QueryCommand = cmd({ db.close() return } - const child = spawn("sqlite3", [Database.Path], { + const child = spawn("sqlite3", [Database.getPath()], { stdio: "inherit", }) await new Promise((resolve) => child.on("close", resolve)) @@ -58,7 +58,7 @@ const PathCommand = cmd({ command: "path", describe: "print the database path", handler: () => { - console.log(Database.Path) + console.log(Database.getPath()) }, }) @@ -66,7 +66,7 @@ const MigrateCommand = cmd({ command: "migrate", describe: "migrate JSON data to SQLite (merges with existing data)", handler: async () => { - const sqlite = new BunDatabase(Database.Path) + const sqlite = new BunDatabase(Database.getPath()) const tty = process.stderr.isTTY const width = 36 const orange = "\x1b[38;5;214m" diff --git a/packages/opencode/src/effect/runtime-flags.ts b/packages/opencode/src/effect/runtime-flags.ts index 332568680a..f4774d156b 100644 --- a/packages/opencode/src/effect/runtime-flags.ts +++ b/packages/opencode/src/effect/runtime-flags.ts @@ -15,6 +15,7 @@ export class Service extends ConfigService.Service()("@opencode/Runtime autoShare: bool("OPENCODE_AUTO_SHARE"), pure: bool("OPENCODE_PURE"), disableDefaultPlugins: bool("OPENCODE_DISABLE_DEFAULT_PLUGINS"), + disableChannelDb: bool("OPENCODE_DISABLE_CHANNEL_DB"), disableEmbeddedWebUi: bool("OPENCODE_DISABLE_EMBEDDED_WEB_UI"), disableClaudeCodeSkills: Config.all({ broad: bool("OPENCODE_DISABLE_CLAUDE_CODE"), diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 86e14da560..3c93639136 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -2,8 +2,8 @@ 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 { RuntimeFlags } from "@/effect/runtime-flags" import { LocalContext } from "@/util/local-context" -import { lazy } from "../util/lazy" import { Global } from "@opencode-ai/core/global" import * as Log from "@opencode-ai/core/util/log" import { NamedError } from "@opencode-ai/core/util/error" @@ -12,9 +12,8 @@ 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/instance-state" -import { iife } from "@/util/iife" import { init } from "#db" -import { Schema } from "effect" +import { Effect, Schema } from "effect" declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number; name: string }[] | undefined @@ -24,24 +23,29 @@ export const NotFoundError = NamedError.create("NotFoundError", { const log = Log.create({ service: "db" }) -export function getChannelPath() { - if (["latest", "beta", "prod"].includes(InstallationChannel) || Flag.OPENCODE_DISABLE_CHANNEL_DB) +type ChannelDbFlags = Pick + +const readRuntimeFlags = () => + Effect.runSync(RuntimeFlags.Service.useSync((flags) => flags).pipe(Effect.provide(RuntimeFlags.defaultLayer))) + +export function getChannelPath(flags: ChannelDbFlags = readRuntimeFlags()) { + if (["latest", "beta", "prod"].includes(InstallationChannel) || flags.disableChannelDb) 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(() => { +export const getPath = (flags?: ChannelDbFlags) => { 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() -}) + return getChannelPath(flags) +} export type Transaction = SQLiteTransaction<"sync", void> -type Client = SQLiteBunDatabase +type Client = ReturnType type Journal = { sql: string; timestamp: number; name: string }[] @@ -85,38 +89,55 @@ function migrations(dir: string): Journal { return sql.sort((a, b) => a.timestamp - b.timestamp) } -export const Client = lazy(() => { - log.info("opening database", { path: Path }) +let client: Client | undefined +let loaded = false - const db = init(Path) +export const Client = Object.assign( + (flags?: ChannelDbFlags): Client => { + if (loaded) return client as Client - 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)") + const dbPath = getPath(flags) + log.info("opening database", { path: dbPath }) - // 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;" + const db = init(dbPath) + + 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;" + } } + applyMigrations(db, entries) } - applyMigrations(db, entries) - } - return db -}) + client = db + loaded = true + return db + }, + { + reset: () => { + loaded = false + client = undefined + }, + loaded: () => loaded, + }, +) export function close() { if (!Client.loaded()) return diff --git a/packages/opencode/test/effect/runtime-flags.test.ts b/packages/opencode/test/effect/runtime-flags.test.ts index 5167905b8e..14e4b12684 100644 --- a/packages/opencode/test/effect/runtime-flags.test.ts +++ b/packages/opencode/test/effect/runtime-flags.test.ts @@ -24,6 +24,7 @@ describe("RuntimeFlags", () => { fromConfig({ OPENCODE_PURE: "true", OPENCODE_DISABLE_DEFAULT_PLUGINS: "true", + OPENCODE_DISABLE_CHANNEL_DB: "true", OPENCODE_AUTO_SHARE: "true", OPENCODE_DISABLE_EMBEDDED_WEB_UI: "true", OPENCODE_EXPERIMENTAL: "true", @@ -39,6 +40,7 @@ describe("RuntimeFlags", () => { expect(flags.pure).toBe(true) expect(flags.autoShare).toBe(true) expect(flags.disableDefaultPlugins).toBe(true) + expect(flags.disableChannelDb).toBe(true) expect(flags.disableEmbeddedWebUi).toBe(true) expect(flags.enableExa).toBe(true) expect(flags.enableParallel).toBe(true) @@ -80,6 +82,7 @@ describe("RuntimeFlags", () => { expect(flags.pure).toBe(false) expect(flags.autoShare).toBe(false) expect(flags.disableDefaultPlugins).toBe(true) + expect(flags.disableChannelDb).toBe(false) expect(flags.disableEmbeddedWebUi).toBe(false) expect(flags.disableClaudeCodeSkills).toBe(false) expect(flags.enableExa).toBe(false) @@ -200,6 +203,7 @@ describe("RuntimeFlags", () => { expect(flags.pure).toBe(false) expect(flags.disableDefaultPlugins).toBe(false) + expect(flags.disableChannelDb).toBe(false) expect(flags.disableEmbeddedWebUi).toBe(false) expect(flags.disableClaudeCodeSkills).toBe(false) expect(flags.enableExa).toBe(false) diff --git a/packages/opencode/test/fixture/db.ts b/packages/opencode/test/fixture/db.ts index 07b42d9946..db4a5df20c 100644 --- a/packages/opencode/test/fixture/db.ts +++ b/packages/opencode/test/fixture/db.ts @@ -5,7 +5,8 @@ import { disposeAllInstances } from "./fixture" export async function resetDatabase() { await disposeAllInstances().catch(() => undefined) Database.close() - await rm(Database.Path, { force: true }).catch(() => undefined) - await rm(`${Database.Path}-wal`, { force: true }).catch(() => undefined) - await rm(`${Database.Path}-shm`, { force: true }).catch(() => undefined) + const dbPath = Database.getPath() + await rm(dbPath, { force: true }).catch(() => undefined) + await rm(`${dbPath}-wal`, { force: true }).catch(() => undefined) + await rm(`${dbPath}-shm`, { force: true }).catch(() => undefined) } diff --git a/packages/opencode/test/storage/db.test.ts b/packages/opencode/test/storage/db.test.ts index f667fc9045..ec351fdd76 100644 --- a/packages/opencode/test/storage/db.test.ts +++ b/packages/opencode/test/storage/db.test.ts @@ -1,14 +1,29 @@ -import { describe, expect, test } from "bun:test" +import { describe, expect } from "bun:test" import path from "path" +import { Effect } from "effect" import { Global } from "@opencode-ai/core/global" import { InstallationChannel } from "@opencode-ai/core/installation/version" +import { RuntimeFlags } from "@/effect/runtime-flags" import { Database } from "@/storage/db" +import { it } from "../lib/effect" -describe("Database.Path", () => { - test("returns database path for the current channel", () => { - const expected = ["latest", "beta"].includes(InstallationChannel) - ? path.join(Global.Path.data, "opencode.db") - : path.join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`) - expect(Database.getChannelPath()).toBe(expected) - }) +describe("Database.getChannelPath", () => { + it.effect("returns database path for the current channel", () => + Effect.gen(function* () { + const flags = yield* RuntimeFlags.Service + const expected = ["latest", "beta", "prod"].includes(InstallationChannel) + ? path.join(Global.Path.data, "opencode.db") + : path.join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`) + + expect(Database.getChannelPath(flags)).toBe(expected) + }).pipe(Effect.provide(RuntimeFlags.layer())), + ) + + it.effect("uses the shared database path when channel databases are disabled", () => + Effect.gen(function* () { + const flags = yield* RuntimeFlags.Service + + expect(Database.getChannelPath(flags)).toBe(path.join(Global.Path.data, "opencode.db")) + }).pipe(Effect.provide(RuntimeFlags.layer({ disableChannelDb: true }))), + ) })