Skip to content
Open
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
4a170f5
node:sqlite: add initial module + process.versions.sqlite (xi58t8)
robobun Apr 28, 2026
f929f05
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 28, 2026
dec6507
node:sqlite: fix lint (unused field, unused parameter)
robobun Apr 28, 2026
0eddfab
node:sqlite: rewrite in C++ (NodeSqlite.cpp)
robobun Apr 28, 2026
8a18929
node:sqlite: always build bundled sqlite3.c; include sqlite3_local.h …
robobun Apr 28, 2026
b3800ae
node:sqlite: forward-declare sqlite3 types in header; drop unused param
robobun Apr 28, 2026
bf2650c
[skip size check] node:sqlite: acknowledge +1.6MB on macOS for bundle…
robobun Apr 28, 2026
a68db4a
node:sqlite: tighten input validation to match Node.js
robobun Apr 28, 2026
83a0f85
Merge branch 'main' into farm/ed7cafd7/node-sqlite
dylan-conway Apr 28, 2026
2a2c0c7
node:sqlite: implement the full API (UDFs, aggregates, Session, itera…
robobun Apr 29, 2026
7716574
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 29, 2026
b2f0425
node:sqlite: address review — move sqlite3_version to NodeSqlite TU; …
robobun Apr 29, 2026
25f9045
Merge branch 'farm/ed7cafd7/node-sqlite' of https://github.com/oven-s…
robobun Apr 29, 2026
12e685d
node:sqlite test: shrink backup() tests + raise timeout for slow-fsyn…
robobun Apr 29, 2026
f3d95f1
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 29, 2026
250fc76
node:sqlite backup(): bound BUSY/LOCKED retry with backoff instead of…
robobun Apr 29, 2026
a1287ce
Merge branch 'farm/ed7cafd7/node-sqlite' of https://github.com/oven-s…
robobun Apr 29, 2026
4e8e24a
node:sqlite: use TopExceptionScope in sqlite callbacks to satisfy val…
robobun Apr 29, 2026
a44b9b0
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 29, 2026
2d2a796
node:sqlite test: force GC after closing file-backed db so tempDir ca…
robobun Apr 29, 2026
96daac9
node:sqlite: guard Session against use-after-free across close()+open()
robobun Apr 29, 2026
4e1d327
node:sqlite: use open-generation counter instead of sqlite3* to detec…
robobun Apr 29, 2026
80d8d33
node:sqlite: iterator return() tolerates finalized statement
robobun Apr 29, 2026
9e04f81
node:sqlite: check iterator done() before isFinalized(); bind int32 a…
robobun Apr 29, 2026
a1c79fb
node:sqlite: route int32 UDF return values through sqlite3_result_int
robobun Apr 29, 2026
5eb0d95
Merge remote-tracking branch 'origin/main' into farm/ed7cafd7/node-sq…
robobun Apr 29, 2026
4f8df1c
node:sqlite: don't double-free UDF/aggregate context on registration …
robobun Apr 29, 2026
f255534
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 29, 2026
7b655ae
node:sqlite run(): read changes64 via sqlite3_db_handle(stmt), not th…
robobun Apr 29, 2026
8b4565d
test: bump builtinModules length for node:sqlite
robobun Apr 29, 2026
78f8f22
node:sqlite: guard against re-entrant db.close() and hostile callbacks
robobun Apr 29, 2026
266d9c3
node:sqlite: drop SQLITE_OPEN_URI from open()/backup()
robobun Apr 29, 2026
9fc9e16
node:sqlite: clamp backup() rate=0 to 1; validate aggregate options.r…
robobun Apr 29, 2026
a1ee62a
test/common: re-export hasSQLite from index.mjs alongside skipIfSQLit…
robobun Apr 29, 2026
0ebf660
[skip size check] node:sqlite: acknowledge ~185KB Windows increase fo…
robobun Apr 29, 2026
3ce63f3
node:sqlite: complete the API — setAuthorizer, limits, serialize/dese…
robobun Apr 29, 2026
b2bb72b
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 29, 2026
b7b5fbd
ci: retry — darwin-aarch64 flaked on GitHub 502 fetching highway/hdrh…
robobun Apr 29, 2026
696d0ab
node:sqlite: backup() — report sqlite3_backup_step()'s return code, n…
robobun Apr 29, 2026
d826e92
Merge remote-tracking branch 'origin/main'
robobun Apr 29, 2026
54a50e8
node:sqlite: address review — share bindValue(), last-wins duplicate …
robobun Apr 29, 2026
f62f0f2
node:sqlite: guard TagStore LRU against concurrent GC; BusyScope dese…
robobun Apr 29, 2026
ff63622
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 29, 2026
72cfee9
[skip size check] node:sqlite: acknowledge darwin +1.8 MB — bundled s…
robobun Apr 30, 2026
d3636ac
Merge origin/main (src/bun.js → src/jsc rename)
robobun May 7, 2026
7da13ac
node:sqlite: free tracked sessions on deserialize(); surface authoriz…
robobun May 7, 2026
b6ae651
[autofix.ci] apply automated fixes
autofix-ci[bot] May 7, 2026
e996adb
node:sqlite: deserialize() — capture input span after option getters;…
robobun May 7, 2026
1bbe87a
node:sqlite: rebuild row-structure cache per reset; handle index-stri…
robobun May 7, 2026
b7140d3
node:sqlite: all() — read column count after step(), not before
robobun May 7, 2026
6c723c7
node:sqlite: TagStore — drop spurious sqlite3_reset() error check; ex…
robobun May 7, 2026
a46b1f8
Merge branch 'farm/ed7cafd7/node-sqlite' of https://github.com/oven-s…
cirospaciari Jun 18, 2026
f79754b
node:sqlite: register the module in the Rust HardcodedModule tables
cirospaciari Jun 18, 2026
37a6f6c
sqlite: update bundled amalgamation to 3.53.2 and enable the percenti…
cirospaciari Jun 18, 2026
6cab772
test: sync the node:sqlite suite to Node v26.3.0
cirospaciari Jun 18, 2026
90025c2
ErrorCode: append ERR_SQLITE_ERROR at the end of the table and mirror…
cirospaciari Jun 18, 2026
4481f87
sqlite: make BusyScope non-copyable and use sqlite3_close_v2 in the t…
cirospaciari Jun 18, 2026
7a6477b
sqlite: document the macOS binary-size cost of bundling [skip size ch…
cirospaciari Jun 19, 2026
40d501d
sqlite: review nits — fix stale limit-count comment, tidy node-sqlite…
cirospaciari Jun 19, 2026
0b75f9c
node:sqlite: collectable UDF callbacks, GC-safe session cleanup, keep…
cirospaciari Jun 19, 2026
9d9c72e
node:sqlite: release superseded UDF callback roots on re-registration…
cirospaciari Jun 19, 2026
fc4aad1
node:sqlite: release superseded callback roots at the registration si…
cirospaciari Jun 19, 2026
5fafbd8
node:sqlite: match SQLite's case-insensitive function identity when r…
cirospaciari Jun 19, 2026
88c72b4
build: update the staticSqlite comment for the always-bundled sqlite …
cirospaciari Jun 19, 2026
7960275
node:sqlite: sweep orphaned sessions from statement execution too [sk…
cirospaciari Jun 19, 2026
ed378a6
node:sqlite: sweep orphaned sessions from BusyScope itself [skip size…
cirospaciari Jun 19, 2026
4d19528
node:sqlite: stale iterator return() leaves the statement alone; reje…
cirospaciari Jun 19, 2026
13bdb62
Merge origin/main [skip size check]
cirospaciari Jun 22, 2026
dde35fb
node:test: return the mock tracker from TestContext.mock [skip size c…
cirospaciari Jun 22, 2026
90c2be6
node:sqlite: a failed step exhausts the iterator [skip size check]
cirospaciari Jun 22, 2026
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
29 changes: 24 additions & 5 deletions scripts/build/deps/sqlite.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
/**
* SQLite — embedded SQL database. Backs bun:sqlite.
* SQLite — embedded SQL database. Backs bun:sqlite and node:sqlite.
*
* Source lives IN THE BUN REPO at src/jsc/bindings/sqlite/ — it's the
* sqlite3 amalgamation (single .c file). No fetch step; tracked in git.
*
* Only built when staticSqlite=true. Otherwise bun dlopen()s the system
* sqlite at runtime (macOS ships a recent sqlite; most linux distros don't,
* so static is the default on linux).
* Always built: node:sqlite uses the bundled copy unconditionally (matching
* Node.js). bun:sqlite additionally supports dlopen()ing the system sqlite
* on macOS when staticSqlite=false (LAZY_LOAD_SQLITE=1), but NodeSqlite.cpp
* includes sqlite3_local.h directly and links against these symbols on
* every platform.
*
* Bundling on macOS (previously dlopen-only there) grows the darwin binaries
* by ~1.8 MB. That is the cost of node:sqlite parity: Apple's system
* libsqlite3 ships without the session extension or percentile() and with
* extension loading disabled, so the bundled build is required — Node.js
* bundles SQLite for the same reason. Linux/Windows already linked the
* bundled copy.
*/

import type { Dependency } from "../source.ts";

export const sqlite: Dependency = {
name: "sqlite",

enabled: cfg => cfg.staticSqlite,
enabled: () => true,
Comment thread
claude[bot] marked this conversation as resolved.

source: () => ({
kind: "in-tree",
Expand All @@ -36,6 +45,16 @@ export const sqlite: Dependency = {
SQLITE_ENABLE_MATH_FUNCTIONS: 1,
SQLITE_ENABLE_UPDATE_DELETE_LIMIT: 1,
SQLITE_UDL_CAPABLE_PARSER: 1,
// node:sqlite exposes createSession/applyChangeset + columns()
// metadata. Match Node.js's compile-time feature set so those
// APIs work identically. PREUPDATE_HOOK is a prerequisite for the
// session extension.
SQLITE_ENABLE_SESSION: 1,
SQLITE_ENABLE_PREUPDATE_HOOK: 1,
SQLITE_ENABLE_DBSTAT_VTAB: 1,
SQLITE_ENABLE_GEOPOLY: 1,
SQLITE_ENABLE_RBU: 1,
SQLITE_ENABLE_PERCENTILE: 1,
},
cflags: [
"-Wno-incompatible-pointer-types-discards-qualifiers",
Expand Down
1 change: 1 addition & 0 deletions scripts/build/unified.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const noUnify: readonly string[] = [
"src/jsc/bindings/webcore/JSDOMPromiseDeferred.cpp",
"src/jsc/bindings/webcore/JSMessageEventCustom.cpp",
"src/jsc/bindings/sqlite/JSSQLStatement.cpp",
"src/jsc/bindings/sqlite/NodeSqlite.cpp",

// WebKit-derived crypto algorithm impls share file-static helper names
// (`aesAlgorithm`, `cryptEncrypt`, `ALG128`, `IVSIZE`, ...) — upstream
Expand Down
72 changes: 68 additions & 4 deletions src/js/node/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,72 @@ function run() {
throwNotImplemented("run()", 5090, "Use `bun:test` in the interim.");
}

function mock() {
throwNotImplemented("mock()", 5090, "Use `bun:test` in the interim.");
// Minimal `mock` tracker — enough for Node's own test suite (notably
// test/parallel/test-sqlite-*.{js,mjs}) which only uses `mock.fn()`,
// `spy.mock.calls[i].arguments`, and `spy.mock.callCount()`. The full
// MockTracker API (timers, getters, method mocking, restore/reset) is
// still #5090.
class MockFunctionContext {
calls: { arguments: unknown[]; result?: unknown; error?: unknown; this: unknown }[];
#impl: Function;
constructor(impl: Function) {
this.calls = [];
this.#impl = impl;
}
callCount() {
return this.calls.length;
}
mockImplementation(impl: Function) {
this.#impl = impl;
}
resetCalls() {
this.calls.length = 0;
}
_invoke(thisArg: unknown, args: unknown[]) {
const record: any = { arguments: args, this: thisArg };
try {
record.result = this.#impl.$apply(thisArg, args);
this.calls.push(record);
return record.result;
} catch (e) {
record.error = e;
this.calls.push(record);
throw e;
}
}
}

class MockTracker {
fn(original: Function = kDefaultFunction, impl: Function = original) {
const ctx = new MockFunctionContext(impl);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
function spy(this: unknown, ...args: unknown[]) {
return ctx._invoke(this, args);
}
Object.defineProperty(spy, "mock", { value: ctx, enumerable: true });
// Preserve .length so code that inspects the spy's declared arity
// (sqlite's aggregate() infers varargs from it) sees the original.
Object.defineProperty(spy, "length", { value: original.length, configurable: true });
return spy;
}
method() {
throwNotImplemented("mock.method()", 5090, "Use `bun:test` in the interim.");
}
getter() {
throwNotImplemented("mock.getter()", 5090, "Use `bun:test` in the interim.");
}
setter() {
throwNotImplemented("mock.setter()", 5090, "Use `bun:test` in the interim.");
}
reset() {}
restoreAll() {}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
get timers() {
throwNotImplemented("mock.timers", 5090, "Use `bun:test` in the interim.");
return undefined;
}
}

const mock = new MockTracker();

function fileSnapshot(_value: unknown, _path: string, _options: { serializers?: Function[] } = kEmptyObject) {
throwNotImplemented("fileSnapshot()", 5090, "Use `bun:test` in the interim.");
}
Expand Down Expand Up @@ -98,8 +160,10 @@ class TestContext {
}

get mock() {
throwNotImplemented("mock", 5090, "Use `bun:test` in the interim.");
return undefined;
// Node gives each TestContext its own tracker so `t.after` can
// restore; the shim is stateless enough that sharing one is fine
// for now (nothing to restore when only fn() is implemented).
return mock;
}

runOnly(_value?: boolean) {
Expand Down
6 changes: 5 additions & 1 deletion src/jsc/ErrorCode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -703,9 +703,11 @@ impl ErrorCode {
pub const TLS_ALPN_CALLBACK_INVALID_RESULT: ErrorCode = ErrorCode(322);
/// `ERR_PROXY_TUNNEL` (instanceof Error)
pub const PROXY_TUNNEL: ErrorCode = ErrorCode(323);
/// `ERR_SQLITE_ERROR` (instanceof Error)
pub const SQLITE_ERROR: ErrorCode = ErrorCode(324);

/// == C++ `NODE_ERROR_COUNT`.
pub const COUNT: u16 = 324;
pub const COUNT: u16 = 325;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

// ──────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -1071,6 +1073,7 @@ impl ErrorCode {
pub const ERR_SECRETS_INTERACTION_REQUIRED: ErrorCode = ErrorCode::SECRETS_INTERACTION_REQUIRED;
pub const ERR_HTTP2_GOAWAY_SESSION: ErrorCode = ErrorCode::HTTP2_GOAWAY_SESSION;
pub const ERR_PROXY_TUNNEL: ErrorCode = ErrorCode::PROXY_TUNNEL;
pub const ERR_SQLITE_ERROR: ErrorCode = ErrorCode::SQLITE_ERROR;

// NOTE: `ERR_SYSTEM_ERROR` / `ERR_CHILD_CLOSED_BEFORE_REPLY` intentionally
// do NOT live here. They belong to the unrelated enum
Expand Down Expand Up @@ -1412,6 +1415,7 @@ static CODE_STR: [&str; ErrorCode::COUNT as usize] = [
"ERR_HTTP2_GOAWAY_SESSION",
"ERR_TLS_ALPN_CALLBACK_INVALID_RESULT",
"ERR_PROXY_TUNNEL",
"ERR_SQLITE_ERROR",
];

// ──────────────────────────────────────────────────────────────────────────
Expand Down
2 changes: 2 additions & 0 deletions src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ extern "C" bool Bun__GlobalObject__connectedIPC(JSGlobalObject*);
extern "C" bool Bun__GlobalObject__hasIPC(JSGlobalObject*);
extern "C" bool Bun__ensureProcessIPCInitialized(JSGlobalObject*);
extern "C" const char* Bun__githubURL;
extern "C" const char* Bun__sqlite3_version();
BUN_DECLARE_HOST_FUNCTION(Bun__Process__send);

extern "C" void Process__emitDisconnectEvent(Zig::GlobalObject* global);
Expand Down Expand Up @@ -254,6 +255,7 @@ static JSValue constructVersions(VM& vm, JSObject* processObject)

object->putDirect(vm, JSC::Identifier::fromString(vm, "icu"_s), JSValue(JSC::jsOwnedString(vm, String(ASCIILiteral::fromLiteralUnsafe(U_ICU_VERSION)))), 0);
object->putDirect(vm, JSC::Identifier::fromString(vm, "unicode"_s), JSValue(JSC::jsOwnedString(vm, String(ASCIILiteral::fromLiteralUnsafe(U_UNICODE_VERSION)))), 0);
object->putDirect(vm, JSC::Identifier::fromString(vm, "sqlite"_s), JSValue(JSC::jsOwnedString(vm, String(ASCIILiteral::fromLiteralUnsafe(Bun__sqlite3_version())))), 0);

#define STRINGIFY_IMPL(x) #x
#define STRINGIFY(x) STRINGIFY_IMPL(x)
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,5 +335,6 @@ const errors: ErrorCodeMapping = [
["ERR_HTTP2_GOAWAY_SESSION", Error],
["ERR_TLS_ALPN_CALLBACK_INVALID_RESULT", TypeError],
["ERR_PROXY_TUNNEL", Error],
["ERR_SQLITE_ERROR", Error],
];
export default errors;
64 changes: 64 additions & 0 deletions src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
#include "JavaScriptCore/JSModuleNamespaceObjectInlines.h"
#include "JavaScriptCore/JSModuleRecord.h"
#include "JavaScriptCore/JSNativeStdFunction.h"
#include "JavaScriptCore/JSIteratorPrototype.h"
#include "JavaScriptCore/JSObject.h"
#include "JavaScriptCore/JSObjectInlines.h"
#include "JavaScriptCore/JSPromise.h"
Expand Down Expand Up @@ -131,6 +132,7 @@
#include "JSReactElement.h"
#include "BunMarkdownMeta.h"
#include "JSSQLStatement.h"
#include "sqlite/NodeSqlite.h"
#include "JSStringDecoder.h"
#include "JSTextEncoder.h"
#include "JSTextEncoderStream.h"
Expand Down Expand Up @@ -2626,6 +2628,68 @@ void GlobalObject::finishCreation(VM& vm)
init.setConstructor(constructor);
});

m_JSDatabaseSyncClassStructure.initLater(
[](LazyClassStructure::Initializer& init) {
auto* prototype = Bun::JSDatabaseSyncPrototype::create(
init.vm, init.global, Bun::JSDatabaseSyncPrototype::createStructure(init.vm, init.global, init.global->objectPrototype()));
auto* structure = Bun::JSDatabaseSync::createStructure(init.vm, init.global, prototype);
auto* constructor = Bun::JSDatabaseSyncConstructor::create(
init.vm, init.global, Bun::JSDatabaseSyncConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()), prototype);
init.setPrototype(prototype);
init.setStructure(structure);
init.setConstructor(constructor);
});

m_JSStatementSyncClassStructure.initLater(
[](LazyClassStructure::Initializer& init) {
auto* prototype = Bun::JSStatementSyncPrototype::create(
init.vm, init.global, Bun::JSStatementSyncPrototype::createStructure(init.vm, init.global, init.global->objectPrototype()));
auto* structure = Bun::JSStatementSync::createStructure(init.vm, init.global, prototype);
auto* constructor = Bun::JSStatementSyncConstructor::create(
init.vm, init.global, Bun::JSStatementSyncConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()), prototype);
init.setPrototype(prototype);
init.setStructure(structure);
init.setConstructor(constructor);
});

m_JSStatementSyncIteratorClassStructure.initLater(
[](LazyClassStructure::Initializer& init) {
// Prototype chain: instance → iterator prototype → %IteratorPrototype%
// so for-of / spread / Iterator helpers all work out of the box.
auto* prototype = Bun::JSStatementSyncIteratorPrototype::create(
init.vm, init.global, Bun::JSStatementSyncIteratorPrototype::createStructure(init.vm, init.global, init.global->iteratorPrototype()));
auto* structure = Bun::JSStatementSyncIterator::createStructure(init.vm, init.global, prototype);
init.setPrototype(prototype);
init.setStructure(structure);
});

m_JSNodeSqliteSessionClassStructure.initLater(
[](LazyClassStructure::Initializer& init) {
auto* prototype = Bun::JSNodeSqliteSessionPrototype::create(
init.vm, init.global, Bun::JSNodeSqliteSessionPrototype::createStructure(init.vm, init.global, init.global->objectPrototype()));
auto* structure = Bun::JSNodeSqliteSession::createStructure(init.vm, init.global, prototype);
init.setPrototype(prototype);
init.setStructure(structure);
});

m_JSNodeSqliteLimitsClassStructure.initLater(
[](LazyClassStructure::Initializer& init) {
// Null prototype: Node's DatabaseSyncLimits is an ObjectTemplate
// with only the named-property handler, so Object.prototype is
// NOT on its chain and can't shadow a limit name.
auto* structure = Bun::JSNodeSqliteLimits::createStructure(init.vm, init.global, JSC::jsNull());
init.setStructure(structure);
});

m_JSNodeSqliteTagStoreClassStructure.initLater(
[](LazyClassStructure::Initializer& init) {
auto* prototype = Bun::JSNodeSqliteTagStorePrototype::create(
init.vm, init.global, Bun::JSNodeSqliteTagStorePrototype::createStructure(init.vm, init.global, init.global->objectPrototype()));
auto* structure = Bun::JSNodeSqliteTagStore::createStructure(init.vm, init.global, prototype);
init.setPrototype(prototype);
init.setStructure(structure);
});

m_JSFFIFunctionStructure.initLater(
[](LazyClassStructure::Initializer& init) {
init.setStructure(Zig::JSFFIFunction::createStructure(init.vm, init.global, init.global->functionPrototype()));
Expand Down
6 changes: 6 additions & 0 deletions src/jsc/bindings/ZigGlobalObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,12 @@ class GlobalObject : public Bun::GlobalScope {
V(private, LazyClassStructure, m_JSH3ResponseSinkClassStructure) \
\
V(private, LazyClassStructure, m_JSStringDecoderClassStructure) \
V(public, LazyClassStructure, m_JSDatabaseSyncClassStructure) \
V(public, LazyClassStructure, m_JSStatementSyncClassStructure) \
V(public, LazyClassStructure, m_JSStatementSyncIteratorClassStructure) \
V(public, LazyClassStructure, m_JSNodeSqliteSessionClassStructure) \
V(public, LazyClassStructure, m_JSNodeSqliteLimitsClassStructure) \
V(public, LazyClassStructure, m_JSNodeSqliteTagStoreClassStructure) \
V(private, LazyClassStructure, m_NapiClassStructure) \
V(private, LazyClassStructure, m_callSiteStructure) \
V(public, LazyClassStructure, m_JSBufferClassStructure) \
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/isBuiltinModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ static constexpr ASCIILiteral builtinModuleNamesSortedLength[] = {
"_tls_common"_s,
"async_hooks"_s,
"fs/promises"_s,
"node:sqlite"_s,
"querystring"_s,
"_http_client"_s,
"_http_common"_s,
Expand Down
12 changes: 10 additions & 2 deletions src/jsc/bindings/sqlite/JSSQLStatement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,16 @@ extern "C" void Bun__closeAllSQLiteDatabasesForTermination()
auto& dbs = _instance->databases;

for (auto& db : dbs) {
if (db->db)
sqlite3_close(db->db);
if (db->db) {
// close_v2: with unfinalized statements still alive, plain
// sqlite3_close() returns SQLITE_BUSY and leaves the connection
// open, which would leak it once the pointer is nulled below.
sqlite3_close_v2(db->db);
// Prevent VersionSqlite3::release() (invoked later by the GC
// finalizer during VM teardown) from closing the same handle
// again, which would be a use-after-free.
db->db = nullptr;
}
}
}
Comment on lines 279 to 293

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.

🟡 Nit: this exit handler now closes bun:sqlite connections cleanly, but the node:sqlite JSDatabaseSync instances added by this PR aren't in _instance->databases (they live only as GC cells), so on default process exit they're never sqlite3_close_v2()'d — leaving WAL -wal/-shm sidecars behind for unclosed file-backed databases where Node.js (via ~DatabaseSync() on environment teardown) and bun:sqlite (via this function) both checkpoint and clean up. Not data loss or memory-unsafe (SQLite recovers the WAL on next open); just an observable Node-compat / internal-consistency gap that this hunk had the opportunity to close. Fine as a follow-up — would need a per-VM list of open JSDatabaseSync* walked from dispatch_on_exit, with the same Worker gating this call already has.

Extended reasoning...

What

Bun__closeAllSQLiteDatabasesForTermination() (called from ExitHandler::dispatch_on_exit at VirtualMachine.rs:508 on every normal main-thread exit, gated only on vm.worker.is_none()) iterates only _instance->databases — the bun:sqlite global registry. The new node:sqlite JSDatabaseSync instances introduced by this PR are not in that registry: a grep of NodeSqlite.cpp for registerDatabase / _instance / closeAllSQLiteDatabasesForTermination returns nothing, and NodeSqlite.h declares no global/per-VM list of open databases. JSDatabaseSync instances live only as GC cells; closeInternal() runs from ~JSDatabaseSync(), which only fires on a GC sweep.

By default Bun does not destroy the VM on exit — should_destruct_main_thread_on_exit() (VirtualMachine.rs:1375) reads BUN_DESTRUCT_VM_ON_EXIT and defaults to false. So on a normal exit, ~JSDatabaseSync() never runs and sqlite3_close_v2() is never called for node:sqlite connections.

Why it's an inconsistency this hunk surfaces

This PR (a) introduces node:sqlite and (b) touches exactly this function to fix bun:sqlite's termination-time double-close UAF (the new db->db = nullptr and the close_v2 switch). So the diff itself reaffirms the explicit choice that bun:sqlite connections get an exit-time close — but does not extend the same treatment to the node:sqlite connections it adds. The PR's own UAF regression test (node-sqlite.test.ts:1086-1112) exercises bun:sqlite with BUN_DESTRUCT_VM_ON_EXIT=1, not node:sqlite's default-exit behavior; no test opens a file-backed node:sqlite database, skips close(), and checks for sidecars after the process exits.

Step-by-step proof

const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync('/tmp/test.db');
db.exec('PRAGMA journal_mode = WAL');
db.exec('CREATE TABLE IF NOT EXISTS t(x); INSERT INTO t VALUES (1)');
// no db.close(); script ends naturally
  1. Process reaches end of script; dispatch_on_exit() runs.
  2. Bun__closeAllSQLiteDatabasesForTermination() is called (line 508), iterates _instance->databases — empty (no bun:sqlite databases were opened). Returns.
  3. global_exit() checks should_destruct_main_thread_on_exit()false (default). VM is not destroyed; ~JSDatabaseSync() never runs; sqlite3_close_v2() is never called.
  4. Process exits. The OS closes the file descriptors, but no WAL checkpoint ran.
  5. On disk: /tmp/test.db, /tmp/test.db-wal, /tmp/test.db-shm all persist.

Under Node.js (natural exit, not process.exit()): DatabaseSync is a BaseObject; ~DatabaseSync() runs during Environment teardown and calls sqlite3_close_v2(), which checkpoints the WAL and removes the sidecars → only /tmp/test.db remains. (One nuance: Node's process.exit() also fast-exits without running BaseObject destructors, so the divergence is specifically for natural exit / falling off the end of the script — which is the common "forgot to close()" case.)

Under Bun with bun:sqlite: the equivalent script using new Database('/tmp/test.db') registers in _instance->databases, so this very function closes it at step 2 → no sidecars.

Impact

Low. Not memory-unsafe (process is exiting; OS reclaims fds). Not data loss — SQLite recovers the WAL on the next open of the database. The observable effect is leftover -wal/-shm files (and a hot -journal if exiting mid-transaction in non-WAL mode), which is a visible filesystem-state difference from both Node.js and bun:sqlite for users who forget close()/using on a file-backed database. The trigger requires (a) a file-backed (non-:memory:) database, (b) WAL mode or an open transaction, and (c) no explicit close().

Suggested fix

Maintain a per-VM (or per-GlobalObject) list of open JSDatabaseSync* (e.g. on the Zig::GlobalObject alongside the lazy class structures, or a static like _instance keyed by VM) and walk it from this function, mirroring what it already does for bun:sqlite. The same vm.worker.is_none() gating that already wraps this call applies. Alternatively, document that node:sqlite users should prefer using db = new DatabaseSync(...) for file-backed databases. Non-blocking; fine as a follow-up issue.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed it's a real parity gap (Node closes DatabaseSync handles on environment teardown, so a clean exit checkpoints WAL and removes the -wal/-shm sidecars; Bun's default exit path doesn't destruct the VM, so unclosed node:sqlite handles never get sqlite3_close_v2). Deferring it from this PR as you suggest — it needs a per-VM registry of open JSDatabaseSync handles walked from the exit handler with the same worker gating, which deserves its own change and tests; noting it in the PR description as a known follow-up. No data loss in the meantime (SQLite recovers the WAL on next open).


Expand Down
Loading
Loading