From 5b22f747cccb2ed76c46ed0a10c04b36b2145af5 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 18 May 2026 16:46:45 -0700 Subject: [PATCH 1/2] Add node:sqlite StatementSync compatibility methods --- index.d.ts | 14 ++++++++--- index.js | 10 ++++++-- src/statement.cpp | 53 ++++++++++++++++++++++++++++++++++-------- src/statement.h | 8 +++++++ src/util.h | 51 +++++++++++++++++++++++++++++----------- test/compat.test.ts | 48 ++++++++++++++++++++++++++++++++++++++ test/statement.test.ts | 5 ++++ 7 files changed, 160 insertions(+), 29 deletions(-) diff --git a/index.d.ts b/index.d.ts index 6d8aa2d..767f276 100644 --- a/index.d.ts +++ b/index.d.ts @@ -85,13 +85,21 @@ export class StatementSync { /** Execute the statement and return change metadata. */ run(...params: unknown[]): RunResult /** Return the first result row, or undefined. */ - get(...params: unknown[]): Record | undefined + get(...params: unknown[]): Record | unknown[] | undefined /** Return all result rows. */ - all(...params: unknown[]): Record[] + all(...params: unknown[]): Array | unknown[]> /** Return an iterator over result rows. */ - iterate(...params: unknown[]): IterableIterator> + iterate(...params: unknown[]): IterableIterator | unknown[]> /** Return column metadata for the statement. */ columns(): ColumnInfo[] + /** Return result rows as arrays instead of objects. */ + setReturnArrays(enabled: boolean): void + /** Return integer columns as BigInt values. */ + setReadBigInts(enabled: boolean): void + /** Allow bare object keys for named parameters. */ + setAllowBareNamedParameters(enabled: boolean): void + /** Ignore unknown named parameter keys. */ + setAllowUnknownNamedParameters(enabled: boolean): void /** The SQL source text of the statement. */ readonly sourceSQL: string /** The SQL text with bound parameters expanded. */ diff --git a/index.js b/index.js index 1d36060..f6ee953 100644 --- a/index.js +++ b/index.js @@ -10,15 +10,21 @@ function loadAddon() { const arch = process.arch const prebuilt = path.join(__dirname, "prebuilds", `${platform}-${arch}`, "doltlite.node") const compiled = path.join(__dirname, "build", "Release", "doltlite.node") + const compiledObjTarget = path.join(__dirname, "build", "Release", "obj.target", "doltlite.node") - for (const candidate of [prebuilt, compiled]) { + const errors = [] + for (const candidate of [prebuilt, compiled, compiledObjTarget]) { + if (!fs.existsSync(candidate)) continue try { return require(candidate) - } catch {} + } catch (err) { + errors.push(`${candidate}: ${err && err.message ? err.message : err}`) + } } throw new Error( `@dolthub/doltlite: no native binary found for ${platform}-${arch}.\n` + + (errors.length ? `Tried:\n${errors.join("\n")}\n` : "") + `Run \`npm install\` to build from source, or file an issue at ` + `https://github.com/dolthub/doltlite-node/issues` ) diff --git a/src/statement.cpp b/src/statement.cpp index bc906de..8474344 100644 --- a/src/statement.cpp +++ b/src/statement.cpp @@ -39,7 +39,8 @@ Statement::~Statement() { Napi::Value Statement::Run(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (!stmt_) { Napi::Error::New(env, "Statement is finalised").ThrowAsJavaScriptException(); return env.Undefined(); } - if (!BindArgs(env, stmt_, info)) return env.Undefined(); + if (!BindArgs(env, stmt_, info, 0, allowBareNamedParameters_, + allowUnknownNamedParameters_)) return env.Undefined(); int rc = sqlite3_step(stmt_); sqlite3_reset(stmt_); @@ -60,11 +61,15 @@ Napi::Value Statement::Run(const Napi::CallbackInfo& info) { Napi::Value Statement::Get(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (!stmt_) { Napi::Error::New(env, "Statement is finalised").ThrowAsJavaScriptException(); return env.Undefined(); } - if (!BindArgs(env, stmt_, info)) return env.Undefined(); + if (!BindArgs(env, stmt_, info, 0, allowBareNamedParameters_, + allowUnknownNamedParameters_)) return env.Undefined(); int rc = sqlite3_step(stmt_); Napi::Value result = env.Undefined(); - if (rc == SQLITE_ROW) result = RowToObject(env, stmt_); + if (rc == SQLITE_ROW) { + result = returnArrays_ ? (Napi::Value)RowToArray(env, stmt_, readBigInts_) + : (Napi::Value)RowToObject(env, stmt_, readBigInts_); + } else if (rc != SQLITE_DONE) ThrowSQLiteError(env, db_->Handle(), "get"); sqlite3_reset(stmt_); return result; @@ -75,18 +80,42 @@ Napi::Value Statement::Get(const Napi::CallbackInfo& info) { Napi::Value Statement::All(const Napi::CallbackInfo& info) { Napi::Env env = info.Env(); if (!stmt_) { Napi::Error::New(env, "Statement is finalised").ThrowAsJavaScriptException(); return env.Undefined(); } - if (!BindArgs(env, stmt_, info)) return env.Undefined(); + if (!BindArgs(env, stmt_, info, 0, allowBareNamedParameters_, + allowUnknownNamedParameters_)) return env.Undefined(); auto result = Napi::Array::New(env); uint32_t idx = 0; int rc; while ((rc = sqlite3_step(stmt_)) == SQLITE_ROW) - result.Set(idx++, RowToObject(env, stmt_)); + result.Set(idx++, returnArrays_ ? (Napi::Value)RowToArray(env, stmt_, readBigInts_) + : (Napi::Value)RowToObject(env, stmt_, readBigInts_)); sqlite3_reset(stmt_); if (rc != SQLITE_DONE) ThrowSQLiteError(env, db_->Handle(), "all"); return result; } +// ── node:sqlite compatibility flags ────────────────────────────────────────── + +Napi::Value Statement::SetReturnArrays(const Napi::CallbackInfo& info) { + returnArrays_ = info.Length() > 0 && info[0].ToBoolean().Value(); + return info.Env().Undefined(); +} + +Napi::Value Statement::SetReadBigInts(const Napi::CallbackInfo& info) { + readBigInts_ = info.Length() > 0 && info[0].ToBoolean().Value(); + return info.Env().Undefined(); +} + +Napi::Value Statement::SetAllowBareNamedParameters(const Napi::CallbackInfo& info) { + allowBareNamedParameters_ = info.Length() > 0 && info[0].ToBoolean().Value(); + return info.Env().Undefined(); +} + +Napi::Value Statement::SetAllowUnknownNamedParameters(const Napi::CallbackInfo& info) { + allowUnknownNamedParameters_ = info.Length() > 0 && info[0].ToBoolean().Value(); + return info.Env().Undefined(); +} + // ── iterate() → IterableIterator ───────────────────────────────────────────── // Returns a plain JS iterator object: { next() { return {value, done} } } // A full Symbol.iterator implementation requires a persistent Napi::Reference @@ -174,11 +203,15 @@ Napi::Value Statement::ExpandedSQLGetter(const Napi::CallbackInfo& info) { Napi::Object Statement::Init(Napi::Env env, Napi::Object exports) { Napi::Function ctor = DefineClass(env, "StatementSync", { - InstanceMethod("run", &Statement::Run), - InstanceMethod("get", &Statement::Get), - InstanceMethod("all", &Statement::All), - InstanceMethod("iterate", &Statement::Iterate), - InstanceMethod("columns", &Statement::Columns), + InstanceMethod("run", &Statement::Run), + InstanceMethod("get", &Statement::Get), + InstanceMethod("all", &Statement::All), + InstanceMethod("iterate", &Statement::Iterate), + InstanceMethod("columns", &Statement::Columns), + InstanceMethod("setReturnArrays", &Statement::SetReturnArrays), + InstanceMethod("setReadBigInts", &Statement::SetReadBigInts), + InstanceMethod("setAllowBareNamedParameters", &Statement::SetAllowBareNamedParameters), + InstanceMethod("setAllowUnknownNamedParameters", &Statement::SetAllowUnknownNamedParameters), InstanceAccessor("sourceSQL", &Statement::SourceSQLGetter, nullptr), InstanceAccessor("expandedSQL", &Statement::ExpandedSQLGetter, nullptr), }); diff --git a/src/statement.h b/src/statement.h index cb73e16..cc6588d 100644 --- a/src/statement.h +++ b/src/statement.h @@ -16,12 +16,20 @@ class Statement : public Napi::ObjectWrap { sqlite3_stmt* stmt_ = nullptr; Database* db_ = nullptr; std::string source_; + bool returnArrays_ = false; + bool readBigInts_ = false; + bool allowBareNamedParameters_ = true; + bool allowUnknownNamedParameters_ = false; Napi::Value Run(const Napi::CallbackInfo& info); Napi::Value Get(const Napi::CallbackInfo& info); Napi::Value All(const Napi::CallbackInfo& info); Napi::Value Iterate(const Napi::CallbackInfo& info); Napi::Value Columns(const Napi::CallbackInfo& info); + Napi::Value SetReturnArrays(const Napi::CallbackInfo& info); + Napi::Value SetReadBigInts(const Napi::CallbackInfo& info); + Napi::Value SetAllowBareNamedParameters(const Napi::CallbackInfo& info); + Napi::Value SetAllowUnknownNamedParameters(const Napi::CallbackInfo& info); Napi::Value SourceSQLGetter(const Napi::CallbackInfo& info); Napi::Value ExpandedSQLGetter(const Napi::CallbackInfo& info); diff --git a/src/util.h b/src/util.h index 960f6f6..99ddd3b 100644 --- a/src/util.h +++ b/src/util.h @@ -4,10 +4,14 @@ #include // Converts a SQLite column value to a Napi::Value. -inline Napi::Value ColumnToNapi(Napi::Env env, sqlite3_stmt* stmt, int col) { +inline Napi::Value ColumnToNapi(Napi::Env env, sqlite3_stmt* stmt, int col, + bool readBigInts = false) { switch (sqlite3_column_type(stmt, col)) { - case SQLITE_INTEGER: - return Napi::Number::New(env, (double)sqlite3_column_int64(stmt, col)); + case SQLITE_INTEGER: { + sqlite3_int64 i = sqlite3_column_int64(stmt, col); + if (readBigInts) return Napi::BigInt::New(env, (int64_t)i); + return Napi::Number::New(env, (double)i); + } case SQLITE_FLOAT: return Napi::Number::New(env, sqlite3_column_double(stmt, col)); case SQLITE_TEXT: { @@ -28,16 +32,28 @@ inline Napi::Value ColumnToNapi(Napi::Env env, sqlite3_stmt* stmt, int col) { } // Builds a JS object from the current row of a prepared statement. -inline Napi::Object RowToObject(Napi::Env env, sqlite3_stmt* stmt) { +inline Napi::Object RowToObject(Napi::Env env, sqlite3_stmt* stmt, + bool readBigInts = false) { auto obj = Napi::Object::New(env); int n = sqlite3_column_count(stmt); for (int i = 0; i < n; i++) { const char* name = sqlite3_column_name(stmt, i); - obj.Set(name, ColumnToNapi(env, stmt, i)); + obj.Set(name, ColumnToNapi(env, stmt, i, readBigInts)); } return obj; } +// Builds a JS array from the current row of a prepared statement. +inline Napi::Array RowToArray(Napi::Env env, sqlite3_stmt* stmt, + bool readBigInts = false) { + int n = sqlite3_column_count(stmt); + auto arr = Napi::Array::New(env, n); + for (int i = 0; i < n; i++) { + arr.Set((uint32_t)i, ColumnToNapi(env, stmt, i, readBigInts)); + } + return arr; +} + // Binds a JS value to a prepared statement parameter (1-based index). inline bool BindNapi(Napi::Env env, sqlite3_stmt* stmt, int idx, Napi::Value val) { if (val.IsNull() || val.IsUndefined()) { @@ -74,9 +90,9 @@ inline bool BindNapi(Napi::Env env, sqlite3_stmt* stmt, int idx, Napi::Value val // named: bind({$name: val, ...}) or bare names {name: val} if allowBare=true inline bool BindArgs(Napi::Env env, sqlite3_stmt* stmt, const Napi::CallbackInfo& info, size_t startIdx = 0, - bool allowBare = true) { + bool allowBare = true, bool allowUnknown = false) { sqlite3_clear_bindings(stmt); - if (info.Length() <= (int)startIdx) return true; + if (info.Length() <= startIdx) return true; // Named parameters via a plain object if (info.Length() == startIdx + 1 && info[startIdx].IsObject() @@ -85,15 +101,22 @@ inline bool BindArgs(Napi::Env env, sqlite3_stmt* stmt, auto keys = obj.GetPropertyNames(); for (uint32_t i = 0; i < keys.Length(); i++) { std::string key = keys.Get(i).As().Utf8Value(); - // Try $name, :name, @name prefixes, then bare if allowBare int idx = 0; - for (const char* prefix : {"", "$", ":", "@"}) { - std::string k = std::string(prefix) + key; - idx = sqlite3_bind_parameter_index(stmt, k.c_str()); - if (idx > 0) break; + if (!key.empty() && (key[0] == '$' || key[0] == ':' || key[0] == '@')) { + idx = sqlite3_bind_parameter_index(stmt, key.c_str()); + } else if (allowBare) { + for (const char* prefix : {"$", ":", "@", ""}) { + std::string k = std::string(prefix) + key; + idx = sqlite3_bind_parameter_index(stmt, k.c_str()); + if (idx > 0) break; + } + } + if (idx == 0) { + if (allowUnknown) continue; + Napi::Error::New(env, "Unknown named parameter '" + key + "'") + .ThrowAsJavaScriptException(); + return false; } - if (idx == 0 && !allowBare) continue; - if (idx == 0) continue; if (!BindNapi(env, stmt, idx, obj.Get(keys.Get(i)))) return false; } return true; diff --git a/test/compat.test.ts b/test/compat.test.ts index 52fba31..dfe3236 100644 --- a/test/compat.test.ts +++ b/test/compat.test.ts @@ -44,6 +44,10 @@ describe("node:sqlite API surface", () => { expect(typeof stmt.all).toBe("function") expect(typeof stmt.iterate).toBe("function") expect(typeof stmt.columns).toBe("function") + expect(typeof stmt.setReturnArrays).toBe("function") + expect(typeof stmt.setReadBigInts).toBe("function") + expect(typeof stmt.setAllowBareNamedParameters).toBe("function") + expect(typeof stmt.setAllowUnknownNamedParameters).toBe("function") }) test("StatementSync exposes sourceSQL and expandedSQL", () => { @@ -133,6 +137,50 @@ describe("node:sqlite behaviour contract", () => { expect(row.x).toBe(9) }) + test("unknown named parameters throw by default", () => { + const stmt = db.prepare("SELECT $x AS x") + expect(() => stmt.get({ $x: 1, y: 2 })).toThrow("Unknown named parameter") + }) + + test("setAllowUnknownNamedParameters ignores extra keys", () => { + const stmt = db.prepare("SELECT $x AS x") + expect(stmt.setAllowUnknownNamedParameters(true)).toBeUndefined() + const row = stmt.get({ $x: 1, y: 2 }) as any + expect(row.x).toBe(1) + }) + + test("setAllowBareNamedParameters controls bare named keys", () => { + const stmt = db.prepare("SELECT $x AS x") + expect(stmt.setAllowBareNamedParameters(false)).toBeUndefined() + expect(() => stmt.get({ x: 1 })).toThrow("Unknown named parameter") + stmt.setAllowBareNamedParameters(true) + const row = stmt.get({ x: 2 }) as any + expect(row.x).toBe(2) + }) + + test("setReturnArrays makes get(), all(), and iterate() return arrays", () => { + db.exec("CREATE TABLE t (id INT, name TEXT)") + db.exec("INSERT INTO t VALUES (1, 'a'), (2, 'b')") + const stmt = db.prepare("SELECT id, name FROM t ORDER BY id") + expect(stmt.setReturnArrays(true)).toBeUndefined() + expect(stmt.get()).toEqual([1, "a"]) + expect(stmt.all()).toEqual([[1, "a"], [2, "b"]]) + expect(Array.from(stmt.iterate())).toEqual([[1, "a"], [2, "b"]]) + stmt.setReturnArrays(false) + expect(stmt.get()).toEqual({ id: 1, name: "a" }) + }) + + test("setReadBigInts makes integer columns return BigInt values", () => { + const stmt = db.prepare("SELECT 42 AS n") + expect(stmt.setReadBigInts(true)).toBeUndefined() + const row = stmt.get() as any + expect(row.n).toBe(42n) + stmt.setReturnArrays(true) + expect(stmt.get()).toEqual([42n]) + stmt.setReadBigInts(false) + expect(stmt.get()).toEqual([42]) + }) + test("transaction isolation: rolled-back inserts are not visible", () => { db.exec("CREATE TABLE t (x INT)") db.exec("BEGIN") diff --git a/test/statement.test.ts b/test/statement.test.ts index e0383fd..64aca81 100644 --- a/test/statement.test.ts +++ b/test/statement.test.ts @@ -41,6 +41,11 @@ describe("DatabaseSync.prepare()", () => { expect(typeof stmt.get).toBe("function") expect(typeof stmt.all).toBe("function") expect(typeof stmt.iterate).toBe("function") + expect(typeof stmt.columns).toBe("function") + expect(typeof stmt.setReturnArrays).toBe("function") + expect(typeof stmt.setReadBigInts).toBe("function") + expect(typeof stmt.setAllowBareNamedParameters).toBe("function") + expect(typeof stmt.setAllowUnknownNamedParameters).toBe("function") }) test("throws on invalid SQL", () => { From ba566e89fbc3b71f8c1cbf18db7a1ad9e7a4e556 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 18 May 2026 17:10:59 -0700 Subject: [PATCH 2/2] Export Bun N-API entrypoint on Linux --- src/bun_compat.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bun_compat.cpp b/src/bun_compat.cpp index 6cb776e..71ab212 100644 --- a/src/bun_compat.cpp +++ b/src/bun_compat.cpp @@ -10,7 +10,7 @@ extern "C" napi_value _doltlite_init(napi_env env, napi_value exports); // this weak definition. MSVC doesn't support weak functions; skip it there // since Windows builds target Node 22+ which supplies the symbol via NAPI_MODULE. #if !defined(_MSC_VER) -extern "C" __attribute__((weak)) napi_value napi_register_module_v1(napi_env env, napi_value exports) { +extern "C" __attribute__((weak, visibility("default"))) napi_value napi_register_module_v1(napi_env env, napi_value exports) { return _doltlite_init(env, exports); } #endif