-
-
Notifications
You must be signed in to change notification settings - Fork 10
refactor: add SQLite adapter to decouple from bun:sqlite #968
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| # Bun → Node.js Migration Plan | ||
|
|
||
| Status: In progress. See below for completed and remaining steps. | ||
|
|
||
| ## Completed | ||
|
|
||
| ### Phase 4 (early): Package Manager Switch | ||
| - [x] Changed `packageManager` from `bun@1.3.13` to `pnpm@10.11.0` | ||
| - [x] Moved `patchedDependencies` into `pnpm` config section | ||
| - [x] Added `onlyBuiltDependencies: ["esbuild"]` | ||
| - [x] Added phantom deps as explicit devDependencies: `@sentry/core`, `@clack/prompts` | ||
| - [x] Generated `pnpm-lock.yaml` | ||
| - [x] Verified all patches apply (including cross-version: `@stricli/core@1.2.5` patch on `1.2.7`) | ||
|
|
||
| ### Phase 2, Group D: SQLite Adapter | ||
| - [x] Created `src/lib/db/sqlite.ts` — runtime-detecting adapter (bun:sqlite under Bun, node:sqlite under Node.js) | ||
| - [x] Updated 4 source files: `db/index.ts`, `schema.ts`, `migration.ts`, `utils.ts` | ||
| - [x] Updated 3 test files: `fix.test.ts`, `telemetry.test.ts`, `schema.test.ts` | ||
| - [x] Zero `bun:sqlite` imports remain in `src/` or `test/` | ||
|
|
||
| ## Remaining | ||
|
|
||
| ### Phase 2: Source Code Migration (replace Bun.* APIs in `src/`) | ||
|
|
||
| **Group A: File I/O** — Replace `Bun.file()` / `Bun.write()` with `node:fs/promises` | ||
| - `Bun.file(path).text()` → `readFile(path, "utf-8")` | ||
| - `Bun.file(path).json()` → `readFile(path, "utf-8")` then `JSON.parse()` | ||
| - `Bun.file(path).exists()` → `access(path).then(() => true, () => false)` | ||
| - `Bun.write(path, content)` → `writeFile(path, content)` | ||
| - Scan all of `src/` for occurrences | ||
|
|
||
| **Group B: Process/System APIs** — Replace Bun.which / Bun.spawn / Bun.sleep | ||
| - `Bun.which("cmd")` → `which` from a Node.js-compatible package or custom implementation | ||
| - `Bun.spawn()` / `Bun.spawnSync()` → `child_process.spawn()` / `spawnSync()` | ||
| - `Bun.sleep(ms)` → `setTimeout` promise wrapper | ||
|
|
||
| **Group C: Miscellaneous Bun APIs** | ||
| - `Bun.Glob` → `tinyglobby` or `picomatch` (already in devDependencies) | ||
| - `Bun.randomUUIDv7()` → `uuidv7` package (already in devDependencies) | ||
| - `Bun.semver.order()` → `semver.compare()` (already in devDependencies) | ||
| - `Bun.zstdCompressSync()` / `Bun.zstdDecompressSync()` → Node.js zlib or `zstd-napi` package | ||
|
|
||
| **Group E: Unpolyfilled APIs** | ||
| - `bspatch.ts` and `upgrade.ts` — Replace any Bun-specific APIs not covered by node-polyfills.ts | ||
|
|
||
| ### Phase 3: Test Migration (`bun:test` → Vitest) | ||
|
|
||
| - Add `vitest` as devDependency | ||
| - Replace `import { ... } from "bun:test"` with Vitest equivalents | ||
| - Replace `bun test` scripts with `vitest` | ||
| - Key differences: | ||
| - `bun:test`'s `mock.module()` → Vitest's `vi.mock()` | ||
| - `bun:test`'s `spyOn` → Vitest's `vi.spyOn()` | ||
| - Test file discovery patterns may differ | ||
| - `--isolate --parallel` behavior needs Vitest equivalent | ||
|
|
||
| ### Phase 4: CI & Dev Scripts (remaining) | ||
|
|
||
| - Update `package.json` scripts: `bun run` → `pnpm run` where appropriate | ||
| - Replace `bun run src/bin.ts` with `tsx src/bin.ts` (add `tsx` devDependency) | ||
| - Replace `bun run script/*.ts` with `tsx script/*.ts` | ||
| - Replace `bunx` with `pnpm exec` | ||
| - Update GitHub Actions workflows to use pnpm + Node.js instead of Bun | ||
| - Update `Dockerfile` / build scripts if applicable | ||
|
|
||
| ### Phase 5: Cleanup | ||
|
|
||
| - Remove `@types/bun` from devDependencies | ||
| - Remove `bun.lock` (replaced by `pnpm-lock.yaml`) | ||
| - Remove or update `script/node-polyfills.ts` (may become unnecessary) | ||
| - Update `AGENTS.md` Bun API reference table | ||
| - Remove Bun-specific `.cursor/rules/bun-cli.mdc` or update for Node.js | ||
| - Clean up any remaining `Bun.*` references in comments/docs | ||
|
|
||
| ## Known Issues | ||
|
|
||
| - `test/lib/index.test.ts` — `sdk.run throws when auth is required but missing` fails under pnpm's strict `node_modules`. The mock fetch returns empty 200s which prevents the expected auth error from being thrown. Pre-existing test fragility, not caused by migration changes. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| /** | ||
| * SQLite adapter that provides a unified API surface for database access. | ||
| * This module is the single import point for all SQLite access in the | ||
| * codebase, replacing direct `bun:sqlite` imports. | ||
| * | ||
| * Runtime detection: | ||
| * - **Bun**: Re-exports `bun:sqlite`'s Database directly (zero overhead). | ||
| * - **Node.js**: Wraps `node:sqlite`'s DatabaseSync with a bun:sqlite- | ||
| * compatible API (`.query()` → `.prepare()`, manual transaction wrapper). | ||
| * | ||
| * Call sites continue to use `.query(sql).get()` / `.all()` / `.run()` | ||
| * and `db.transaction()` exactly as before — no migration churn needed. | ||
| */ | ||
|
|
||
| /** Valid SQLite binding value */ | ||
| export type SQLQueryBindings = | ||
| | string | ||
| | number | ||
| | bigint | ||
| | null | ||
| | Uint8Array | ||
| | undefined; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Runtime detection | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| const isBun = typeof globalThis.Bun !== "undefined"; | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Node.js implementation (wraps node:sqlite DatabaseSync) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * Minimal statement wrapper matching the bun:sqlite query API. | ||
| * Wraps node:sqlite's StatementSync to expose `.get()`, `.all()`, `.run()`. | ||
| */ | ||
| class NodeStatementWrapper { | ||
| // biome-ignore lint/suspicious/noExplicitAny: node:sqlite types loaded lazily | ||
| private readonly stmt: any; | ||
|
|
||
| // biome-ignore lint/suspicious/noExplicitAny: node:sqlite types loaded lazily | ||
| constructor(stmt: any) { | ||
| this.stmt = stmt; | ||
| } | ||
|
|
||
| get( | ||
| ...params: SQLQueryBindings[] | ||
| ): Record<string, SQLQueryBindings> | undefined { | ||
| return this.stmt.get(...params) as | ||
| | Record<string, SQLQueryBindings> | ||
| | undefined; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adapter
|
||
|
|
||
| all(...params: SQLQueryBindings[]): Record<string, SQLQueryBindings>[] { | ||
| return this.stmt.all(...params) as Record<string, SQLQueryBindings>[]; | ||
| } | ||
|
|
||
| run(...params: SQLQueryBindings[]): void { | ||
| this.stmt.run(...params); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Node.js SQLite database wrapper with bun:sqlite-compatible API. | ||
| * Used only when running under Node.js (not Bun). | ||
| */ | ||
| class NodeDatabase { | ||
| // biome-ignore lint/suspicious/noExplicitAny: node:sqlite types loaded lazily | ||
| private readonly db: any; | ||
|
|
||
| constructor(path: string) { | ||
| // Lazy-load node:sqlite to avoid crashing on runtimes without it. | ||
| // biome-ignore lint/suspicious/noExplicitAny: node:sqlite types loaded lazily | ||
| const { DatabaseSync } = require("node:sqlite") as any; | ||
| this.db = new DatabaseSync(path); | ||
| } | ||
|
|
||
| exec(sql: string): void { | ||
| this.db.exec(sql); | ||
| } | ||
|
|
||
| query(sql: string): NodeStatementWrapper { | ||
| return new NodeStatementWrapper(this.db.prepare(sql)); | ||
| } | ||
|
|
||
| close(): void { | ||
| this.db.close(); | ||
| } | ||
|
|
||
| transaction<T>(fn: () => T): () => T { | ||
| return () => { | ||
| this.db.exec("BEGIN"); | ||
| try { | ||
| const result = fn(); | ||
| this.db.exec("COMMIT"); | ||
| return result; | ||
| } catch (error) { | ||
| this.db.exec("ROLLBACK"); | ||
| throw error; | ||
| } | ||
| }; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Unified export — picks the right implementation at runtime | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** | ||
| * SQLite Database class. | ||
| * | ||
| * Under Bun this is the native `bun:sqlite` Database (zero-overhead re-export). | ||
| * Under Node.js this is a wrapper around `node:sqlite`'s DatabaseSync that | ||
| * provides the same `.exec()`, `.query()`, `.close()`, `.transaction()` API. | ||
| */ | ||
| // biome-ignore lint/suspicious/noExplicitAny: conditional runtime export | ||
| export const Database: any = isBun | ||
| ? require("bun:sqlite").Database | ||
| : NodeDatabase; | ||
|
|
||
| // Re-export the type so `import type { Database }` works correctly. | ||
| // The type matches the bun:sqlite Database shape used across the codebase. | ||
| export type Database = InstanceType<typeof Database>; | ||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The check for
error.name === "SQLiteError"may fail for errors fromnode:sqlite, silently disabling database error handling and auto-repair for Node.js users.Severity: HIGH
Suggested Fix
Instead of checking
error.name, consider checking a more reliable property on the error object fromnode:sqlite, such aserror.code. Alternatively, wrap errors fromnode:sqlitein a custom error class that correctly sets thenameproperty to"SQLiteError"before they are processed by the error handling logic.Prompt for AI Agent
Did we get this right? 👍 / 👎 to inform future reviews.