Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 11 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> | undefined
get(...params: unknown[]): Record<string, unknown> | unknown[] | undefined
/** Return all result rows. */
all(...params: unknown[]): Record<string, unknown>[]
all(...params: unknown[]): Array<Record<string, unknown> | unknown[]>
/** Return an iterator over result rows. */
iterate(...params: unknown[]): IterableIterator<Record<string, unknown>>
iterate(...params: unknown[]): IterableIterator<Record<string, unknown> | 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. */
Expand Down
10 changes: 8 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
)
Expand Down
2 changes: 1 addition & 1 deletion src/bun_compat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
53 changes: 43 additions & 10 deletions src/statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_);
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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),
});
Expand Down
8 changes: 8 additions & 0 deletions src/statement.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,20 @@ class Statement : public Napi::ObjectWrap<Statement> {
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);

Expand Down
51 changes: 37 additions & 14 deletions src/util.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
#include <string>

// 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: {
Expand All @@ -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()) {
Expand Down Expand Up @@ -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()
Expand All @@ -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<Napi::String>().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;
Expand Down
48 changes: 48 additions & 0 deletions test/compat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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")
Expand Down
5 changes: 5 additions & 0 deletions test/statement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading