From 9f6e242e42fb092bd06fbaed8fe301e0150115f2 Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Sun, 18 May 2025 00:51:23 -0300 Subject: [PATCH] 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()),