From 48056e0941044a435d1a8fa65fadf2a08e9d81cb Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Wed, 4 Jun 2025 09:20:32 -0300 Subject: [PATCH 1/6] bind/js: Refactor presentation modes to use an enum --- bindings/javascript/src/lib.rs | 219 ++++++++++++++++----------------- 1 file changed, 104 insertions(+), 115 deletions(-) diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index f587d6ccb..29c9e1b9f 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -202,6 +202,13 @@ impl Database { } } +#[derive(Debug, Clone)] +enum PresentationMode { + Raw, + Pluck, + None, +} + #[napi] #[derive(Clone)] pub struct Statement { @@ -216,8 +223,7 @@ pub struct Statement { pub source: String, database: Database, - raw: bool, - pluck: bool, + presentation_mode: PresentationMode, binded: bool, inner: Rc>, } @@ -229,9 +235,8 @@ impl Statement { inner: Rc::new(inner), database, source, - pluck: false, + presentation_mode: PresentationMode::None, binded: false, - raw: false, } } @@ -244,43 +249,44 @@ impl Statement { limbo_core::StepResult::Row => { let row = stmt.row().unwrap(); - if self.raw { - assert!(!self.pluck, "Cannot use raw mode with pluck mode"); + match self.presentation_mode { + PresentationMode::Raw => { + let mut raw_obj = env.create_array(row.len() as u32)?; + for (idx, value) in row.get_values().enumerate() { + let js_value = to_js_value(&env, value); - let mut raw_obj = env.create_array(row.len() as u32)?; - for (idx, value) in row.get_values().enumerate() { - let js_value = to_js_value(&env, value); + raw_obj.set(idx as u32, js_value)?; + } - raw_obj.set(idx as u32, js_value)?; + Ok(raw_obj.coerce_to_object()?.into_unknown()) } + PresentationMode::Pluck => { + let mut obj = env.create_object()?; - return Ok(raw_obj.coerce_to_object()?.into_unknown()); + let (idx, value) = + row.get_values().enumerate().next().ok_or(napi::Error::new( + napi::Status::GenericFailure, + "Pluck mode requires at least one column in the result", + ))?; + let key = stmt.get_column_name(idx); + let js_value = to_js_value(&env, value); + obj.set_named_property(&key, js_value)?; + + Ok(obj.into_unknown()) + } + PresentationMode::None => { + let mut obj = env.create_object()?; + + for (idx, value) in row.get_values().enumerate() { + let key = stmt.get_column_name(idx); + let js_value = to_js_value(&env, value); + + obj.set_named_property(&key, js_value)?; + } + + Ok(obj.into_unknown()) + } } - - let mut obj = env.create_object()?; - if self.pluck { - assert!(!self.raw, "Cannot use pluck mode with raw mode"); - - let (idx, value) = - row.get_values().enumerate().next().ok_or(napi::Error::new( - napi::Status::GenericFailure, - "Pluck mode requires at least one column in the result", - ))?; - let key = stmt.get_column_name(idx); - let js_value = to_js_value(&env, value); - obj.set_named_property(&key, js_value)?; - - return Ok(obj.into_unknown()); - } - - for (idx, value) in row.get_values().enumerate() { - let key = stmt.get_column_name(idx); - let js_value = to_js_value(&env, value); - - obj.set_named_property(&key, js_value)?; - } - - Ok(obj.into_unknown()) } limbo_core::StepResult::Done => Ok(env.get_undefined()?.into_unknown()), limbo_core::StepResult::IO => todo!(), @@ -305,20 +311,11 @@ impl Statement { args: Option>, ) -> napi::Result { self.check_and_bind(args)?; - if self.raw { - assert!(!self.pluck, "Cannot use raw mode with pluck mode"); - } - - if self.pluck { - assert!(!self.raw, "Cannot use pluck mode with raw mode"); - } - Ok(IteratorStatement { stmt: Rc::clone(&self.inner), database: self.database.clone(), env, - pluck: self.pluck, - raw: self.raw, + presentation_mode: self.presentation_mode.clone(), }) } @@ -342,42 +339,40 @@ impl Statement { let row = stmt.row().unwrap(); let mut obj = env.create_object()?; - if self.raw { - assert!(!self.pluck, "Cannot use raw mode with pluck mode"); - - let mut raw_array = env.create_array(row.len() as u32)?; - for (idx, value) in row.get_values().enumerate() { - let js_value = to_js_value(&env, value)?; - raw_array.set(idx as u32, js_value)?; + match self.presentation_mode { + PresentationMode::Raw => { + let mut raw_array = env.create_array(row.len() as u32)?; + for (idx, value) in row.get_values().enumerate() { + let js_value = to_js_value(&env, value)?; + raw_array.set(idx as u32, js_value)?; + } + results.set_element(index, raw_array.coerce_to_object()?)?; + index += 1; + continue; + } + PresentationMode::Pluck => { + let (idx, value) = + row.get_values().enumerate().next().ok_or(napi::Error::new( + napi::Status::GenericFailure, + "Pluck mode requires at least one column in the result", + ))?; + let key = stmt.get_column_name(idx); + let js_value = to_js_value(&env, value)?; + obj.set_named_property(&key, js_value)?; + results.set_element(index, obj)?; + index += 1; + continue; + } + PresentationMode::None => { + for (idx, value) in row.get_values().enumerate() { + let key = stmt.get_column_name(idx); + let js_value = to_js_value(&env, value); + obj.set_named_property(&key, js_value)?; + } + results.set_element(index, obj)?; + index += 1; } - results.set_element(index, raw_array.coerce_to_object()?)?; - index += 1; - continue; } - - if self.pluck { - assert!(!self.raw, "Cannot use pluck mode with raw mode"); - - let (idx, value) = - row.get_values().enumerate().next().ok_or(napi::Error::new( - napi::Status::GenericFailure, - "Pluck mode requires at least one column in the result", - ))?; - let key = stmt.get_column_name(idx); - let js_value = to_js_value(&env, value)?; - obj.set_named_property(&key, js_value)?; - results.set_element(index, obj)?; - index += 1; - continue; - } - - for (idx, value) in row.get_values().enumerate() { - let key = stmt.get_column_name(idx); - let js_value = to_js_value(&env, value); - obj.set_named_property(&key, js_value)?; - } - results.set_element(index, obj)?; - index += 1; } limbo_core::StepResult::Done => { break; @@ -400,11 +395,10 @@ impl Statement { #[napi] pub fn pluck(&mut self, pluck: Option) { if let Some(false) = pluck { - self.pluck = false; + self.presentation_mode = PresentationMode::None; } - self.raw = false; - self.pluck = true; + self.presentation_mode = PresentationMode::Pluck; } #[napi] @@ -415,11 +409,10 @@ impl Statement { #[napi] pub fn raw(&mut self, raw: Option) { if let Some(false) = raw { - self.raw = false; + self.presentation_mode = PresentationMode::None; } - self.pluck = false; - self.raw = true; + self.presentation_mode = PresentationMode::Raw; } #[napi] @@ -466,8 +459,7 @@ pub struct IteratorStatement { stmt: Rc>, database: Database, env: Env, - pluck: bool, - raw: bool, + presentation_mode: PresentationMode, } impl Generator for IteratorStatement { @@ -485,41 +477,38 @@ impl Generator for IteratorStatement { let row = stmt.row().unwrap(); let mut js_row = self.env.create_object().ok()?; - if self.raw { - assert!(!self.pluck, "Cannot use raw mode with pluck mode"); + match self.presentation_mode { + PresentationMode::Raw => { + let mut raw_array = self.env.create_array(row.len() as u32).ok()?; + for (idx, value) in row.get_values().enumerate() { + let js_value = to_js_value(&self.env, value); + raw_array.set(idx as u32, js_value).ok()?; + } - let mut raw_array = self.env.create_array(row.len() as u32).ok()?; - for (idx, value) in row.get_values().enumerate() { - let js_value = to_js_value(&self.env, value); - raw_array.set(idx as u32, js_value).ok()?; + raw_array.coerce_to_object().ok() } + PresentationMode::Pluck => { + let (idx, value) = row.get_values().enumerate().next()?; + 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) + } + PresentationMode::None => { + 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()?; + } - // TODO: fix this unwrap - return Some(raw_array.coerce_to_object().unwrap()); + Some(js_row) + } } - - if self.pluck { - assert!(!self.raw, "Cannot use pluck mode with raw mode"); - - let (idx, value) = row.get_values().enumerate().next()?; - 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()?; - return Some(js_row); - } - - 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 + None // clearly it's incorrect, it should return to user } limbo_core::StepResult::Interrupt | limbo_core::StepResult::Busy => None, } From 9f00f84f5b02006e4d1f7acc3bf4e03539093b5c Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Wed, 4 Jun 2025 09:21:22 -0300 Subject: [PATCH 2/6] bind/js: Improve test suite to mirror behaviour --- .../__test__/better-sqlite3.spec.mjs | 115 ++++++++++++++++-- bindings/javascript/__test__/limbo.spec.mjs | 68 +++++++++-- 2 files changed, 163 insertions(+), 20 deletions(-) diff --git a/bindings/javascript/__test__/better-sqlite3.spec.mjs b/bindings/javascript/__test__/better-sqlite3.spec.mjs index a271648ba..3c539c686 100644 --- a/bindings/javascript/__test__/better-sqlite3.spec.mjs +++ b/bindings/javascript/__test__/better-sqlite3.spec.mjs @@ -35,12 +35,23 @@ test("Statement.run() returns correct result object", async (t) => { 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); + db.prepare( + "CREATE TABLE users (name TEXT, age INTEGER, nationality TEXT)", + ).run(); + db.prepare("INSERT INTO users (name, age, nationality) VALUES (?, ?, ?)").run( + ["Alice", 42], + "UK", + ); + db.prepare("INSERT INTO users (name, age, nationality) VALUES (?, ?, ?)").run( + "Bob", + 24, + "USA", + ); + let rows = db.prepare("SELECT * FROM users").iterate(); for (const row of rows) { t.truthy(row.name); + t.truthy(row.nationality); t.true(typeof row.age === "number"); } }); @@ -55,18 +66,17 @@ test("Empty prepared statement should throw", async (t) => { ); }); -test("Test pragma", async (t) => { +test("Test pragma()", async (t) => { const [db] = await connect(":memory:"); t.deepEqual(typeof db.pragma("cache_size")[0].cache_size, "number"); t.deepEqual(typeof db.pragma("cache_size", { simple: true }), "number"); }); -test("Test bind()", async (t) => { +test("Statement shouldn't bind twice with bind()", 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); let stmt = db.prepare("SELECT * FROM users WHERE name = ?").bind("Alice"); - console.log(db.prepare("SELECT * FROM users").raw().get()); for (const row of stmt.iterate()) { t.truthy(row.name); @@ -81,7 +91,98 @@ test("Test bind()", async (t) => { ); }); -test("Test exec()", async (t) => { +test("Test pluck(): Rows should only have the values of the first column", 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 stmt = db.prepare("SELECT * FROM users").pluck(); + + for (const row of stmt.iterate()) { + t.truthy(row); + t.assert(typeof row === "string"); + } +}); + +test("Test raw(): Rows should be returned as arrays", 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 stmt = db.prepare("SELECT * FROM users").raw(); + + for (const row of stmt.iterate()) { + t.true(Array.isArray(row)); + t.true(typeof row[0] === "string"); + t.true(typeof row[1] === "number"); + } + + stmt = db.prepare("SELECT * FROM users WHERE name = ?").raw(); + const row = stmt.get("Alice"); + t.true(Array.isArray(row)); + t.is(row.length, 2); + t.is(row[0], "Alice"); + t.is(row[1], 42); + + const noRow = stmt.get("Charlie"); + t.is(noRow, undefined); + + stmt = db.prepare("SELECT * FROM users").raw(); + const rows = stmt.all(); + t.true(Array.isArray(rows)); + t.is(rows.length, 2); + t.deepEqual(rows[0], ["Alice", 42]); + t.deepEqual(rows[1], ["Bob", 24]); +}); + + +test("Presentation modes should be mutually exclusive", 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); + + + // test raw() + let stmt = db.prepare("SELECT * FROM users").pluck().raw(); + + for (const row of stmt.iterate()) { + t.true(Array.isArray(row)); + t.true(typeof row[0] === "string"); + t.true(typeof row[1] === "number"); + } + + stmt = db.prepare("SELECT * FROM users WHERE name = ?").raw(); + const row = stmt.get("Alice"); + t.true(Array.isArray(row)); + t.is(row.length, 2); + t.is(row[0], "Alice"); + t.is(row[1], 42); + + const noRow = stmt.get("Charlie"); + t.is(noRow, undefined); + + stmt = db.prepare("SELECT * FROM users").raw(); + const rows = stmt.all(); + t.true(Array.isArray(rows)); + t.is(rows.length, 2); + t.deepEqual(rows[0], ["Alice", 42]); + t.deepEqual(rows[1], ["Bob", 24]); + + // test pluck() + stmt = db.prepare("SELECT * FROM users").raw().pluck(); + + for (const name of stmt.iterate()) { + t.truthy(name); + t.assert(typeof name === "string"); + } +}); + + +test("Test exec(): Should correctly load multiple statements from file", async (t) => { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); diff --git a/bindings/javascript/__test__/limbo.spec.mjs b/bindings/javascript/__test__/limbo.spec.mjs index 51cf204ec..9f9ca8941 100644 --- a/bindings/javascript/__test__/limbo.spec.mjs +++ b/bindings/javascript/__test__/limbo.spec.mjs @@ -19,7 +19,7 @@ test("Statement.get() returns data", async (t) => { t.is(result2["1"], 1); }); -test("Statement.get() returns null when no data", async (t) => { +test("Statement.get() returns undefined when no data", async (t) => { const [db] = await connect(":memory:"); const stmt = db.prepare("SELECT 1 WHERE 1 = 2"); const result = stmt.get(); @@ -41,11 +41,11 @@ test("Statment.iterate() should correctly return an iterable object", async (t) db.prepare( "CREATE TABLE users (name TEXT, age INTEGER, nationality TEXT)", ).run(); - db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run( + db.prepare("INSERT INTO users (name, age, nationality) VALUES (?, ?, ?)").run( ["Alice", 42], "UK", ); - db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run( + db.prepare("INSERT INTO users (name, age, nationality) VALUES (?, ?, ?)").run( "Bob", 24, "USA", @@ -54,6 +54,7 @@ test("Statment.iterate() should correctly return an iterable object", async (t) let rows = db.prepare("SELECT * FROM users").iterate(); for (const row of rows) { t.truthy(row.name); + t.truthy(row.nationality); t.true(typeof row.age === "number"); } }); @@ -74,7 +75,7 @@ test("Test pragma()", async (t) => { t.true(typeof db.pragma("cache_size", { simple: true }) === "number"); }); -test("Statement binded with bind() shouldn't be binded again", async (t) => { +test("Statement shouldn't bind twice with bind()", 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); @@ -98,23 +99,23 @@ test("Test pluck(): Rows should only have the values of the first column", async 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 stmt = db.prepare("SELECT * FROM users").raw().pluck(); + + let stmt = db.prepare("SELECT * FROM users").pluck(); for (const row of stmt.iterate()) { - t.truthy(row.name); - t.true(typeof row.age === "undefined"); + t.truthy(row); + t.assert(typeof row === "string"); } }); - -test("Test raw()", async (t) => { +test("Test raw(): Rows should be returned as arrays", 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); - - // Pluck and raw should be exclusive - let stmt = db.prepare("SELECT * FROM users").pluck().raw(); + + + let stmt = db.prepare("SELECT * FROM users").raw(); for (const row of stmt.iterate()) { t.true(Array.isArray(row)); @@ -140,8 +141,49 @@ test("Test raw()", async (t) => { t.deepEqual(rows[1], ["Bob", 24]); }); +test("Presentation modes should be mutually exclusive", 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); -test("Test exec()", async (t) => { + + // test raw() + let stmt = db.prepare("SELECT * FROM users").pluck().raw(); + + for (const row of stmt.iterate()) { + t.true(Array.isArray(row)); + t.true(typeof row[0] === "string"); + t.true(typeof row[1] === "number"); + } + + stmt = db.prepare("SELECT * FROM users WHERE name = ?").raw(); + const row = stmt.get("Alice"); + t.true(Array.isArray(row)); + t.is(row.length, 2); + t.is(row[0], "Alice"); + t.is(row[1], 42); + + const noRow = stmt.get("Charlie"); + t.is(noRow, undefined); + + stmt = db.prepare("SELECT * FROM users").raw(); + const rows = stmt.all(); + t.true(Array.isArray(rows)); + t.is(rows.length, 2); + t.deepEqual(rows[0], ["Alice", 42]); + t.deepEqual(rows[1], ["Bob", 24]); + + // test pluck() + stmt = db.prepare("SELECT * FROM users").raw().pluck(); + + for (const name of stmt.iterate()) { + t.truthy(name); + t.assert(typeof name === "string"); + } +}); + +test("Test exec(): Should correctly load multiple statements from file", async (t) => { const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); From 4c47e8c4dd20f1153988a4a8b34d43faab20c647 Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Wed, 4 Jun 2025 09:39:22 -0300 Subject: [PATCH 3/6] bind/js: Fix incompatible pluck mode --- bindings/javascript/src/lib.rs | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index 29c9e1b9f..315cd9d5e 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -9,7 +9,6 @@ use std::sync::Arc; use limbo_core::types::Text; use limbo_core::{maybe_init_database_file, LimboError, StepResult}; use napi::iterator::Generator; -use napi::JsObject; use napi::{bindgen_prelude::ObjectFinalize, Env, JsUnknown}; use napi_derive::napi; @@ -261,18 +260,14 @@ impl Statement { Ok(raw_obj.coerce_to_object()?.into_unknown()) } PresentationMode::Pluck => { - let mut obj = env.create_object()?; - - let (idx, value) = + let (_, value) = row.get_values().enumerate().next().ok_or(napi::Error::new( napi::Status::GenericFailure, "Pluck mode requires at least one column in the result", ))?; - let key = stmt.get_column_name(idx); - let js_value = to_js_value(&env, value); - obj.set_named_property(&key, js_value)?; + let js_value = to_js_value(&env, value)?; - Ok(obj.into_unknown()) + Ok(js_value) } PresentationMode::None => { let mut obj = env.create_object()?; @@ -337,7 +332,6 @@ impl Statement { match stmt.step().map_err(into_napi_error)? { limbo_core::StepResult::Row => { let row = stmt.row().unwrap(); - let mut obj = env.create_object()?; match self.presentation_mode { PresentationMode::Raw => { @@ -351,19 +345,18 @@ impl Statement { continue; } PresentationMode::Pluck => { - let (idx, value) = + let (_, value) = row.get_values().enumerate().next().ok_or(napi::Error::new( napi::Status::GenericFailure, "Pluck mode requires at least one column in the result", ))?; - let key = stmt.get_column_name(idx); let js_value = to_js_value(&env, value)?; - obj.set_named_property(&key, js_value)?; - results.set_element(index, obj)?; + results.set_element(index, js_value)?; index += 1; continue; } PresentationMode::None => { + let mut obj = env.create_object()?; for (idx, value) in row.get_values().enumerate() { let key = stmt.get_column_name(idx); let js_value = to_js_value(&env, value); @@ -463,7 +456,7 @@ pub struct IteratorStatement { } impl Generator for IteratorStatement { - type Yield = JsObject; + type Yield = JsUnknown; type Next = (); @@ -475,7 +468,6 @@ impl Generator for IteratorStatement { match stmt.step().ok()? { limbo_core::StepResult::Row => { let row = stmt.row().unwrap(); - let mut js_row = self.env.create_object().ok()?; match self.presentation_mode { PresentationMode::Raw => { @@ -485,23 +477,21 @@ impl Generator for IteratorStatement { raw_array.set(idx as u32, js_value).ok()?; } - raw_array.coerce_to_object().ok() + Some(raw_array.coerce_to_object().ok()?.into_unknown()) } PresentationMode::Pluck => { - let (idx, value) = row.get_values().enumerate().next()?; - 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) + let (_, value) = row.get_values().enumerate().next()?; + to_js_value(&self.env, value).ok() } PresentationMode::None => { + 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) + Some(js_row.into_unknown()) } } } From a565b6b008b62b9c1e193167fa5db44a404ab257 Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Wed, 4 Jun 2025 10:36:07 -0300 Subject: [PATCH 4/6] bind/js: Adds README I'm assuming this will be the README on npmjs.com --- bindings/javascript/README.md | 36 +++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 bindings/javascript/README.md diff --git a/bindings/javascript/README.md b/bindings/javascript/README.md new file mode 100644 index 000000000..652723e14 --- /dev/null +++ b/bindings/javascript/README.md @@ -0,0 +1,36 @@ +# Limbo + +Limbo is a _work-in-progress_, in-process OLTP database engine library written in Rust that has: + +* **Asynchronous I/O** support on Linux with `io_uring` +* **SQLite compatibility** [[doc](COMPAT.md)] for SQL dialect, file formats, and the C API +* **Language bindings** for JavaScript/WebAssembly, Rust, Go, Python, and [Java](bindings/java) +* **OS support** for Linux, macOS, and Windows + +In the future, we will be also working on: + +* **Integrated vector search** for embeddings and vector similarity. +* **`BEGIN CONCURRENT`** for improved write throughput. +* **Improved schema management** including better `ALTER` support and strict column types by default. + +## Install + +```sh +npm i @tursodatabase/limbo +``` + +## Usage + +```js +import { Database } from 'limbo-wasm'; + +const db = new Database('sqlite.db'); +const stmt = db.prepare('SELECT * FROM users'); +const users = stmt.all(); +console.log(users); +``` + +## Documentation + +- [API Docs](https://github.com/tursodatabase/limbo/blob/main/bindings/javascript/docs/API.md) + From 90e75ee70d897d55f8ef0fb863ddf80e7b9cff85 Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Wed, 4 Jun 2025 10:37:03 -0300 Subject: [PATCH 5/6] bind/js: Updates API docs --- bindings/javascript/docs/{api.md => API.md} | 66 ++++++++++++++------- 1 file changed, 46 insertions(+), 20 deletions(-) rename bindings/javascript/docs/{api.md => API.md} (68%) diff --git a/bindings/javascript/docs/api.md b/bindings/javascript/docs/API.md similarity index 68% rename from bindings/javascript/docs/api.md rename to bindings/javascript/docs/API.md index a89235bda..b3aa865d9 100644 --- a/bindings/javascript/docs/api.md +++ b/bindings/javascript/docs/API.md @@ -28,8 +28,6 @@ Prepares a SQL statement for execution. The function returns a `Statement` object. -This function is currently not supported. - ### transaction(function) ⇒ function Returns a function that runs the given function in a transaction. @@ -38,11 +36,21 @@ Returns a function that runs the given function in a transaction. | -------- | --------------------- | ------------------------------------- | | function | function | The function to run in a transaction. | -This function is currently not supported. - ### pragma(string, [options]) ⇒ results -This function is currently not supported. +Executes the given PRAGMA and returns its results. + +| Param | Type | Description | +| -------- | --------------------- | ----------------------| +| source | string | Pragma to be executed | +| options | object | Options. | + +Most PRAGMA return a single value, the `simple: boolean` option is provided to return the first column of the first row. + +```js +db.pragma('cache_size = 32000'); +console.log(db.pragma('cache_size', { simple: true })); // => 32000 +``` ### backup(destination, [options]) ⇒ promise @@ -68,7 +76,9 @@ This function is currently not supported. Loads a SQLite3 extension -This function is currently not supported. +| Param | Type | Description | +| ------ | ------------------- | -----------------------------------------| +| path | string | The path to the extention to be loaded. | ### exec(sql) ⇒ this @@ -78,7 +88,7 @@ Executes a SQL statement. | ------ | ------------------- | ------------------------------------ | | sql | string | The SQL statement string to execute. | -This function is currently not supported. +This can execute strings that contain multiple SQL statements. ### interrupt() ⇒ this @@ -92,15 +102,15 @@ This function is currently not supported. Closes the database connection. -This function is currently not supported. - # class Statement ## Methods ### run([...bindParameters]) ⇒ object -Executes the SQL statement and returns an info object. +Executes the SQL statement and (currently) returns an array with results. + +**Note:** It should return an info object. | Param | Type | Description | | -------------- | ----------------------------- | ------------------------------------------------ | @@ -118,8 +128,6 @@ Executes the SQL statement and returns the first row. | -------------- | ----------------------------- | ------------------------------------------------ | | bindParameters | array of objects | The bind parameters for executing the statement. | -This function is currently not supported. - ### all([...bindParameters]) ⇒ array of rows Executes the SQL statement and returns an array of the resulting rows. @@ -128,8 +136,6 @@ Executes the SQL statement and returns an array of the resulting rows. | -------------- | ----------------------------- | ------------------------------------------------ | | bindParameters | array of objects | The bind parameters for executing the statement. | -This function is currently not supported. - ### iterate([...bindParameters]) ⇒ iterator Executes the SQL statement and returns an iterator to the resulting rows. @@ -138,11 +144,21 @@ Executes the SQL statement and returns an iterator to the resulting rows. | -------------- | ----------------------------- | ------------------------------------------------ | | bindParameters | array of objects | The bind parameters for executing the statement. | -This function is currently not supported. - ### pluck([toggleState]) ⇒ this -This function is currently not supported. +Makes the prepared statement only return the value of the first column of any rows that it retrieves. + +| Param | Type | Description | +| -------------- | ----------------------------- | ------------------------------------------------ | +| pluckMode | boolean | Enable of disable pluck mode. If you don't pass the paramenter, pluck mode is enabled. | + +```js +stmt.pluck(); // plucking ON +stmt.pluck(true); // plucking ON +stmt.pluck(false); // plucking OFF +``` + +> NOTE: When plucking is turned on, raw mode is turned off (they are mutually exclusive options). ### expand([toggleState]) ⇒ this @@ -150,7 +166,7 @@ This function is currently not supported. ### raw([rawMode]) ⇒ this -Toggle raw mode. +Makes the prepared statement return rows as arrays instead of objects. | Param | Type | Description | | ------- | -------------------- | --------------------------------------------------------------------------------- | @@ -158,7 +174,13 @@ Toggle raw mode. This function enables or disables raw mode. Prepared statements return objects by default, but if raw mode is enabled, the functions return arrays instead. -This function is currently not supported. +```js +stmt.raw(); // raw mode ON +stmt.raw(true); // raw mode ON +stmt.raw(false); // raw mode OFF +``` + +> NOTE: When raw mode is turned on, plucking is turned off (they are mutually exclusive options). ### columns() ⇒ array of objects @@ -168,4 +190,8 @@ This function is currently not supported. ### bind([...bindParameters]) ⇒ this -This function is currently not supported. +| Param | Type | Description | +| -------------- | ----------------------------- | ------------------------------------------------ | +| bindParameters | array of objects | The bind parameters for executing the statement. | + +Binds **permanently** the given parameters to the statement. After a statement's parameters are bound this way, you may no longer provide it with execution-specific (temporary) bound parameters. \ No newline at end of file From f2f2432d3907e897080e694b7d05f37db27f8e69 Mon Sep 17 00:00:00 2001 From: Diego Reis Date: Wed, 4 Jun 2025 10:37:25 -0300 Subject: [PATCH 6/6] bind/js: Adds contribution guide --- bindings/javascript/docs/CONTRIBUTING.md | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 bindings/javascript/docs/CONTRIBUTING.md diff --git a/bindings/javascript/docs/CONTRIBUTING.md b/bindings/javascript/docs/CONTRIBUTING.md new file mode 100644 index 000000000..27b071dbb --- /dev/null +++ b/bindings/javascript/docs/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing + +So you want to contribute to Limbo's binding for the ~second~ best language in the world? Awesome. + +First things first you'll need to install [napi-rs](https://napi.rs/), follow the instructions [here](https://napi.rs/docs/introduction/getting-started) althought is highly recommended to use `yarn` with: + +```sh +yarn global add @napi-rs/cli +``` + +Run `yarn build` to build our napi project and run `yarn test` to run our test suite, if nothing breaks you're ready to start! + +## API + +You can check the API docs [here](./API.md), it aims to be fully compatible with [better-sqlite](https://github.com/WiseLibs/better-sqlite3/) and borrows some things from [libsql](https://github.com/tursodatabase/libsql-js). So if you find some incompability in behaviour and/or lack of functions/attributes, that's an issue and you should work on it for a great good :) + +## Code Structure + +The Rust code for the bind is on [lib.rs](../src/lib.rs). It's exposed to JS users through [wrapper](../wrapper.js), where you can +use some JS' ~weirdness~ facilities, for instance, since Rust doesn't have variadic functions the wrapper enables us to "normalize" `bindParameters` into an array. + +All tests should be within the [__test__](../__test__/) folder. + +# Before open a PR + +Please be assured that: + +- Your fix/feature has a test checking the new behaviour; +- Your code follows Rust's conventions with `cargo fmt`; +- If applicable, update the [API docs](./API.md) to match the current implementation; +