-
-
Notifications
You must be signed in to change notification settings - Fork 10
refactor: add SQLite adapter to decouple from bun:sqlite #970
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,147 @@ | ||
| /** | ||
| * SQLite adapter wrapping node:sqlite's DatabaseSync with a convenient API. | ||
| * | ||
| * This module is the single import point for all SQLite access in the | ||
| * codebase. It provides a `.query(sql).get()` / `.all()` / `.run()` | ||
| * interface and a manual `transaction()` wrapper. | ||
| * | ||
| * Uses `node:sqlite` (Node 22+) as the backing implementation. Falls back | ||
| * to `bun:sqlite` when `node:sqlite` is unavailable (Bun runtime) — this | ||
| * fallback will be removed once the test runner migrates off Bun. | ||
| */ | ||
|
|
||
| import { logger } from "../logger.js"; | ||
|
|
||
| const log = logger.withTag("sqlite"); | ||
|
|
||
| /** Valid SQLite binding value. */ | ||
| export type SQLQueryBindings = | ||
| | string | ||
| | number | ||
| | bigint | ||
| | boolean | ||
| | null | ||
| | Uint8Array | ||
| | undefined; | ||
|
|
||
| /** | ||
| * Prepared statement wrapper exposing `.get()`, `.all()`, `.run()`. | ||
| * | ||
| * Uses a Proxy to pass through any additional driver-specific methods | ||
| * (e.g. bun:sqlite's `.values()`) while normalising `.get()` to return | ||
| * `null` (not `undefined`) for no-row results. | ||
| */ | ||
| type StatementWrapper = { | ||
| get(...params: SQLQueryBindings[]): Record<string, SQLQueryBindings> | null; | ||
| all(...params: SQLQueryBindings[]): Record<string, SQLQueryBindings>[]; | ||
| run(...params: SQLQueryBindings[]): void; | ||
| /** Allow driver-specific methods (e.g. bun:sqlite `.values()`) to pass through. */ | ||
| [method: string]: unknown; | ||
| }; | ||
|
|
||
| // biome-ignore lint/suspicious/noExplicitAny: backing driver types vary | ||
| function wrapStatement(stmt: any): StatementWrapper { | ||
| return new Proxy(stmt, { | ||
| get(target, prop) { | ||
| if (prop === "get") { | ||
| return (...params: SQLQueryBindings[]) => | ||
| // node:sqlite returns undefined for no rows; bun:sqlite returns null. | ||
| // Normalise to null so callers can rely on a single sentinel. | ||
| (target.get(...params) as Record<string, SQLQueryBindings>) ?? null; | ||
| } | ||
| const value = Reflect.get(target, prop); | ||
| if (typeof value === "function") { | ||
| return value.bind(target); | ||
| } | ||
| return value; | ||
| }, | ||
| }) as StatementWrapper; | ||
| } | ||
|
|
||
| /** | ||
| * Resolve the underlying SQLite database constructor. | ||
| * | ||
| * Prefers `node:sqlite` (Node 22+). Falls back to `bun:sqlite` when | ||
| * `node:sqlite` is unavailable (Bun runtime). The fallback will be | ||
| * removed once the test runner migrates off Bun. | ||
| */ | ||
| function getSqliteConstructor(): new ( | ||
| path: string | ||
| ) => { | ||
| exec(sql: string): void; | ||
| close(): void; | ||
| } { | ||
| try { | ||
| return require("node:sqlite").DatabaseSync; | ||
| } catch (error) { | ||
| log.debug("node:sqlite unavailable, falling back to bun:sqlite", error); | ||
| return require("bun:sqlite").Database; | ||
| } | ||
| } | ||
|
|
||
| // biome-ignore lint/suspicious/noExplicitAny: resolved dynamically | ||
| const SqliteImpl: any = getSqliteConstructor(); | ||
|
|
||
| /** | ||
| * SQLite database wrapper. | ||
| * | ||
| * - `exec(sql)` — execute raw SQL (DDL, multi-statement) | ||
| * - `query(sql)` — prepare a statement → `.get()` / `.all()` / `.run()` | ||
| * - `close()` — close the connection | ||
| * - `transaction(fn)` — wrap a function in BEGIN/COMMIT/ROLLBACK | ||
| */ | ||
| export class Database { | ||
| // biome-ignore lint/suspicious/noExplicitAny: backing driver resolved at runtime | ||
| private readonly db: any; | ||
|
|
||
| constructor(path: string) { | ||
| this.db = new SqliteImpl(path); | ||
| } | ||
|
|
||
| /** Execute raw SQL (DDL statements, multi-statement strings). */ | ||
| exec(sql: string): void { | ||
| this.db.exec(sql); | ||
| } | ||
|
|
||
| /** | ||
| * Prepare a SQL statement. | ||
| * Returns a wrapper with `.get()`, `.all()`, `.run()`. | ||
| * | ||
| * Uses bun:sqlite's `.query()` (cached statements) when available, | ||
| * falling back to node:sqlite's `.prepare()`. | ||
| */ | ||
| query(sql: string): StatementWrapper { | ||
| // bun:sqlite exposes both .query() (cached) and .prepare() (fresh). | ||
| // Prefer .query() to preserve the caching semantics all consumers | ||
| // were written against. node:sqlite only has .prepare(). | ||
| const prepFn = this.db.query ?? this.db.prepare; | ||
| return wrapStatement(prepFn.call(this.db, sql)); | ||
| } | ||
|
|
||
| /** Close the database connection. */ | ||
| close(): void { | ||
| this.db.close(); | ||
| } | ||
|
|
||
| /** | ||
| * Wrap a function in a transaction. Returns a callable that executes | ||
| * the function within BEGIN/COMMIT, with ROLLBACK on error. | ||
| */ | ||
| transaction<T>(fn: () => T): () => T { | ||
| // bun:sqlite has native transaction(); node:sqlite does not | ||
| if (typeof this.db.transaction === "function") { | ||
| return this.db.transaction(fn); | ||
| } | ||
| return () => { | ||
| this.db.exec("BEGIN"); | ||
| try { | ||
| const result = fn(); | ||
| this.db.exec("COMMIT"); | ||
| return result; | ||
| } catch (error) { | ||
| this.db.exec("ROLLBACK"); | ||
| throw error; | ||
| } | ||
|
sentry-warden[bot] marked this conversation as resolved.
|
||
| }; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.