Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
"@tailwindcss/vite": "4.1.11",
"diff": "8.0.2",
"dompurify": "3.3.1",
"drizzle-kit": "1.0.0-beta.16-ea816b6",
"drizzle-orm": "1.0.0-beta.16-ea816b6",
"drizzle-kit": "1.0.0-beta.19-d95b7a4",
"drizzle-orm": "1.0.0-beta.19-d95b7a4",
"effect": "4.0.0-beta.35",
"ai": "5.0.124",
"hono": "4.10.7",
Expand Down
15 changes: 11 additions & 4 deletions packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
"exports": {
"./*": "./src/*.ts"
},
"imports": {
"#db": {
"bun": "./src/storage/db.bun.ts",
"node": "./src/storage/db.node.ts",
"default": "./src/storage/db.bun.ts"
}
},
"devDependencies": {
"@babel/core": "7.28.4",
"@effect/language-service": "0.79.0",
Expand All @@ -50,8 +57,8 @@
"@types/which": "3.0.4",
"@types/yargs": "17.0.33",
"@typescript/native-preview": "catalog:",
"drizzle-kit": "1.0.0-beta.16-ea816b6",
"drizzle-orm": "1.0.0-beta.16-ea816b6",
"drizzle-kit": "catalog:",
"drizzle-orm": "catalog:",
"typescript": "catalog:",
"vscode-languageserver-types": "3.17.5",
"why-is-node-running": "3.2.2",
Expand Down Expand Up @@ -113,7 +120,7 @@
"cross-spawn": "^7.0.6",
"decimal.js": "10.5.0",
"diff": "catalog:",
"drizzle-orm": "1.0.0-beta.16-ea816b6",
"drizzle-orm": "catalog:",
"effect": "catalog:",
"fuzzysort": "3.1.0",
"glob": "13.0.5",
Expand Down Expand Up @@ -144,6 +151,6 @@
"zod-to-json-schema": "3.24.5"
},
"overrides": {
"drizzle-orm": "1.0.0-beta.16-ea816b6"
"drizzle-orm": "catalog:"
}
}
8 changes: 8 additions & 0 deletions packages/opencode/src/storage/db.bun.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Database } from "bun:sqlite"
import { drizzle } from "drizzle-orm/bun-sqlite"

export function init(path: string) {
const sqlite = new Database(path, { create: true })
const db = drizzle({ client: sqlite })
return db
}
8 changes: 8 additions & 0 deletions packages/opencode/src/storage/db.node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { DatabaseSync } from "node:sqlite"
import { drizzle } from "drizzle-orm/node-sqlite"

export function init(path: string) {
const sqlite = new DatabaseSync(path)
const db = drizzle({ client: sqlite })
return db
}
36 changes: 12 additions & 24 deletions packages/opencode/src/storage/db.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Database as BunDatabase } from "bun:sqlite"
import { drizzle, type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
import { type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Bun-specific type alias persists for Client

SQLiteBunDatabase is imported and used as the Client type alias (line 40: type Client = SQLiteBunDatabase). Under Node.js, init() from db.node.ts returns a BetterSQLite3Database<Record<string, never>> (or equivalent drizzle node-sqlite type), not a SQLiteBunDatabase. This mismatch means all Client-typed values, including the $client access in close(), are typed against the wrong runtime object.

The fix would be to either export a common DB type from #db or use ReturnType<typeof init> to infer the correct type.

import { migrate } from "drizzle-orm/bun-sqlite/migrator"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Bun-specific migrator not abstracted behind #db

The whole point of this PR is to abstract the runtime-specific database layer, but migrate is still imported directly from drizzle-orm/bun-sqlite/migrator. On line 104, migrate(db, entries) is called passing whatever init() returned — which, on Node.js, will be a drizzle-orm/node-sqlite client — into a Bun-specific migrator. The two adapters are typed differently and this is likely to fail at runtime under Node.js.

The migrator should either be exported from #db alongside init, or a separate #migrator conditional import should be added to package.json.

Suggested change
import { migrate } from "drizzle-orm/bun-sqlite/migrator"
import { migrate } from "#db/migrator"

And correspondingly, db.bun.ts should re-export from drizzle-orm/bun-sqlite/migrator and db.node.ts from drizzle-orm/node-sqlite/migrator.

import { type SQLiteTransaction } from "drizzle-orm/sqlite-core"
export * from "drizzle-orm"
Expand All @@ -11,10 +10,10 @@ 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"
import { iife } from "@/util/iife"
import { init } from "#db"

declare const OPENCODE_MIGRATIONS: { sql: string; timestamp: number; name: string }[] | undefined

Expand All @@ -36,17 +35,12 @@ export namespace Database {
return path.join(Global.Path.data, `opencode-${safe}.db`)
})

type Schema = typeof schema
export type Transaction = SQLiteTransaction<"sync", void, Schema>
export type Transaction = SQLiteTransaction<"sync", void>

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
Expand Down Expand Up @@ -83,17 +77,14 @@ export namespace Database {
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 = init(Path)

const db = drizzle({ client: sqlite })
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 =
Expand All @@ -117,14 +108,11 @@ export namespace Database {
})

export function close() {
const sqlite = state.sqlite
if (!sqlite) return
sqlite.close()
state.sqlite = undefined
Client().$client.close()
Client.reset()
}
Comment on lines 110 to 113
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 close() is no longer a safe no-op when DB is uninitialized

The original implementation guarded against uninitialized state with if (!sqlite) return. The new implementation calls Client(), which lazily initializes the database if it hasn't been opened yet. This means:

  1. If close() is called before the database has ever been accessed, it will now open the database, then immediately close it — a behavioral regression.
  2. If close() is called a second time (after Client.reset()), it will re-open the database again before closing it, rather than being a no-op.

A simple guard can restore the original safety:

Suggested change
export function close() {
const sqlite = state.sqlite
if (!sqlite) return
sqlite.close()
state.sqlite = undefined
Client().$client.close()
Client.reset()
}
export function close() {
if (!Client.loaded) return
Client().$client.close()
Client.reset()
}

(Requires exposing a loaded flag on the lazy wrapper, or checking via a separate flag.)


export type TxOrDb = SQLiteTransaction<"sync", void, any, any> | Client
export type TxOrDb = Transaction | Client

const ctx = Context.create<{
tx: TxOrDb
Expand Down
Loading