Merge 'bindings/javascript: Add Statement.iterate() method' from Diego Reis

I still didn't find a good way to implement variadic functions, we
should have some sort of wrapper in JS layer but it didn't work so well
for me so far. But once done it will be easily transferable to any
function.
It also should probably be async, but AFAIC napi doesn't have a straight
way to implement async iterators.

Closes #1515
This commit is contained in:
Pekka Enberg
2025-05-19 20:44:40 +03:00
6 changed files with 252 additions and 164 deletions

4
Cargo.lock generated
View File

@@ -2205,9 +2205,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"

View File

@@ -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"

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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;

View File

@@ -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<limbo_core::Statement>,
inner: Rc<RefCell<limbo_core::Statement>>,
}
#[napi]
impl Statement {
pub fn new(inner: RefCell<limbo_core::Statement>, 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<RefCell<limbo_core::Statement>>,
database: Database,
env: Env,
}
impl Generator for IteratorStatement {
type Yield = JsObject;
type Next = ();
type Return = ();
fn next(&mut self, _: Option<Self::Next>) -> Option<Self::Yield> {
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<JsUnknown> {
match value {
limbo_core::Value::Null => Ok(env.get_null()?.into_unknown()),