Merge 'Make iterate() lazily evaluated on wasm' from Diego Reis

#514
Introduces a new feature for lazy evaluation in the
`Statement.raw().iterate()` method and includes related changes in both
the test and implementation files. The most important changes include
adding a test case for lazy evaluation, creating a `RowIterator` struct,
and modifying the `iterate` method to use this new struct.
Everything seems to works fine, but suggestions on code improvement and
test use cases are welcoming.

Closes #527
This commit is contained in:
Pekka Enberg
2025-01-05 20:23:06 +02:00
5 changed files with 1164 additions and 437 deletions

3
.gitignore vendored
View File

@@ -26,3 +26,6 @@ dist/
# OS
.DS_Store
# Javascript
**/node_modules/

File diff suppressed because it is too large Load Diff

View File

@@ -3,13 +3,13 @@
"type": "module",
"private": true,
"scripts": {
"test": "PROVIDER=better-sqlite3 ava tests/test.js && PROVIDER=limbo-wasm ava tests/test.js"
"test": "PROVIDER=better-sqlite3 ava tests/test.js && PROVIDER=limbo-wasm ava tests/test.js && rm *.db *.db-wal"
},
"devDependencies": {
"ava": "^5.3.0"
"ava": "^6.2.0"
},
"dependencies": {
"better-sqlite3": "^8.4.0",
"better-sqlite3": "^11.7.0",
"limbo-wasm": "../pkg"
}
}

View File

@@ -7,17 +7,17 @@ test.beforeEach(async (t) => {
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)
`);
db.exec(
"INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"
"INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"
);
db.exec(
"INSERT INTO users (id, name, email) VALUES (2, 'Bob', 'bob@example.com')"
"INSERT INTO users (id, name, email) VALUES (2, 'Bob', 'bob@example.com')"
);
t.context = {
db,
errorType,
provider
db,
errorType,
provider
};
});
});
test.serial("Statement.raw().all()", async (t) => {
const db = t.context.db;
@@ -31,54 +31,54 @@ test.serial("Statement.raw().all()", async (t) => {
});
test.serial("Statement.raw().get()", async (t) => {
const db = t.context.db;
const db = t.context.db;
const stmt = db.prepare("SELECT * FROM users");
const expected = [
1, "Alice", "alice@example.org"
];
t.deepEqual(stmt.raw().get(), expected);
const stmt = db.prepare("SELECT * FROM users");
const expected = [
1, "Alice", "alice@example.org"
];
t.deepEqual(stmt.raw().get(), expected);
const emptyStmt = db.prepare("SELECT * FROM users WHERE id = -1");
t.is(emptyStmt.raw().get(), undefined);
const emptyStmt = db.prepare("SELECT * FROM users WHERE id = -1");
t.is(emptyStmt.raw().get(), undefined);
});
test.serial("Statement.raw().iterate()", async (t) => {
const db = t.context.db;
const db = t.context.db;
const stmt = db.prepare("SELECT * FROM users");
const expected = [
{ done: false, value: [1, "Alice", "alice@example.org"] },
{ done: false, value: [2, "Bob", "bob@example.com"] },
{ done: true, value: undefined },
];
const stmt = db.prepare("SELECT * FROM users");
const expected = [
{ done: false, value: [1, "Alice", "alice@example.org"] },
{ done: false, value: [2, "Bob", "bob@example.com"] },
{ done: true, value: undefined },
];
let iter = stmt.raw().iterate();
t.is(typeof iter[Symbol.iterator], 'function');
t.deepEqual(iter.next(), expected[0])
t.deepEqual(iter.next(), expected[1])
t.deepEqual(iter.next(), expected[2])
let iter = stmt.raw().iterate();
t.is(typeof iter[Symbol.iterator], 'function');
t.deepEqual(iter.next(), expected[0])
t.deepEqual(iter.next(), expected[1])
t.deepEqual(iter.next(), expected[2])
const emptyStmt = db.prepare("SELECT * FROM users WHERE id = -1");
t.is(typeof emptyStmt[Symbol.iterator], 'undefined');
t.throws(() => emptyStmt.next(), { instanceOf: TypeError });
const emptyStmt = db.prepare("SELECT * FROM users WHERE id = -1");
t.is(typeof emptyStmt[Symbol.iterator], 'undefined');
t.throws(() => emptyStmt.next(), { instanceOf: TypeError });
});
const connect = async (path_opt) => {
const path = path_opt ?? "hello.db";
const provider = process.env.PROVIDER;
if (provider === "limbo-wasm") {
const database = process.env.LIBSQL_DATABASE ?? path;
const x = await import("limbo-wasm");
const options = {};
const db = new x.Database(database, options);
return [db, x.SqliteError, provider];
const database = process.env.LIBSQL_DATABASE ?? path;
const x = await import("limbo-wasm");
const options = {};
const db = new x.Database(database, options);
return [db, x.SqliteError, provider];
}
if (provider == "better-sqlite3") {
const x = await import("better-sqlite3");
const options = {};
const db = x.default(path, options);
return [db, x.SqliteError, provider];
const x = await import("better-sqlite3");
const options = {};
const db = x.default(path, options);
return [db, x.SqliteError, provider];
}
throw new Error("Unknown provider: " + provider);
};
};

View File

@@ -1,3 +1,4 @@
use js_sys::{Array, Object};
use limbo_core::{
maybe_init_database_file, BufferPool, OpenFlags, Pager, Result, WalFile, WalFileShared,
};
@@ -55,6 +56,38 @@ impl Database {
}
}
#[wasm_bindgen]
pub struct RowIterator {
inner: RefCell<limbo_core::Statement>,
}
#[wasm_bindgen]
impl RowIterator {
fn new(inner: RefCell<limbo_core::Statement>) -> Self {
Self { inner }
}
#[wasm_bindgen]
pub fn next(&mut self) -> JsValue {
match self.inner.borrow_mut().step() {
Ok(limbo_core::RowResult::Row(row)) => {
let row_array = Array::new();
for value in row.values {
let value = to_js_value(value);
row_array.push(&value);
}
JsValue::from(row_array)
}
Ok(limbo_core::RowResult::IO) => JsValue::UNDEFINED,
Ok(limbo_core::RowResult::Done) | Ok(limbo_core::RowResult::Interrupt) => {
JsValue::UNDEFINED
}
Ok(limbo_core::RowResult::Busy) => JsValue::UNDEFINED,
Err(e) => panic!("Error: {:?}", e),
}
}
}
#[wasm_bindgen]
pub struct Statement {
inner: RefCell<limbo_core::Statement>,
@@ -113,16 +146,35 @@ impl Statement {
array
}
pub fn iterate(&self) -> JsValue {
let all = self.all();
let iterator_fn = js_sys::Reflect::get(&all, &js_sys::Symbol::iterator())
.expect("Failed to get iterator function")
.dyn_into::<js_sys::Function>()
.expect("Symbol.iterator is not a function");
#[wasm_bindgen]
pub fn iterate(self) -> JsValue {
let iterator = RowIterator::new(self.inner);
let iterator_obj = Object::new();
iterator_fn
.call0(&all)
.expect("Failed to call iterator function")
// Define the next method that will be called by JavaScript
let next_fn = js_sys::Function::new_with_args(
"",
"const value = this.iterator.next();
const done = value === undefined;
return {
value,
done
};",
);
js_sys::Reflect::set(&iterator_obj, &JsValue::from_str("next"), &next_fn).unwrap();
js_sys::Reflect::set(
&iterator_obj,
&JsValue::from_str("iterator"),
&JsValue::from(iterator),
)
.unwrap();
let symbol_iterator = js_sys::Function::new_no_args("return this;");
js_sys::Reflect::set(&iterator_obj, &js_sys::Symbol::iterator(), &symbol_iterator).unwrap();
JsValue::from(iterator_obj)
}
}