Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
980e26b
node:sqlite: implement the module and pass the Node v26.3.0 test suite
cirospaciari Jul 3, 2026
d8cbdba
node:sqlite: address review — bundled sqlite on macOS, Session/SQLTag…
alii Jul 7, 2026
f327a00
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 7, 2026
a734628
node:sqlite: dlopen the system SQLite on macOS instead of bundling
alii Jul 7, 2026
abb6f09
test: pass createTagStore capacity as a number so the LRU-churn test …
alii Jul 7, 2026
0e07202
node:sqlite: run the shared SQLite one-time config before either modu…
cirospaciari Jul 7, 2026
525f648
process.versions.sqlite: don't force-dlopen the system SQLite
alii Jul 8, 2026
6d37274
node:sqlite: align with Node v26 semantics
alii Jul 8, 2026
a805a90
test(sqlite): assert -wal is checkpointed on worker exit; use module-…
alii Jul 8, 2026
0990265
node:sqlite: fix UDF reentrancy crashes and Array-first-arg Node parity
robobun Jul 8, 2026
6cafa3d
node:sqlite: guard iterator next() against stepping its own statement
robobun Jul 8, 2026
efff7e7
node:sqlite: install isOpen/isTransaction/limits/sourceSQL/expandedSQ…
robobun Jul 8, 2026
32ccb44
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 8, 2026
2e42058
node:sqlite: defer sqlite3_close_v2 while a BusyScope is on the stack
robobun Jul 8, 2026
72ccccc
node:sqlite: skip iterator return()'s sqlite3_reset while stepping; d…
robobun Jul 8, 2026
e61e668
node:sqlite: report return-code-authoritative SQLite errors from the …
cirospaciari Jul 8, 2026
44ebc81
Merge remote-tracking branch 'origin/main' into claude/node-sqlite-v26
robobun Jul 8, 2026
90071bb
node:sqlite: refuse open() during a deferred close; read step errors …
robobun Jul 8, 2026
65c1d30
Merge remote-tracking branch 'origin/main' into claude/node-sqlite-v26
robobun Jul 9, 2026
f6b14a2
node:sqlite: capture the connection for serialize()/TagStore error pa…
robobun Jul 9, 2026
75505df
node:sqlite: address second-pass review on #32498
alii Jul 9, 2026
f282050
node:sqlite: skip sqlite3_finalize in ~JSStatementSync when a Steppin…
alii Jul 10, 2026
407a229
node:sqlite: fix limits HasProperty contract, subclassing, backup() t…
robobun Jul 10, 2026
137d34b
test(node:sqlite): use `using` for the backup-terminate lock holder
robobun Jul 10, 2026
1a706c5
Merge origin/main
cirospaciari Jul 14, 2026
52ea349
node:sqlite: build every ERR_SQLITE_ERROR through one helper
cirospaciari Jul 14, 2026
afdbc3a
Merge origin/main into claude/node-sqlite-v26
cirospaciari Jul 14, 2026
d3332be
Merge remote-tracking branch 'origin/main' into claude/node-sqlite-v26
cirospaciari Jul 15, 2026
7c1ab08
node:sqlite: correct the aggregate accumulator Strong<> safety comment
cirospaciari Jul 15, 2026
522c777
node:sqlite: guard session.changeset()/patchset() against re-entrant …
robobun Jul 16, 2026
d63b221
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 16, 2026
39b72f9
node:sqlite: correct the inUse invariant comment and assert it; docum…
robobun Jul 16, 2026
b7724ae
Merge remote-tracking branch 'origin/main' into claude/node-sqlite-v26
robobun Jul 16, 2026
871b117
node:sqlite: free pChangeset when the authorizer throws on sessionGen…
robobun Jul 16, 2026
7efc966
node:sqlite: reword the backup() threadpool divergence note
robobun Jul 16, 2026
d6cbff5
node:sqlite: correct the deleteTrackedSessions() inUse-invariant premise
robobun Jul 16, 2026
50dacfe
ci: retrigger
robobun Jul 16, 2026
0db244b
node:sqlite: guard nodeSqliteAuthorizerCallback against re-entry with…
robobun Jul 16, 2026
b188a5e
node:sqlite: adopt serialize()/changeset()/patchset() buffers zero-copy
robobun Jul 16, 2026
2546808
Merge branch 'main' into claude/node-sqlite-v26
robobun Jul 16, 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
2 changes: 1 addition & 1 deletion docs/runtime/nodejs-compat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ This page is updated regularly and reflects the latest version of Bun's compatib

### [`node:sqlite`](https://nodejs.org/api/sqlite.html)

🔴 Not implemented.
🟢 Fully implemented. `backup()` runs synchronously and blocks the event loop for the duration of the copy (Node runs it on a worker thread) — throw from the `progress` callback to abort a backup that keeps hitting `SQLITE_BUSY`. A `Buffer`/`Uint8Array` database path must be valid UTF-8 (Node passes the raw bytes through; Bun rejects non-UTF-8 with `ERR_INVALID_ARG_VALUE`). On macOS, Bun uses the system `libsqlite3.dylib`; `loadExtension()` (and, on older macOS releases, `createSession()`/`applyChangeset()`) require a full SQLite build — call `require("bun:sqlite").Database.setCustomSQLite(path)` before opening a database.

### [`node:test`](https://nodejs.org/api/test.html)

Expand Down
6 changes: 5 additions & 1 deletion scripts/build/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,11 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con
const canary = partial.canary ?? true;
const canaryRevision = canary ? "1" : "0";

// Static SQLite: off on Apple (uses system), on elsewhere
// Whether bun:sqlite and node:sqlite link the bundled sqlite3 directly
// (LAZY_LOAD_SQLITE=0) or dlopen the system library at runtime. macOS
// defaults to dlopen so both APIs share Apple's libsqlite3 (one library,
// one POSIX-lock inode map — howtocorrupt.html §2.2.1); Linux/Windows
// link the bundled amalgamation.
const staticSqlite = partial.staticSqlite ?? !darwin;

// Static libatomic: on by default. Arch/Manjaro don't ship libatomic.a —
Expand Down
22 changes: 18 additions & 4 deletions scripts/build/deps/sqlite.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
/**
* 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).
* Built when staticSqlite is true (the Linux/Windows default). On macOS
* both bun:sqlite and node:sqlite dlopen the system libsqlite3.dylib at
* runtime (LAZY_LOAD_SQLITE=1) so exactly one library is loaded per
* process — see the corruption caveat in config.ts. Apple's build omits
* load_extension/percentile()/geopoly/rbu (and, on older macOS releases,
* the session extension); node:sqlite runtime-gates the affected APIs and
* points at Database.setCustomSQLite() for a full build.
*/

import type { Dependency } from "../source.ts";
Expand Down Expand Up @@ -36,6 +40,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
6 changes: 4 additions & 2 deletions src/js/node/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,8 +450,10 @@ class TestContext {
}

get mock() {
throwNotImplemented("mock", 5090, "Use `bun:test` in the interim.");
return undefined;
// Node gives each TestContext its own tracker so after-test restoration
// is scoped; sharing the module-level tracker is enough for what's
// implemented today (Node's own sqlite tests use t.mock.fn()).
return mock;
}

runOnly(_value?: boolean) {
Expand Down
2 changes: 2 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ unsafe extern "C" {
safe fn Process__dispatchOnBeforeExit(global: &JSGlobalObject, code: u8);
safe fn Process__dispatchOnExit(global: &JSGlobalObject, code: u8);
safe fn Bun__closeAllSQLiteDatabasesForTermination();
safe fn Bun__closeAllNodeSqliteDatabasesForTermination(global: &JSGlobalObject);
safe fn Bun__WebView__closeAllForTermination();
safe fn Zig__GlobalObject__destructOnExit(global: &JSGlobalObject);
}
Expand Down Expand Up @@ -506,6 +507,7 @@ impl ExitHandler {
Process__dispatchOnExit(vm.global(), exit_code);
if vm.worker.is_none() {
Bun__closeAllSQLiteDatabasesForTermination();
Bun__closeAllNodeSqliteDatabasesForTermination(vm.global());
Bun__WebView__closeAllForTermination();
}
}
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 @@ -168,6 +168,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 @@ -255,6 +256,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 @@ -353,6 +353,7 @@ const errors: ErrorCodeMapping = [
["ERR_INVALID_BUFFER_SIZE", RangeError],
["ERR_TRACE_EVENTS_CATEGORY_REQUIRED", TypeError],
["ERR_TRACE_EVENTS_UNAVAILABLE", Error],
["ERR_SQLITE_ERROR", Error],
["ERR_CRYPTO_ARGON2_NOT_SUPPORTED", Error],
// llhttp reports a missing CRLF after a chunk's data as HPE_STRICT,
// distinct from a malformed chunk-size line (HPE_INVALID_CHUNK_SIZE).
Expand Down
72 changes: 72 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 @@ -134,6 +135,7 @@
#include "JSReactElement.h"
#include "BunMarkdownMeta.h"
#include "JSSQLStatement.h"
#include "sqlite/NodeSqlite.h"
#include "JSStringDecoder.h"
#include "JSTextEncoder.h"
#include "streams/JSTextEncoderStream.h"
Expand Down Expand Up @@ -2684,6 +2686,76 @@ 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);
auto* constructor = Bun::JSNodeSqliteSessionConstructor::create(
init.vm, init.global, Bun::JSNodeSqliteSessionConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()), prototype);
init.setPrototype(prototype);
init.setStructure(structure);
init.setConstructor(constructor);
});

m_JSNodeSqliteLimitsClassStructure.initLater(
[](LazyClassStructure::Initializer& init) {
// Node's DatabaseSyncLimits is a V8 ObjectTemplate: instances get a
// per-template prototype whose own [[Prototype]] is Object.prototype.
// Match the observable chain (limits → {} → Object.prototype).
auto* prototype = JSC::constructEmptyObject(init.global, init.global->objectPrototype());
auto* structure = Bun::JSNodeSqliteLimits::createStructure(init.vm, init.global, prototype);
init.setPrototype(prototype);
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);
auto* constructor = Bun::JSNodeSqliteTagStoreConstructor::create(
init.vm, init.global, Bun::JSNodeSqliteTagStoreConstructor::createStructure(init.vm, init.global, init.global->functionPrototype()), prototype);
init.setPrototype(prototype);
init.setStructure(structure);
init.setConstructor(constructor);
});

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 @@ -559,6 +559,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
37 changes: 31 additions & 6 deletions src/jsc/bindings/sqlite/JSSQLStatement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ class AutoDestructingSQLiteStatement {
}
};

static void initializeSQLite()
// One-time sqlite3_config() calls. Must run before the FIRST sqlite3_open_v2
// from EITHER bun:sqlite or node:sqlite (they share one library, and config
// is SQLITE_MISUSE after init). extern "C" for the cross-TU forward-declare.
extern "C" void Bun__initializeSQLite()
{
static std::once_flag onceFlag;
std::call_once(onceFlag, [] {
Expand Down Expand Up @@ -270,6 +273,19 @@ static VersionSqlite3* databaseForHandle(int32_t handle)
return dbs[static_cast<size_t>(handle)];
}

// Shared with node:sqlite's termination path (Bun__closeAllNodeSqliteDatabasesForTermination):
// with unfinalized statements close_v2 only zombifies the connection and
// defers the WAL checkpoint to a finalize that never comes, so flush the WAL
// into the main database file explicitly. Zero busy_timeout first — TRUNCATE
// waits on readers via the connection's busy-handler, so a large user-set
// timeout plus a cross-process reader would stall process.exit(); with a
// zero handler TRUNCATE degrades to a passive checkpoint immediately.
extern "C" void Bun__sqliteCheckpointForTermination(sqlite3* db)
{
sqlite3_busy_timeout(db, 0);
sqlite3_wal_checkpoint_v2(db, nullptr, SQLITE_CHECKPOINT_TRUNCATE, nullptr, nullptr);
}

extern "C" void Bun__closeAllSQLiteDatabasesForTermination()
{
if (!_instance) {
Expand All @@ -279,8 +295,17 @@ extern "C" void Bun__closeAllSQLiteDatabasesForTermination()
auto& dbs = _instance->databases;

for (auto& db : dbs) {
if (db->db)
sqlite3_close(db->db);
if (db->db) {
Bun__sqliteCheckpointForTermination(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 thread
claude[bot] marked this conversation as resolved.

Expand Down Expand Up @@ -1194,7 +1219,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementSetCustomSQLite, (JSC::JSGlobalObject * l
}
#endif

initializeSQLite();
Bun__initializeSQLite();

RELEASE_AND_RETURN(scope, JSValue::encode(JSC::jsBoolean(true)));
}
Expand Down Expand Up @@ -1247,7 +1272,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementDeserialize, (JSC::JSGlobalObject * lexic
return {};
}
#endif
initializeSQLite();
Bun__initializeSQLite();

size_t byteLength = array->byteLength();
void* ptr = array->vector();
Expand Down Expand Up @@ -1730,7 +1755,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementOpenStatementFunction, (JSC::JSGlobalObje
return {};
}
#endif
initializeSQLite();
Bun__initializeSQLite();

auto topExceptionScope = DECLARE_TOP_EXCEPTION_SCOPE(vm);
String path = pathValue.toWTFString(lexicalGlobalObject);
Expand Down
Loading
Loading