From 9f6e242e42fb092bd06fbaed8fe301e0150115f2 Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Sun, 18 May 2025 00:51:23 -0300 Subject: [PATCH 1/2] bind/js: Partially implements iterate() method The API still is sync and isn't variadic --- Cargo.lock | 4 +- bindings/javascript/Cargo.toml | 2 +- .../__test__/better-sqlite3.spec.mjs | 24 ++++++-- bindings/javascript/__test__/limbo.spec.mjs | 18 +++++- bindings/javascript/src/lib.rs | 59 +++++++++++++++++-- 5 files changed, 91 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 003b2fa28..ea96fbf44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2135,9 +2135,9 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.1.6" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28acfa557c083f6e254a786e01ba253fc56f18ee000afcd4f79af735f73a6da" +checksum = "03acbfa4f156a32188bfa09b86dc11a431b5725253fc1fc6f6df5bed273382c4" [[package]] name = "napi-derive" diff --git a/bindings/javascript/Cargo.toml b/bindings/javascript/Cargo.toml index 6dfd21312..bffbe8630 100644 --- a/bindings/javascript/Cargo.toml +++ b/bindings/javascript/Cargo.toml @@ -16,4 +16,4 @@ napi = { version = "2.16.17", default-features = false, features = ["napi4"] } napi-derive = { version = "2.16.13", default-features = false } [build-dependencies] -napi-build = "2.0.1" +napi-build = "2.2.0" diff --git a/bindings/javascript/__test__/better-sqlite3.spec.mjs b/bindings/javascript/__test__/better-sqlite3.spec.mjs index 0af24075e..aefe0aa92 100644 --- a/bindings/javascript/__test__/better-sqlite3.spec.mjs +++ b/bindings/javascript/__test__/better-sqlite3.spec.mjs @@ -7,7 +7,6 @@ test("Open in-memory database", async (t) => { t.is(db.memory, true); }); - test("Statement.get() returns data", async (t) => { const [db] = await connect(":memory:"); const stmt = db.prepare("SELECT 1"); @@ -31,11 +30,26 @@ test("Statement.run() returns correct result object", async (t) => { t.deepEqual(rows, { changes: 1, lastInsertRowid: 1 }); }); +test("Statment.iterate() should correctly return an iterable object", async (t) => { + const [db] = await connect(":memory:"); + db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run(); + db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 42); + db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Bob", 24); + let rows = db.prepare("SELECT * FROM users").iterate(); + for (const row of rows) { + t.truthy(row.name); + t.true(typeof row.age === "number"); + } +}); + test("Empty prepared statement should throw", async (t) => { - const [db] = await connect(":memory:"); - t.throws(() => { - db.prepare(""); - }, { instanceOf: Error }); + const [db] = await connect(":memory:"); + t.throws( + () => { + db.prepare(""); + }, + { instanceOf: Error }, + ); }); const connect = async (path) => { diff --git a/bindings/javascript/__test__/limbo.spec.mjs b/bindings/javascript/__test__/limbo.spec.mjs index 617898d78..8657fb8dd 100644 --- a/bindings/javascript/__test__/limbo.spec.mjs +++ b/bindings/javascript/__test__/limbo.spec.mjs @@ -27,10 +27,22 @@ test("Statement.get() returns null when no data", async (t) => { // it should return a result object, not a row object test("Statement.run() returns correct result object", async (t) => { const [db] = await connect(":memory:"); - db.prepare("CREATE TABLE users (name TEXT)").run(); - db.prepare("INSERT INTO users (name) VALUES (?)").run(["Alice"]); + db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run(); + db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run(["Alice", 42]); let rows = db.prepare("SELECT * FROM users").all(); - t.deepEqual(rows, [{ name: "Alice" }]); + t.deepEqual(rows, [{ name: "Alice", age: 42 }]); +}); + +test("Statment.iterate() should correctly return an iterable object", async (t) => { + const [db] = await connect(":memory:"); + db.prepare("CREATE TABLE users (name TEXT, age INTEGER)").run(); + db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run(["Alice", 42]); + db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run(["Bob", 24]); + let rows = db.prepare("SELECT * FROM users").iterate(); + for (const row of rows) { + t.truthy(row.name); + t.true(typeof row.age === "number"); + } }); test("Empty prepared statement should throw", async (t) => { diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index dd7668535..016c47fc4 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -8,6 +8,8 @@ use std::sync::Arc; use limbo_core::types::Text; use limbo_core::{maybe_init_database_file, LimboError}; +use napi::iterator::Generator; +use napi::JsObject; use napi::{bindgen_prelude::ObjectFinalize, Env, JsUnknown}; use napi_derive::napi; @@ -136,20 +138,23 @@ impl Database { #[napi] pub struct Statement { // TODO: implement each property when core supports it - // #[napi(writable = false)] + // #[napi(able = false)] // pub reader: bool, // #[napi(writable = false)] // pub readonly: bool, // #[napi(writable = false)] // pub busy: bool, database: Database, - inner: RefCell, + inner: Rc>, } #[napi] impl Statement { pub fn new(inner: RefCell, database: Database) -> Self { - Self { inner, database } + Self { + inner: Rc::new(inner), + database, + } } #[napi] @@ -194,8 +199,12 @@ impl Statement { } #[napi] - pub fn iterate() { - todo!() + pub fn iterate(&self, env: Env) -> IteratorStatement { + IteratorStatement { + stmt: Rc::clone(&self.inner), + database: self.database.clone(), + env, + } } #[napi] @@ -266,6 +275,46 @@ impl Statement { } } +#[napi(iterator)] +pub struct IteratorStatement { + stmt: Rc>, + database: Database, + env: Env, +} + +impl Generator for IteratorStatement { + type Yield = JsObject; + + type Next = (); + + type Return = (); + + fn next(&mut self, _: Option) -> Option { + let mut stmt = self.stmt.borrow_mut(); + + match stmt.step().ok()? { + limbo_core::StepResult::Row => { + let row = stmt.row().unwrap(); + let mut js_row = self.env.create_object().ok()?; + + for (idx, value) in row.get_values().enumerate() { + let key = stmt.get_column_name(idx); + let js_value = to_js_value(&self.env, value); + js_row.set_named_property(&key, js_value).ok()?; + } + + Some(js_row) + } + limbo_core::StepResult::Done => None, + limbo_core::StepResult::IO => { + self.database.io.run_once().ok()?; + None // clearly it's incorrect it should return to user + } + limbo_core::StepResult::Interrupt | limbo_core::StepResult::Busy => None, + } + } +} + fn to_js_value(env: &napi::Env, value: &limbo_core::Value) -> napi::Result { match value { limbo_core::Value::Null => Ok(env.get_null()?.into_unknown()), From bc88b7cb6585d389658463e96486f315cf78fdb4 Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Sun, 18 May 2025 00:51:49 -0300 Subject: [PATCH 2/2] bind/js: Formatting --- bindings/javascript/index.js | 309 ++++++++++++++++++----------------- 1 file changed, 161 insertions(+), 148 deletions(-) diff --git a/bindings/javascript/index.js b/bindings/javascript/index.js index eefe328aa..6ea5e29f8 100644 --- a/bindings/javascript/index.js +++ b/bindings/javascript/index.js @@ -5,312 +5,325 @@ /* auto-generated by NAPI-RS */ const { existsSync, readFileSync } = require('fs') -const { join } = require('path') +const { join } = require("path"); -const { platform, arch } = process +const { platform, arch } = process; -let nativeBinding = null -let localFileExisted = false -let loadError = null +let nativeBinding = null; +let localFileExisted = false; +let loadError = null; function isMusl() { // For Node 10 - if (!process.report || typeof process.report.getReport !== 'function') { + if (!process.report || typeof process.report.getReport !== "function") { try { - const lddPath = require('child_process').execSync('which ldd').toString().trim() - return readFileSync(lddPath, 'utf8').includes('musl') + const lddPath = require("child_process") + .execSync("which ldd") + .toString() + .trim(); + return readFileSync(lddPath, "utf8").includes("musl"); } catch (e) { - return true + return true; } } else { - const { glibcVersionRuntime } = process.report.getReport().header - return !glibcVersionRuntime + const { glibcVersionRuntime } = process.report.getReport().header; + return !glibcVersionRuntime; } } switch (platform) { - case 'android': + case "android": switch (arch) { - case 'arm64': - localFileExisted = existsSync(join(__dirname, 'turso-limbo.android-arm64.node')) + case "arm64": + localFileExisted = existsSync( + join(__dirname, "turso-limbo.android-arm64.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.android-arm64.node') + nativeBinding = require("./turso-limbo.android-arm64.node"); } else { - nativeBinding = require('@tursodatabase/limbo-android-arm64') + nativeBinding = require("@tursodatabase/limbo-android-arm64"); } } catch (e) { - loadError = e + loadError = e; } - break - case 'arm': - localFileExisted = existsSync(join(__dirname, 'turso-limbo.android-arm-eabi.node')) + break; + case "arm": + localFileExisted = existsSync( + join(__dirname, "turso-limbo.android-arm-eabi.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.android-arm-eabi.node') + nativeBinding = require("./turso-limbo.android-arm-eabi.node"); } else { - nativeBinding = require('@tursodatabase/limbo-android-arm-eabi') + nativeBinding = require("@tursodatabase/limbo-android-arm-eabi"); } } catch (e) { - loadError = e + loadError = e; } - break + break; default: - throw new Error(`Unsupported architecture on Android ${arch}`) + throw new Error(`Unsupported architecture on Android ${arch}`); } - break - case 'win32': + break; + case "win32": switch (arch) { - case 'x64': + case "x64": localFileExisted = existsSync( - join(__dirname, 'turso-limbo.win32-x64-msvc.node') - ) + join(__dirname, "turso-limbo.win32-x64-msvc.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.win32-x64-msvc.node') + nativeBinding = require("./turso-limbo.win32-x64-msvc.node"); } else { - nativeBinding = require('@tursodatabase/limbo-win32-x64-msvc') + nativeBinding = require("@tursodatabase/limbo-win32-x64-msvc"); } } catch (e) { - loadError = e + loadError = e; } - break - case 'ia32': + break; + case "ia32": localFileExisted = existsSync( - join(__dirname, 'turso-limbo.win32-ia32-msvc.node') - ) + join(__dirname, "turso-limbo.win32-ia32-msvc.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.win32-ia32-msvc.node') + nativeBinding = require("./turso-limbo.win32-ia32-msvc.node"); } else { - nativeBinding = require('@tursodatabase/limbo-win32-ia32-msvc') + nativeBinding = require("@tursodatabase/limbo-win32-ia32-msvc"); } } catch (e) { - loadError = e + loadError = e; } - break - case 'arm64': + break; + case "arm64": localFileExisted = existsSync( - join(__dirname, 'turso-limbo.win32-arm64-msvc.node') - ) + join(__dirname, "turso-limbo.win32-arm64-msvc.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.win32-arm64-msvc.node') + nativeBinding = require("./turso-limbo.win32-arm64-msvc.node"); } else { - nativeBinding = require('@tursodatabase/limbo-win32-arm64-msvc') + nativeBinding = require("@tursodatabase/limbo-win32-arm64-msvc"); } } catch (e) { - loadError = e + loadError = e; } - break + break; default: - throw new Error(`Unsupported architecture on Windows: ${arch}`) + throw new Error(`Unsupported architecture on Windows: ${arch}`); } - break - case 'darwin': - localFileExisted = existsSync(join(__dirname, 'turso-limbo.darwin-universal.node')) + break; + case "darwin": + localFileExisted = existsSync( + join(__dirname, "turso-limbo.darwin-universal.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.darwin-universal.node') + nativeBinding = require("./turso-limbo.darwin-universal.node"); } else { - nativeBinding = require('@tursodatabase/limbo-darwin-universal') + nativeBinding = require("@tursodatabase/limbo-darwin-universal"); } - break + break; } catch {} switch (arch) { - case 'x64': - localFileExisted = existsSync(join(__dirname, 'turso-limbo.darwin-x64.node')) - try { - if (localFileExisted) { - nativeBinding = require('./turso-limbo.darwin-x64.node') - } else { - nativeBinding = require('@tursodatabase/limbo-darwin-x64') - } - } catch (e) { - loadError = e - } - break - case 'arm64': + case "x64": localFileExisted = existsSync( - join(__dirname, 'turso-limbo.darwin-arm64.node') - ) + join(__dirname, "turso-limbo.darwin-x64.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.darwin-arm64.node') + nativeBinding = require("./turso-limbo.darwin-x64.node"); } else { - nativeBinding = require('@tursodatabase/limbo-darwin-arm64') + nativeBinding = require("@tursodatabase/limbo-darwin-x64"); } } catch (e) { - loadError = e + loadError = e; } - break + break; + case "arm64": + localFileExisted = existsSync( + join(__dirname, "turso-limbo.darwin-arm64.node"), + ); + try { + if (localFileExisted) { + nativeBinding = require("./turso-limbo.darwin-arm64.node"); + } else { + nativeBinding = require("@tursodatabase/limbo-darwin-arm64"); + } + } catch (e) { + loadError = e; + } + break; default: - throw new Error(`Unsupported architecture on macOS: ${arch}`) + throw new Error(`Unsupported architecture on macOS: ${arch}`); } - break - case 'freebsd': - if (arch !== 'x64') { - throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) + break; + case "freebsd": + if (arch !== "x64") { + throw new Error(`Unsupported architecture on FreeBSD: ${arch}`); } - localFileExisted = existsSync(join(__dirname, 'turso-limbo.freebsd-x64.node')) + localFileExisted = existsSync( + join(__dirname, "turso-limbo.freebsd-x64.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.freebsd-x64.node') + nativeBinding = require("./turso-limbo.freebsd-x64.node"); } else { - nativeBinding = require('@tursodatabase/limbo-freebsd-x64') + nativeBinding = require("@tursodatabase/limbo-freebsd-x64"); } } catch (e) { - loadError = e + loadError = e; } - break - case 'linux': + break; + case "linux": switch (arch) { - case 'x64': + case "x64": if (isMusl()) { localFileExisted = existsSync( - join(__dirname, 'turso-limbo.linux-x64-musl.node') - ) + join(__dirname, "turso-limbo.linux-x64-musl.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.linux-x64-musl.node') + nativeBinding = require("./turso-limbo.linux-x64-musl.node"); } else { - nativeBinding = require('@tursodatabase/limbo-linux-x64-musl') + nativeBinding = require("@tursodatabase/limbo-linux-x64-musl"); } } catch (e) { - loadError = e + loadError = e; } } else { localFileExisted = existsSync( - join(__dirname, 'turso-limbo.linux-x64-gnu.node') - ) + join(__dirname, "turso-limbo.linux-x64-gnu.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.linux-x64-gnu.node') + nativeBinding = require("./turso-limbo.linux-x64-gnu.node"); } else { - nativeBinding = require('@tursodatabase/limbo-linux-x64-gnu') + nativeBinding = require("@tursodatabase/limbo-linux-x64-gnu"); } } catch (e) { - loadError = e + loadError = e; } } - break - case 'arm64': + break; + case "arm64": if (isMusl()) { localFileExisted = existsSync( - join(__dirname, 'turso-limbo.linux-arm64-musl.node') - ) + join(__dirname, "turso-limbo.linux-arm64-musl.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.linux-arm64-musl.node') + nativeBinding = require("./turso-limbo.linux-arm64-musl.node"); } else { - nativeBinding = require('@tursodatabase/limbo-linux-arm64-musl') + nativeBinding = require("@tursodatabase/limbo-linux-arm64-musl"); } } catch (e) { - loadError = e + loadError = e; } } else { localFileExisted = existsSync( - join(__dirname, 'turso-limbo.linux-arm64-gnu.node') - ) + join(__dirname, "turso-limbo.linux-arm64-gnu.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.linux-arm64-gnu.node') + nativeBinding = require("./turso-limbo.linux-arm64-gnu.node"); } else { - nativeBinding = require('@tursodatabase/limbo-linux-arm64-gnu') + nativeBinding = require("@tursodatabase/limbo-linux-arm64-gnu"); } } catch (e) { - loadError = e + loadError = e; } } - break - case 'arm': + break; + case "arm": if (isMusl()) { localFileExisted = existsSync( - join(__dirname, 'turso-limbo.linux-arm-musleabihf.node') - ) + join(__dirname, "turso-limbo.linux-arm-musleabihf.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.linux-arm-musleabihf.node') + nativeBinding = require("./turso-limbo.linux-arm-musleabihf.node"); } else { - nativeBinding = require('@tursodatabase/limbo-linux-arm-musleabihf') + nativeBinding = require("@tursodatabase/limbo-linux-arm-musleabihf"); } } catch (e) { - loadError = e + loadError = e; } } else { localFileExisted = existsSync( - join(__dirname, 'turso-limbo.linux-arm-gnueabihf.node') - ) + join(__dirname, "turso-limbo.linux-arm-gnueabihf.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.linux-arm-gnueabihf.node') + nativeBinding = require("./turso-limbo.linux-arm-gnueabihf.node"); } else { - nativeBinding = require('@tursodatabase/limbo-linux-arm-gnueabihf') + nativeBinding = require("@tursodatabase/limbo-linux-arm-gnueabihf"); } } catch (e) { - loadError = e + loadError = e; } } - break - case 'riscv64': + break; + case "riscv64": if (isMusl()) { localFileExisted = existsSync( - join(__dirname, 'turso-limbo.linux-riscv64-musl.node') - ) + join(__dirname, "turso-limbo.linux-riscv64-musl.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.linux-riscv64-musl.node') + nativeBinding = require("./turso-limbo.linux-riscv64-musl.node"); } else { - nativeBinding = require('@tursodatabase/limbo-linux-riscv64-musl') + nativeBinding = require("@tursodatabase/limbo-linux-riscv64-musl"); } } catch (e) { - loadError = e + loadError = e; } } else { localFileExisted = existsSync( - join(__dirname, 'turso-limbo.linux-riscv64-gnu.node') - ) + join(__dirname, "turso-limbo.linux-riscv64-gnu.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.linux-riscv64-gnu.node') + nativeBinding = require("./turso-limbo.linux-riscv64-gnu.node"); } else { - nativeBinding = require('@tursodatabase/limbo-linux-riscv64-gnu') + nativeBinding = require("@tursodatabase/limbo-linux-riscv64-gnu"); } } catch (e) { - loadError = e + loadError = e; } } - break - case 's390x': + break; + case "s390x": localFileExisted = existsSync( - join(__dirname, 'turso-limbo.linux-s390x-gnu.node') - ) + join(__dirname, "turso-limbo.linux-s390x-gnu.node"), + ); try { if (localFileExisted) { - nativeBinding = require('./turso-limbo.linux-s390x-gnu.node') + nativeBinding = require("./turso-limbo.linux-s390x-gnu.node"); } else { - nativeBinding = require('@tursodatabase/limbo-linux-s390x-gnu') + nativeBinding = require("@tursodatabase/limbo-linux-s390x-gnu"); } } catch (e) { - loadError = e + loadError = e; } - break + break; default: - throw new Error(`Unsupported architecture on Linux: ${arch}`) + throw new Error(`Unsupported architecture on Linux: ${arch}`); } - break + break; default: - throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`) + throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`); } if (!nativeBinding) { if (loadError) { - throw loadError + throw loadError; } - throw new Error(`Failed to load native binding`) + throw new Error(`Failed to load native binding`); } -const { Database, Statement } = nativeBinding +const { Database, Statement } = nativeBinding; -module.exports.Database = Database -module.exports.Statement = Statement +module.exports.Database = Database; +module.exports.Statement = Statement;