Skip to content

Provide a better-sqlite3 module backed by bun:sqlite - #36712

Open
robobun wants to merge 20 commits into
mainfrom
farm/cbc46e92/better-sqlite3-shim
Open

Provide a better-sqlite3 module backed by bun:sqlite#36712
robobun wants to merge 20 commits into
mainfrom
farm/cbc46e92/better-sqlite3-shim

Conversation

@robobun

@robobun robobun commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

What

better-sqlite3 is a V8-API native addon, so its compiled .node file cannot be loaded by Bun (process.dlopen throws ERR_DLOPEN_FAILED: "'better-sqlite3' is not yet supported in Bun", tracked in #4290). Its install script also falls back to node-gyp rebuild when no prebuilt matches the reported Node ABI, which either spends minutes compiling a binary Bun cannot use, or fails to compile against the Node headers Bun reports:

$ bun install    # with better-sqlite3@11.5.0
prebuild-install warn install No prebuilt binaries found (target=26.3.0 runtime=node arch=x64 libc= platform=linux)
./src/objects/database.lzz:416:89: error: 'const class v8::PropertyCallbackInfo<v8::Value>' has no member named 'This'
...
error: install script from "better-sqlite3" exited with 1   (~2m16s)

This adds a thirdparty override: require("better-sqlite3") / import "better-sqlite3" now resolves to a shim that wraps bun:sqlite with the better-sqlite3 Database / Statement API, including:

  • new Database(filename | Buffer, { readonly, fileMustExist, timeout, verbose, nativeBinding }) plus .name / .open / .inTransaction / .readonly / .memory
  • .prepare / .exec / .close / .pragma / .transaction (with .deferred / .immediate / .exclusive) / .serialize / .loadExtension / .defaultSafeIntegers / .unsafeMode
  • Statement: .run / .get / .all / .iterate, plus the chainable mode setters .raw() / .pluck() / .expand() / .bind() / .safeIntegers() and .columns() / .reader
  • SqliteError (with instanceof matching for bun:sqlite's SQLiteError)
  • .function / .aggregate / .table / .backup throw ERR_NOT_IMPLEMENTED with a pointer to bun:sqlite

Also:

  • Remove better-sqlite3 from the default-trusted-dependencies list so bun install no longer runs its install script by default. Users who want the real compile can still add it to trustedDependencies.
  • Fix the internal-module codegen scanner so JS builtins can require("bun:sqlite") (the src/js/bun/ directory was never mapped to the bun: specifier prefix).
  • Update the ERR_DLOPEN_FAILED message for better_sqlite3.node to point at the shim instead of just bun:sqlite.

Verification

With the reporter's project (drizzle-kit 0.27.1 + drizzle-orm 0.36.0 + better-sqlite3 11.5.0), after this change:

$ bun install
+ better-sqlite3@11.5.0
38 packages installed [4.04s]
Blocked 1 postinstall. Run `bun pm untrusted` for details.

$ bun --bun run drizzle:migrate
Reading config file './drizzle.config.ts'
[✓] migrations applied successfully!

$ bun -e 'const {Database}=require("bun:sqlite"); console.log(new Database("db.sqlite").query("SELECT name FROM sqlite_master").all())'
[ { name: "__drizzle_migrations" }, { name: "users" } ]

Install goes from ~2m16s (failed) to ~4s, and drizzle-kit migrate runs to completion.

test/js/first_party/better-sqlite3/better-sqlite3.test.ts covers the shim API surface and the specific .prepare().bind().all() / .raw(bool).all() / .transaction()[behavior]() call shapes that drizzle-kit and drizzle-orm use.

Fixes #14997
Fixes #16050


[review] gate passed · iteration 4 · 12 files touched

fails on main (without fix)
ASAN without fix: BUILD FAILED (no junit output)
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/sqlite/column-types.test.js "test/js/first_party/better-sqlite3/better-sqlite3.test.ts"
ninja: Entering directory `/workspace/bun/build/debug'
[1/182] gen cpp.rs (cppbind)
[2/182] gen BunProcess.lut.h
Generating /workspace/bun/build/debug/codegen/BunProcess.lut.h from /workspace/bun/src/jsc/bindings/BunProcess.cpp
[3/182] gen JS modules (bundle-modules)
FAILED: codegen/WebCoreJSBuiltins.cpp codegen/WebCoreJSBuiltins.h codegen/InternalModuleRegistryConstants.h codegen/InternalModuleRegistry+createInternalModuleById.h codegen/InternalModuleRegistry+enum.h codegen/InternalModuleRegistry+numberOfModules.h codegen/NativeModuleImpl.h codegen/SyntheticModuleType.h codegen/GeneratedJS2Native.h codegen/generated_js2native.rs codegen/generated_resolved_source_tag.rs codegen/InternalModuleRegistryConstants.S codegen/InternalModuleRegistryConstants.bin 
cd /workspace/bun && TARGET_PLATFORM=linux TARGET_ARCH=x64 /workspace/bun/build/release/bun run /workspace/bun/src/codegen/bundle-modules.ts --debug=ON /workspace/bun/build/debug
73 |  
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (6cf828fb5)

test/js/bun/sqlite/column-types.test.js:
(pass) SQLite Statement column types > reports correct column types for a variety of data types [0.81ms]
(pass) SQLite Statement column types > handles NULL values correctly [0.18ms]
(pass) SQLite Statement column types > reports actual column types based on data values [0.19ms]
(pass) SQLite Statement column types > reports actual types for columns from expressions [0.12ms]
(pass) SQLite Statement column types > handles multiple different expressions and functions [0.15ms]
(pass) SQLite Statement column types > shows difference between columnTypes and declaredTypes for expressions [0.10ms]
(pass) SQLite Statement column types > shows difference for dynamic column types [0.19ms]
(pass) SQLite Statement column types > columnTypes and declaredTypes are available before statement execution [0.25ms]
(pass) SQLite Statement column types > throws an error when accessing columnTypes on non-read-only statements [0.30ms]

test/js/first_party/better-sqlite3/better-sqlite3.test.ts:
(pass) better-sqlite3 shim > default export is a Database constructor [0.06ms]
(pass) better-sqlite3 shim > basic CRUD 
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/sqlite/column-types.test.js "test/js/first_party/better-sqlite3/better-sqlite3.test.ts"
bun test v1.4.0 (0ae892b8f)

test/js/bun/sqlite/column-types.test.js:
(pass) SQLite Statement column types > reports correct column types for a variety of data types [36.63ms]
(pass) SQLite Statement column types > handles NULL values correctly [4.12ms]
(pass) SQLite Statement column types > reports actual column types based on data values [5.48ms]
(pass) SQLite Statement column types > reports actual types for columns from expressions [4.41ms]
(pass) SQLite Statement column types > handles multiple different expressions and functions [6.64ms]
(pass) SQLite Statement column types > shows difference between columnTypes and declaredTypes for expressions [3.05ms]
(pass) SQLite Statement column types > shows difference for dynamic column types [4.95ms]
(pass) SQLite Statement column types > columnTypes and declaredTypes are available before statement execution [3.00ms]
(pass) SQLite Statement column types > throws an error when accessing col
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 644ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/139] gen cpp.rs (cppbind)
[2/139] gen BunProcess.lut.h
Generating /workspace/bun/build/release/codegen/BunProcess.lut.h from /workspace/bun/src/jsc/bindings/BunProcess.cpp
[3/139] gen JS modules (bundle-modules)
Preprocess modules (8722ms)
Bundle modules (62ms)
Postprocesss modules (29ms)
Bundle Functions (717ms)
Generate Code (20ms)

[9.57s] Bundled "src/js" for production
  2573 kb
  194 internal modules
  13 native modules
  90 internal functions across 19 files
[3/139] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_install v0.0.0 (/workspace/bun/src/install)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)

... (truncated)
diff hotspot
docs/runtime/sqlite.mdx                            |   2 +-
 packages/bun-types/sqlite.d.ts                     |   7 +-
 scripts/handle-crash-patterns.ts                   |   2 +-
 src/codegen/internal-module-registry-scanner.ts    |   4 +-
 src/install/default-trusted-dependencies.txt       |   1 -
 src/js/bun/sqlite.ts                               |   3 +-
 src/js/thirdparty/better-sqlite3.ts                | 435 +++++++++++++++++++++
 src/jsc/bindings/BunProcess.cpp                    |   2 +-
 src/jsc/bindings/sqlite/JSSQLStatement.cpp         |  19 +-
 src/resolve_builtins/HardcodedModule.rs            |   4 +
 test/js/bun/sqlite/column-types.test.js            |   8 +-
 .../better-sqlite3/better-sqlite3.test.ts          | 383 ++++++++++++++++++
 12 files changed, 847 insertions(+), 23 deletions(-)

gate history · 12 passed · 0 rejected · iteration 4

evidence per changed file
file                                                      reads  edits  tests
docs/runtime/sqlite.mdx                                       1      1      0
packages/bun-types/sqlite.d.ts                                1      1      0
scripts/handle-crash-patterns.ts                              1      1      0
src/codegen/internal-module-registry-scanner.ts               3      2      0
src/install/default-trusted-dependencies.txt                  1      1      0
src/js/bun/sqlite.ts                                          4      2      0
src/js/thirdparty/better-sqlite3.ts                          22     56      0
src/jsc/bindings/BunProcess.cpp                               1      1      0
src/jsc/bindings/sqlite/JSSQLStatement.cpp                    2      5      0
src/resolve_builtins/HardcodedModule.rs                       1      3      0
test/js/bun/sqlite/column-types.test.js                       1      1      0
…st/js/first_party/better-sqlite3/better-sqlite3.test.ts      2     18      0

better-sqlite3 is a V8-API native addon, so its compiled .node file cannot
be loaded by Bun (dlopen throws ERR_DLOPEN_FAILED, see #4290). Its install
script also falls back to node-gyp when no prebuilt matches Bun's reported
Node ABI, which either compiles a useless binary for minutes or fails
outright against newer Node headers.

This adds a thirdparty override: require('better-sqlite3') resolves to a
shim that wraps bun:sqlite with the better-sqlite3 Database/Statement API
(including chainable .raw/.pluck/.bind, .pragma, .columns, and the
.deferred/.immediate/.exclusive transaction variants). Packages that
depend on better-sqlite3 (drizzle-kit, drizzle-orm, Kysely, knex) now
work without a native compile step.

Also:
- Drop better-sqlite3 from the default-trusted list so bun install no
  longer runs its install script by default.
- Fix the internal-module scanner so builtins can require('bun:sqlite')
  (the 'bun/' directory was never mapped to the 'bun:' prefix).
- Update the ERR_DLOPEN_FAILED message for better_sqlite3.node to point
  at the shim.

Fixes #14997
Fixes #16050
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Bun now resolves better-sqlite3 to a compatibility module backed by bun:sqlite. The module implements database, statement, transaction, row, error, persistence, and safe-integer APIs. SQLite statements expose read-only metadata and pre-execution declared types.

Changes

better-sqlite3 compatibility

Layer / File(s) Summary
Module resolution and SQLite bindings
src/codegen/internal-module-registry-scanner.ts, src/resolve_builtins/HardcodedModule.rs, src/js/bun/sqlite.ts, src/jsc/bindings/sqlite/JSSQLStatement.cpp, test/js/bun/sqlite/column-types.test.js, docs/runtime/sqlite.mdx, packages/bun-types/sqlite.d.ts
Bun recognizes bun/ module paths and maps better-sqlite3 to the built-in implementation. SQLStatement.prototype.readonly is available, and declaredTypes can be read before execution.
better-sqlite3 compatibility API
src/js/thirdparty/better-sqlite3.ts
Adds Statement, Database, and SqliteError compatibility APIs backed by bun:sqlite. The APIs cover binding, row modes, transactions, pragmas, persistence, safe integers, and explicit unsupported-operation errors.
Compatibility validation
test/js/first_party/better-sqlite3/better-sqlite3.test.ts
Tests module exports, CRUD operations, statement modes, transactions, metadata, errors, options, file persistence, unsupported methods, and Drizzle-style usage.
Runtime diagnostics
src/jsc/bindings/BunProcess.cpp, scripts/handle-crash-patterns.ts
Native addon and crash-pattern messages direct users to Bun’s built-in better-sqlite3 implementation and its supported loading form.

Possibly related issues

Possibly related PRs

Suggested reviewers: alii

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address both linked issues by avoiding unusable native compilation and enabling better-sqlite3 and Drizzle migrations in Bun.
Out of Scope Changes check ✅ Passed The changes support the shim, SQLite compatibility, installation behavior, diagnostics, documentation, and related tests without unrelated code.
Title check ✅ Passed The title clearly identifies the primary change: adding a better-sqlite3 module backed by bun:sqlite.
Description check ✅ Passed The description explains the change, scope, compatibility limits, linked issues, and verification results, despite using different section headings than the template.

Comment @coderabbitai help to get the list of available commands.

Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. better-sqlite3 .node exe is not bundled #8895 - The shim makes the native .node file unnecessary, so the bundling problem (better-sqlite3 .node exe is not bundled) goes away entirely

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #8895

🤖 Generated with Claude Code

Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Comment thread test/js/first_party/better-sqlite3/better-sqlite3.test.ts Outdated
- .raw()/.pluck()/.expand() iterate() now uses values() so duplicate
  column names (SELECT a.id, b.id FROM a JOIN b) keep their positional
  values instead of collapsing through an object key.
- Expose sqlite3_stmt_readonly() as a native 'readonly' getter on the
  bun:sqlite statement and delegate Statement#readonly to it, so
  INSERT/UPDATE/DELETE correctly report readonly === false.
- Use the shared throwNotImplemented helper for the stubbed methods.
- Tighten the fileMustExist toThrow() assertion to expect SqliteError.
@robobun

robobun commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 11:05 PM PT - Aug 1st, 2026

@robobun, your commit 0ae892b has 2 failures in Build #87528 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 36712

That installs a local version of the PR into your bun-36712 executable, so you can run:

bun-36712 --bun

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/codegen/internal-module-registry-scanner.ts`:
- Line 18: Update the internal registry lookup in the scanner to test whether
the result is undefined rather than relying on truthiness, so module ID 0 from
internalRegistry.get("bun:ffi") is treated as a valid match. Preserve the
existing generated bun: key fallback and resolution behavior for actual misses.

In `@src/js/thirdparty/better-sqlite3.ts`:
- Line 216: Update the verbose handling in the better-sqlite3 shim so the
validated callback is invoked with the SQL source in both the prepare and exec
paths. Preserve the existing validation and ensure callers passing verbose
receive query logging rather than silently being ignored.
- Around line 76-87: Update get() to fetch only one row through the statement’s
single-row API, then apply the existing raw/pluck/expand mapping without
materializing values(). Update iterate() to consume this.#stmt.iterate() lazily
and map each yielded row. Retain values() only for the duplicate-column
positional case covered by the existing test, preserving lazy cursor behavior
otherwise.
- Around line 315-317: Update the unsafeMode method so calling unsafeMode(true)
throws an explicit unsupported-operation error, while unsafeMode(false) remains
a no-op that returns the current database instance.

In `@test/js/first_party/better-sqlite3/better-sqlite3.test.ts`:
- Around line 198-210: Add tests alongside the existing better-sqlite3 shim
coverage for expand() and expand().all() using the $ bucket, Database
constructed from a serialized buffer, statement safeIntegers(true), database
defaultSafeIntegers(true), serialize() round-tripping, and
Statement[Symbol.iterator] iteration. Exercise each entry point through its
expected behavior while preserving the current tests and keeping the cases near
their related variant coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 354ef716-de6a-4e53-8af7-25272d62e2be

📥 Commits

Reviewing files that changed from the base of the PR and between f91d5c9 and 3e0e897.

📒 Files selected for processing (8)
  • src/codegen/internal-module-registry-scanner.ts
  • src/install/default-trusted-dependencies.txt
  • src/js/bun/sqlite.ts
  • src/js/thirdparty/better-sqlite3.ts
  • src/jsc/bindings/BunProcess.cpp
  • src/jsc/bindings/sqlite/JSSQLStatement.cpp
  • src/resolve_builtins/HardcodedModule.rs
  • test/js/first_party/better-sqlite3/better-sqlite3.test.ts
💤 Files with no reviewable changes (1)
  • src/install/default-trusted-dependencies.txt

Comment thread src/codegen/internal-module-registry-scanner.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Comment thread test/js/first_party/better-sqlite3/better-sqlite3.test.ts
- internal-module-registry scanner: treat module id 0 as a hit (bun/ffi.ts
  is alphabetically first and now receives id 0).
- Wire the verbose callback so it is invoked with the SQL source on each
  run/get/all/iterate/exec.
- unsafeMode(true) now throws ERR_NOT_IMPLEMENTED since bun:sqlite keeps
  SQLITE_DBCONFIG_DEFENSIVE on; unsafeMode(false) remains a no-op.
- Add tests for expand(), Symbol.iterator, serialize()/Buffer round-trip,
  safeIntegers()/defaultSafeIntegers(), verbose, and unsafeMode.
Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Comment thread src/codegen/internal-module-registry-scanner.ts
Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Comment thread src/js/thirdparty/better-sqlite3.ts
- Named parameters (@name/:name/$name) bound via { name: value } now match
  instead of silently binding NULL, and unknown keys throw.
- Remove the dead try/catch around PRAGMA busy_timeout (cannot fail on a
  freshly-opened handle with a validated integer).
- bun:sqlite Database#transaction(fn, self): use self for the returned
  function's .database back-reference when provided, so the better-sqlite3
  shim's tx.database points at the shim instance instead of the internal
  bun:sqlite handle.
Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Comment thread src/js/thirdparty/better-sqlite3.ts
- Remove the unreachable fallback branch in all() and fold all three mode
  methods into a single #mapRow(row) call.
- Drop the unused #safeIntegers field and constructor parameter.
- Wire verbose into pragma() too (test extended).

@claude claude Bot left a comment

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.

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/js/thirdparty/better-sqlite3.ts:381-388 — The third clause SQLiteError[Symbol.hasInstance](instance) is redundant with the second: bun:sqlite's SQLiteError[Symbol.hasInstance] is exactly instance?.name === "SQLiteError", and the outer guard already ensures instance is a non-null object, so it reduces byte-for-byte to instance.name === "SQLiteError". Drop one of the two — or, if the delegation is intentional future-proofing against bun:sqlite switching to a brand check, drop the string compare instead so there's exactly one source of truth.

    Extended reasoning...

    What the issue is

    SqliteError[Symbol.hasInstance] at src/js/thirdparty/better-sqlite3.ts:381-388 accepts an instance when any of three conditions hold:

    instance != null &&
    typeof instance === "object" &&
    (instance.name === "SqliteError" || instance.name === "SQLiteError" || SQLiteError[Symbol.hasInstance](instance))

    The third disjunct calls bun:sqlite's SQLiteError[Symbol.hasInstance], whose entire implementation (src/js/bun/sqlite.ts) is:

    static [Symbol.hasInstance](instance) {
      return instance?.name === "SQLiteError";
    }

    The outer guard on lines 383-384 has already established instance != null && typeof instance === "object", so the optional-chain in instance?.name is a no-op and the delegated call reduces to exactly instance.name === "SQLiteError" — byte-identical to the second clause. The third clause can therefore never make the disjunction true when the second is false; it is provably dead code introduced in this PR.

    Why nothing prevents it

    SQLiteError is imported at the top of the file from bun:sqlite and its [Symbol.hasInstance] is a static class method — there is no runtime configuration that changes what it does. For any non-hostile input (i.e., .name is not a getter that flips between reads), evaluating the second clause and the third clause produces the same boolean.

    Step-by-step proof

    Take err thrown by db.exec("NOT VALID SQL") (a bun:sqlite SQLiteError with .name === "SQLiteError"), and evaluate err instanceof SqliteError:

    1. Line 383: err != null → true.
    2. Line 384: typeof err === "object" → true.
    3. Clause 1: err.name === "SqliteError" → false (capital L).
    4. Clause 2: err.name === "SQLiteError"true. Short-circuit; result is true.

    Now take any err with .name === "Foo":

    1. Guard passes.
    2. Clause 1: false. Clause 2: false.
    3. Clause 3: SQLiteError[Symbol.hasInstance](err)err?.name === "SQLiteError""Foo" === "SQLiteError"false.

    For clause 3 to add anything, there would need to exist an instance where instance.name === "SQLiteError" is false but instance?.name === "SQLiteError" is true — which is impossible for a non-null object whose .name is a plain data property.

    Impact

    None at runtime — one redundant property read + string compare per instanceof SqliteError check. This is purely code hygiene in a new file this PR introduces. REVIEW.md's dead-code rule: "Every line you add must be demonstrably live."

    How to fix

    The comment on line 379 ("Let err instanceof SqliteError match bun:sqlite's SQLiteError too") suggests the intent is delegation. Two consistent fixes:

    • Keep the delegation, drop the string compare — remove instance.name === "SQLiteError" || and keep SQLiteError[Symbol.hasInstance](instance). This future-proofs the shim if bun:sqlite ever switches its hasInstance to a real brand check; the shim stays correct without a matching edit.
    • Keep the string compare, drop the delegation — remove || SQLiteError[Symbol.hasInstance](instance). Simpler and avoids the extra call, but hardcodes bun:sqlite's current implementation detail here.

    Either way, one of the two should go so the disjunction contains no clause that is provably subsumed by another.

Comment thread src/jsc/bindings/BunProcess.cpp
Comment thread src/js/thirdparty/better-sqlite3.ts
… message

- sqlite3_column_decltype() reads schema metadata and does not require a
  stepped row, so remove the hasExecuted guard from the native
  declaredTypes getter. The shim's columns() now reads it directly
  (type: 'INTEGER'/'TEXT'/... on a freshly prepared statement) instead of
  swallowing the throw and returning null. column-types.test.js updated.
- Drop the redundant name === 'SQLiteError' clause from the SqliteError
  Symbol.hasInstance; the delegation to bun:sqlite's SQLiteError covers it.
- Update scripts/handle-crash-patterns.ts to point better-sqlite3 crash
  reporters at the built-in module instead of saying it is unsupported.

@claude claude Bot left a comment

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.

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/js/thirdparty/better-sqlite3.ts:358-366 — The third disjunct SQLiteError[Symbol.hasInstance](instance) is provably dead: bun:sqlite defines it as instance?.name === "SQLiteError", and after the outer instance != null && typeof instance === "object" guard that reduces to exactly instance.name === "SQLiteError" — the second disjunct immediately preceding it. Drop the third disjunct so the parenthesized check is just (instance.name === "SqliteError" || instance.name === "SQLiteError").

    Extended reasoning...

    What the issue is

    The shim's SqliteError[Symbol.hasInstance] at lines 358-366 checks three disjuncts after establishing instance != null && typeof instance === "object":

    (instance.name === "SqliteError" || instance.name === "SQLiteError" || SQLiteError[Symbol.hasInstance](instance))

    The third disjunct calls into bun:sqlite's SQLiteError[Symbol.hasInstance], which is defined in src/js/bun/sqlite.ts as:

    static [Symbol.hasInstance](instance) {
      return instance?.name === "SQLiteError";
    }

    Why it can never be the deciding term

    By the time the third disjunct is evaluated, two facts hold:

    1. The outer guard has established instance != null && typeof instance === "object", so the optional-chain in instance?.name is moot — it's just instance.name.
    2. || short-circuits, so the second disjunct instance.name === "SQLiteError" has already evaluated false.

    Substituting: the third disjunct evaluates instance.name === "SQLiteError", which fact (2) has already established is false. It can never be true when reached, so it is provably dead.

    There is no path around this. SQLiteError is captured at module load from require("bun:sqlite") (line 2), and bun:sqlite's SQLiteError class is a fixed built-in whose Symbol.hasInstance is not user-overridable — no reentrant user code can change what the third disjunct computes.

    Step-by-step proof

    Take a bun:sqlite-thrown error e with e.name === "SQLiteError":

    1. Outer guard: e != null ✓, typeof e === "object" ✓ → enter parenthesized OR.
    2. First disjunct: "SQLiteError" === "SqliteError" → false.
    3. Second disjunct: "SQLiteError" === "SQLiteError"true, return true. Third disjunct never evaluated.

    Take any object o with o.name === "other":

    1. Outer guard passes.
    2. First disjunct: false. Second disjunct: false.
    3. Third disjunct: SQLiteError[Symbol.hasInstance](o)o?.name === "SQLiteError""other" === "SQLiteError"false. Return false.

    For every possible instance.name, the third disjunct either isn't reached (name is "SQLiteError") or evaluates to false (name is anything else). Deleting it changes no observable behavior — the SqliteError test in better-sqlite3.test.ts (which checks both SqliteError("oops", "SQLITE_TEST") instanceof SqliteError and a bun:sqlite-thrown SQLiteError instanceof SqliteError) continues to pass.

    Why flag it

    REVIEW.md's dead-code rule is explicit: "Every line you add must be demonstrably live" and "Delete dead code in the same PR that makes it dead." This is a new file introduced by this PR, so the rule applies directly. The comment on line 357 ("Let err instanceof SqliteError match bun:sqlite's SQLiteError") describes the intent the second disjunct already fully implements — the third disjunct looks like belt-and-suspenders that predates confirming what bun:sqlite's hasInstance actually does.

    Impact

    None at runtime — pure code hygiene. One redundant cross-module property lookup + call on the (rare) miss path.

    Fix

    Drop the third disjunct:

    (instance.name === "SqliteError" || instance.name === "SQLiteError")

Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Comment thread src/js/thirdparty/better-sqlite3.ts Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/js/thirdparty/better-sqlite3.ts (1)

102-111: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate bound parameters when iterate() is called.

Because iterate() is a generator, #params(args) runs only on the first .next(). stmt.bind(1).iterate(2) must throw "This statement already has bound parameters" during the iterate() call. Move #params(args) outside the generator. Keep #trace() with row production because upstream invokes its logger on the first .next().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/thirdparty/better-sqlite3.ts` around lines 102 - 111, Update the
statement iterate flow around iterate and `#params` so bound-parameter validation
occurs immediately when iterate() is called, before generator execution;
preserve the existing "This statement already has bound parameters" error for
calls such as stmt.bind(1).iterate(2). Keep `#trace`() inside the generator so
logging still occurs on the first .next(), and continue using the validated
parameters for row production.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/js/thirdparty/better-sqlite3.ts`:
- Around line 102-111: Update the statement iterate flow around iterate and
`#params` so bound-parameter validation occurs immediately when iterate() is
called, before generator execution; preserve the existing "This statement
already has bound parameters" error for calls such as stmt.bind(1).iterate(2).
Keep `#trace`() inside the generator so logging still occurs on the first .next(),
and continue using the validated parameters for row production.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 653ebfb9-5251-4291-894e-fa01bf7b0c57

📥 Commits

Reviewing files that changed from the base of the PR and between 3e0e897 and 340368c.

📒 Files selected for processing (7)
  • scripts/handle-crash-patterns.ts
  • src/codegen/internal-module-registry-scanner.ts
  • src/js/bun/sqlite.ts
  • src/js/thirdparty/better-sqlite3.ts
  • src/jsc/bindings/sqlite/JSSQLStatement.cpp
  • test/js/bun/sqlite/column-types.test.js
  • test/js/first_party/better-sqlite3/better-sqlite3.test.ts
💤 Files with no reviewable changes (1)
  • src/jsc/bindings/sqlite/JSSQLStatement.cpp

Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/jsc/bindings/sqlite/JSSQLStatement.cpp
Comment thread src/js/thirdparty/better-sqlite3.ts
- db.exec('')/db.exec('-- comment') now return this instead of throwing,
  matching sqlite3_exec()'s documented no-op.
- pragma() applies defaultSafeIntegers to its internal statement.
- Update packages/bun-types/sqlite.d.ts and docs/runtime/sqlite.mdx to
  reflect that declaredTypes no longer requires the statement to have
  been executed.
@robobun
robobun requested a review from alii as a code owner August 1, 2026 13:50

@claude claude Bot left a comment

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.

Beyond the inline findings, I also checked the internal-module-registry-scanner.ts change for wider impact: the startsWith("bun/") fix newly registers bun:ffi/bun:sql/bun:sqlite in the internal registry (previously the branch never matched), and none of those collide with _NativeModule.h entries; the directMatch !== undefined fix is needed because bun/ffi.ts now sorts to index 0. No other builtin currently require("bun:ffi"|"bun:sql"|"bun:sqlite"), so no existing module changes behavior.

Extended reasoning...

The three inline findings this run are all nit-severity compat divergences in the shim (O(N) materialization for .raw()/.pluck()/.expand() in iterate()/get(), missing REQUIRE_STATEMENT_RETURNS_DATA guard, and verbose passing raw source instead of sqlite3_expanded_sql). None block the drizzle path the PR targets. Separately I traced the codegen scanner edit — it's a shared build-time file, so I confirmed the newly-registered bun: specifiers don't overlap _NativeModule.h and no existing src/js file was already requiring them, so the change is scoped to enabling the new shim's require("bun:sqlite").

Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
- get/all/iterate/pluck/raw/expand/columns now throw
  'This statement does not return data. Use run() instead' when the
  prepared statement has zero result columns, matching better-sqlite3's
  REQUIRE_STATEMENT_RETURNS_DATA guard. A DELETE accessed via .all() is
  refused rather than executed.
- The verbose callback now receives sqlite3_expanded_sql() (via
  Statement#toString()) after parameters are bound, so
  'INSERT INTO t VALUES (?)'.run(1) logs 'INSERT INTO t VALUES (1)'.
Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
…Names

- run/get/all/iterate now wrap the native call in try/finally so verbose
  fires for failing statements too (matching sqlite3_trace_v2 firing at
  step start), and iterate() pumps the first .next() before tracing so
  toString() reflects the current bindings in object mode.
- #expandRow caches columnNames once per Statement instead of re-reading
  the native getter per row.
Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
- All verbose invocations now go through a single guarded trace() closure
  so a throwing logger cannot replace the primary result or mask an
  underlying SQLiteError.
- .bind() copies TypedArray/Buffer arguments so mutating the caller's
  buffer before execution no longer changes what is inserted
  (SQLITE_TRANSIENT semantics).
@robobun

robobun commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Known divergences from better-sqlite3

These are intentional gaps left for follow-up; none affect the drizzle-kit / drizzle-orm path this PR targets.

Area Divergence Why
.function() / .aggregate() / .table() / .backup() / .unsafeMode(true) Throw ERR_NOT_IMPLEMENTED Need native support in bun:sqlite
.expand() Groups all columns under $ instead of per-table sqlite3_column_table_name is not exposed
.raw()/.pluck()/.expand() with .get() / .iterate() Fully materialize via values() instead of stepping once / lazily bun:sqlite has no positional single-row or per-row-array iterator; using the object iterator collapses duplicate column names (see #discussion_r3695501016)
Statement#busy Always false; no busy guard on re-entry Requires a JS-side flag around #iterate; failure mode needs manual interleaving of an open iterator with other calls on the same Statement
.iterate() early break then re-iterate same Statement Second iteration may resume where the first stopped instead of restarting bun:sqlite has no reset() primitive; native iterate() skips reset when sqlite3_stmt_busy() is true
.bind() Parameter-count / type validation is deferred to execution; only positional Uint8Array arguments are snapshotted bun:sqlite has no bind-only entry point. Named-parameter-object values are not deep-copied.
Named parameters Object keys must omit the @/:/$ prefix ({name: 'x'} for @name); {'@name': 'x'} throws Missing parameter "name" bun:sqlite's strict-mode binder strips the prefix and looks up only the stripped key with no prefixed fallback. Real better-sqlite3 accepts both forms.
.prepare("A; B") Prepares only A and drops B instead of throwing RangeError bun:sqlite's prepare passes nullptr for pzTail; no native multi-statement detection wired yet
Post-close() method calls Surface a native bun:sqlite error (or silently succeed for defaultSafeIntegers) instead of TypeError('The database connection is not open') isOpen is tracked but not checked in every method; post-close use is a caller bug
verbose during .transaction() BEGIN/COMMIT/ROLLBACK/SAVEPOINT are not traced They run via bun:sqlite's cached transaction controller, which the shim does not wrap
.run() under safeIntegers changes is a number, only lastInsertRowid is a bigint bun:sqlite's native Changes.changes is unconditionally jsNumber(); no precision hazard (sqlite3_total_changes is 32-bit)
Database method placement Methods are own enumerable properties, not on Database.prototype Closures capture per-instance state (db/isOpen/defaultSafeIntegers/trace). Prototype-patching instrumentation (OTel/dd-trace better-sqlite3 plugins) is silently shadowed; Object.keys(db) includes method names.

Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/js/thirdparty/better-sqlite3.ts (1)

158-167: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

bind() snapshot misses Uint8Array/Buffer values nested in named-parameter objects.

The loop only converts a top-level array element when args[i] instanceof Uint8Array. When a caller binds named parameters through a single object argument, for example stmt.bind({ $blob: buf }), args[0] is a plain object and the check does not descend into its properties. A Uint8Array/Buffer value inside that object is not snapshotted, so mutating the buffer after .bind() and before .run()/.get() produces stale or corrupted data for named blob parameters, even though positional blob parameters are correctly snapshotted.

🛡️ Proposed fix to also snapshot named-parameter object values
     for (let i = 0; i < args.length; i++) {
-      if (args[i] instanceof Uint8Array) args[i] = Buffer.from(args[i]);
+      if (args[i] instanceof Uint8Array) {
+        args[i] = Buffer.from(args[i]);
+      } else if (args[i] !== null && typeof args[i] === "object" && !Array.isArray(args[i])) {
+        const obj = args[i];
+        for (const key in obj) {
+          if (obj[key] instanceof Uint8Array) obj[key] = Buffer.from(obj[key]);
+        }
+      }
     }

Consider adding a test alongside test/js/first_party/better-sqlite3/better-sqlite3.test.ts Lines 78-85 that exercises .bind({ $blob: buf }) with a mutated buffer, to lock in the same SQLITE_TRANSIENT-style guarantee for named parameters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js/thirdparty/better-sqlite3.ts` around lines 158 - 167, Update bind() to
snapshot Uint8Array/Buffer values inside named-parameter objects as well as
top-level positional arguments, while preserving the existing single-bind guard
and fluent return. Ensure object properties are converted before assigning
this.#bound, and add coverage in the better-sqlite3 tests for mutating a buffer
after bind({ $blob: buf }) but before execution.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/thirdparty/better-sqlite3.ts`:
- Around line 299-311: Update Database#exec, specifically the this.exec
function, to avoid relying on the exact error message when handling
whitespace/comment-only SQL. Detect empty/comment-only input before calling
db.run(), or introduce and use a stable error code/property for that condition,
while preserving propagation of all other database errors.

---

Outside diff comments:
In `@src/js/thirdparty/better-sqlite3.ts`:
- Around line 158-167: Update bind() to snapshot Uint8Array/Buffer values inside
named-parameter objects as well as top-level positional arguments, while
preserving the existing single-bind guard and fluent return. Ensure object
properties are converted before assigning this.#bound, and add coverage in the
better-sqlite3 tests for mutating a buffer after bind({ $blob: buf }) but before
execution.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 01d44001-23ae-42af-8489-8973f57d5adc

📥 Commits

Reviewing files that changed from the base of the PR and between 340368c and 166cc71.

📒 Files selected for processing (4)
  • docs/runtime/sqlite.mdx
  • packages/bun-types/sqlite.d.ts
  • src/js/thirdparty/better-sqlite3.ts
  • test/js/first_party/better-sqlite3/better-sqlite3.test.ts

Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
@robobun

robobun commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Build 87528 (0ae892b, the FinalizationRegistry commit): better-sqlite3.test.ts passes on all lanes. The two red failures are unrelated and marked [pre-existing]/main breaks (reported for triage):

  • test/cli/install/bun-upgrade.test.ts on Windows 11 aarch64: "Canary builds are not available for this platform yet"
  • test/regression/issue/36577.test.ts on Windows 2019 x64: install frozen-lockfile regression, pre-existing on main

Everything else is flaky (passed on retry/alone). Diff is green on the lanes it touches; ready for review.

On Windows the database file handle stays open until every sqlite3_stmt
is finalized, so the using-tempDir cleanup in the fileMustExist test hit
EBUSY. Track the bun:sqlite statements created via prepare() and
finalize them in close(), matching better-sqlite3's close() which
iterates sqlite3_next_stmt.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/thirdparty/better-sqlite3.ts`:
- Line 272: Replace the strong Set-based tracking in Database.prepare and
Database.close with lifecycle-aware tracking that allows unreachable raw
statements to be released while retaining and finalizing live statements on
close. Preserve native finalize behavior and add a regression test covering
repeated prepare-and-discard cycles to verify resources are not retained for the
database lifetime.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8b418eb3-edab-4ccb-9e63-03dd6bff5cc0

📥 Commits

Reviewing files that changed from the base of the PR and between 166cc71 and 8fb5753.

📒 Files selected for processing (1)
  • src/js/thirdparty/better-sqlite3.ts

Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts
Comment thread src/js/thirdparty/better-sqlite3.ts Outdated
Long-running servers that never call close() would otherwise accumulate
one WeakRef + Set slot per prepare() call unboundedly. Follows the same
shape as src/js/node/diagnostics_channel.ts WeakRefMap.

@claude claude Bot left a comment

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.

The FinalizationRegistry in 0ae892b resolves the dead-WeakRef accumulation I flagged last pass; no further findings from this run. Deferring to a maintainer for the product/API calls this PR makes — the documented known-divergences table, the own-property (vs prototype) method placement, and dropping better-sqlite3 from default-trusted-dependencies.txt.

Checked this pass:

  • Statement lifecycle: WeakRef + FinalizationRegistry bounds the tracking Set to live statements; close() derefs and finalizes survivors, and post-clear registry callbacks are harmless no-ops.
  • declaredTypes hasExecuted-guard removal — sqlite3_column_decltype reads schema metadata and does not require a stepped row; docs/types updated to match.
  • Codegen scanner: the "bun/" prefix fix and directMatch !== undefined (index 0 was previously falsy-rejected) both look correct.
  • .name === ':memory:' for Buffer input was examined and ruled out as a bug.
Extended reasoning...

Overview

This PR adds a hardcoded better-sqlite3 module override (src/js/thirdparty/better-sqlite3.ts, ~435 lines) that wraps bun:sqlite with the better-sqlite3 Database/Statement API, so require('better-sqlite3') works without the V8-API native addon. Supporting changes: a native readonly getter on JSSQLStatement and removal of the hasExecuted guard on declaredTypes; module-resolver entries in HardcodedModule.rs; a codegen scanner fix so builtins can require('bun:sqlite'); removal of better-sqlite3 from default-trusted-dependencies.txt; updated dlopen error text; docs/types; and a 383-line test file plus updates to column-types.test.js.

Level of scrutiny

High — this is new user-facing API surface with intentional, documented divergences from the reference implementation (prototype-vs-own methods affecting APM instrumentation, prefixed named-parameter binding, .expand() grouping under $, changes type under safeIntegers, iterator early-return not resetting the native stmt, etc.). Those are all recorded in the PR's known-divergences comment and have been discussed across ~200 timeline events, but which ones are acceptable to ship is a maintainer call. Removing better-sqlite3 from the default-trusted list is also a product decision that changes bun install behavior for existing projects that opted into the real native build.

Security risks

Low. The shim delegates to bun:sqlite; the only filesystem interaction is existsSync(dirname(resolve(filename))) mirroring better-sqlite3's own directory check. loadExtension passes through to the existing bun:sqlite path (no new capability). nativeBinding is accepted and ignored. No new native allocation paths beyond the trivial sqlite3_stmt_readonly getter.

Other factors

The bug hunter found nothing this run. Over the PR's lifetime I raised a series of findings (raw/pluck iterate reading positional values, .readonly correctness, Buffer.from element-wise copy on wide TypedArrays, strong-ref statement leak, then dead-WeakRef accumulation) — each was fixed or logged as a known divergence, and 0ae892b closed the last one. Test coverage for the shim is thorough for the drizzle call shapes it targets and CI is green per the author's build-87520 note (the one Windows-aarch64 failure is unrelated). Given the scope and the design-tradeoff acceptances baked into the known-divergences table, this warrants a human maintainer's sign-off rather than auto-approval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants