Merge 'bind/js: Add support for bind() method and reduce boilerplate' from Diego Reis

EDIT: This PR also adds support for the `pluck()` logic in all methods

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #1582
This commit is contained in:
Jussi Saurio
2025-05-27 11:01:27 +03:00
4 changed files with 122 additions and 44 deletions

View File

@@ -58,6 +58,25 @@ test("Test pragma", async (t) => {
t.deepEqual(typeof db.pragma("cache_size", { simple: true }), "number");
});
test("Test 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");
for (const row of stmt.iterate()) {
t.truthy(row.name);
t.true(typeof row.age === "number");
}
t.throws(
() => {
db.bind("Bob");
},
{ instanceOf: Error },
);
});
const connect = async (path) => {
const db = new Database(path);
return [db];

View File

@@ -1,5 +1,4 @@
import test from "ava";
import fs from "fs";
import { Database } from "../wrapper.js";
@@ -66,12 +65,44 @@ test("Empty prepared statement should throw", async (t) => {
);
});
test("Test pragma", async (t) => {
test("Test pragma()", async (t) => {
const [db] = await connect(":memory:");
t.true(typeof db.pragma("cache_size")[0].cache_size === "number");
t.true(typeof db.pragma("cache_size", { simple: true }) === "number");
});
test("Statement binded with bind() shouldn't be binded again", 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");
for (const row of stmt.iterate()) {
t.truthy(row.name);
t.true(typeof row.age === "number");
}
t.throws(
() => {
db.bind("Bob");
},
{ instanceOf: Error },
);
});
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.name);
t.true(typeof row.age === "undefined");
}
});
const connect = async (path) => {
const db = new Database(path);
return [db];

View File

@@ -170,8 +170,8 @@ impl Database {
}
}
// TODO: Add the (parent) 'database' property
#[napi]
#[derive(Clone)]
pub struct Statement {
// TODO: implement each property when core supports it
// #[napi(able = false)]
@@ -183,8 +183,9 @@ pub struct Statement {
#[napi(writable = false)]
pub source: String,
toggle: bool,
database: Database,
pluck: bool,
binded: bool,
inner: Rc<RefCell<limbo_core::Statement>>,
}
@@ -195,21 +196,14 @@ impl Statement {
inner: Rc::new(inner),
database,
source,
toggle: false,
pluck: false,
binded: false,
}
}
#[napi]
pub fn get(&self, env: Env, args: Option<Vec<JsUnknown>>) -> napi::Result<JsUnknown> {
let mut stmt = self.inner.borrow_mut();
stmt.reset();
if let Some(args) = args {
for (i, elem) in args.into_iter().enumerate() {
let value = from_js_value(elem)?;
stmt.bind_at(NonZeroUsize::new(i + 1).unwrap(), value);
}
}
let mut stmt = self.check_and_bind(args)?;
let step = stmt.step().map_err(into_napi_error)?;
match step {
@@ -220,6 +214,10 @@ impl Statement {
let key = stmt.get_column_name(idx);
let js_value = to_js_value(&env, value);
obj.set_named_property(&key, js_value)?;
if self.pluck {
return Ok(obj.into_unknown());
}
}
Ok(obj.into_unknown())
}
@@ -234,14 +232,7 @@ impl Statement {
// TODO: Return Info object (https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#runbindparameters---object)
#[napi]
pub fn run(&self, env: Env, args: Option<Vec<JsUnknown>>) -> napi::Result<JsUnknown> {
let mut stmt = self.inner.borrow_mut();
stmt.reset();
if let Some(args) = args {
for (i, elem) in args.into_iter().enumerate() {
let value = from_js_value(elem)?;
stmt.bind_at(NonZeroUsize::new(i + 1).unwrap(), value);
}
}
let stmt = self.check_and_bind(args)?;
self.internal_all(env, stmt)
}
@@ -252,32 +243,19 @@ impl Statement {
env: Env,
args: Option<Vec<JsUnknown>>,
) -> napi::Result<IteratorStatement> {
let mut stmt = self.inner.borrow_mut();
stmt.reset();
if let Some(args) = args {
for (i, elem) in args.into_iter().enumerate() {
let value = from_js_value(elem)?;
stmt.bind_at(NonZeroUsize::new(i + 1).unwrap(), value);
}
}
self.check_and_bind(args)?;
Ok(IteratorStatement {
stmt: Rc::clone(&self.inner),
database: self.database.clone(),
env,
plucked: self.pluck,
})
}
#[napi]
pub fn all(&self, env: Env, args: Option<Vec<JsUnknown>>) -> napi::Result<JsUnknown> {
let mut stmt = self.inner.borrow_mut();
stmt.reset();
if let Some(args) = args {
for (i, elem) in args.into_iter().enumerate() {
let value = from_js_value(elem)?;
stmt.bind_at(NonZeroUsize::new(i + 1).unwrap(), value);
}
}
let stmt = self.check_and_bind(args)?;
self.internal_all(env, stmt)
}
@@ -298,6 +276,10 @@ impl Statement {
let key = stmt.get_column_name(idx);
let js_value = to_js_value(&env, value);
obj.set_named_property(&key, js_value)?;
if self.pluck {
break;
}
}
results.set_element(index, obj)?;
index += 1;
@@ -321,29 +303,60 @@ impl Statement {
}
#[napi]
pub fn pluck(&mut self, toggle: Option<bool>) {
if let Some(false) = toggle {
self.toggle = false;
pub fn pluck(&mut self, pluck: Option<bool>) {
if let Some(false) = pluck {
self.pluck = false;
}
self.toggle = true;
self.pluck = true;
}
#[napi]
pub fn expand() {
todo!()
}
#[napi]
pub fn raw() {
todo!()
}
#[napi]
pub fn columns() {
todo!()
}
#[napi]
pub fn bind() {
todo!()
pub fn bind(&mut self, args: Option<Vec<JsUnknown>>) -> napi::Result<Self> {
self.check_and_bind(args)?;
self.binded = true;
Ok(self.clone())
}
/// Check if the Statement is already binded by the `bind()` method
/// and bind values do variables. The expected type for args is `Option<Vec<JsUnknown>>`
fn check_and_bind(
&self,
args: Option<Vec<JsUnknown>>,
) -> napi::Result<RefMut<'_, limbo_core::Statement>> {
let mut stmt = self.inner.borrow_mut();
stmt.reset();
if let Some(args) = args {
if self.binded {
return Err(napi::Error::new(
napi::Status::InvalidArg,
"This statement already has bound parameters",
));
}
for (i, elem) in args.into_iter().enumerate() {
let value = from_js_value(elem)?;
stmt.bind_at(NonZeroUsize::new(i + 1).unwrap(), value);
}
}
Ok(stmt)
}
}
@@ -352,6 +365,7 @@ pub struct IteratorStatement {
stmt: Rc<RefCell<limbo_core::Statement>>,
database: Database,
env: Env,
plucked: bool,
}
impl Generator for IteratorStatement {
@@ -373,6 +387,10 @@ impl Generator for IteratorStatement {
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()?;
if self.plucked {
break;
}
}
Some(js_row)

View File

@@ -224,6 +224,16 @@ class Statement {
columns() {
return this.stmt.columns();
}
/**
* Binds the given parameters to the statement _permanently_
*
* @param bindParameters - The bind parameters for binding the statement.
* @returns this - Statement with binded parameters
*/
bind(...bindParameters) {
return this.stmt.bind(bindParameters.flat());
}
}
module.exports.Database = Database;