Skip to content

Commit

Permalink
fix(ext/node): implement StatementSync#iterate (#28168)
Browse files Browse the repository at this point in the history
Fixes #28130
  • Loading branch information
littledivy authored Feb 18, 2025
1 parent 2f45275 commit 4ab380e
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 0 deletions.
1 change: 1 addition & 0 deletions ext/node/ops/sqlite/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ impl DatabaseSync {
inner: raw_stmt,
db: self.conn.clone(),
use_big_ints: Cell::new(false),
is_iter_finished: false,
})
}

Expand Down
139 changes: 139 additions & 0 deletions ext/node/ops/sqlite/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ pub struct RunStatementResult {
changes: u64,
}

#[derive(Debug)]
pub struct StatementSync {
pub inner: *mut ffi::sqlite3_stmt,
pub db: Rc<RefCell<Option<rusqlite::Connection>>>,

pub use_big_ints: Cell<bool>,
pub is_iter_finished: bool,
}

impl Drop for StatementSync {
Expand Down Expand Up @@ -369,6 +371,143 @@ impl StatementSync {
Ok(arr)
}

fn iterate<'a>(
&self,
scope: &mut v8::HandleScope<'a>,
#[varargs] params: Option<&v8::FunctionCallbackArguments>,
) -> Result<v8::Local<'a, v8::Object>, SqliteError> {
macro_rules! v8_static_strings {
($($ident:ident = $str:literal),* $(,)?) => {
$(
pub static $ident: deno_core::FastStaticString = deno_core::ascii_str!($str);
)*
};
}

v8_static_strings! {
ITERATOR = "Iterator",
PROTOTYPE = "prototype",
NEXT = "next",
RETURN = "return",
DONE = "done",
VALUE = "value",
}

self.reset();

self.bind_params(scope, params)?;

let iterate_next = |scope: &mut v8::HandleScope,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue| {
let context = v8::Local::<v8::External>::try_from(args.data())
.expect("Iterator#next expected external data");
// SAFETY: `context` is a valid pointer to a StatementSync instance
let statement = unsafe { &mut *(context.value() as *mut StatementSync) };

let names = &[
DONE.v8_string(scope).unwrap().into(),
VALUE.v8_string(scope).unwrap().into(),
];

if statement.is_iter_finished {
let values = &[
v8::Boolean::new(scope, true).into(),
v8::undefined(scope).into(),
];
let null = v8::null(scope).into();
let result =
v8::Object::with_prototype_and_properties(scope, null, names, values);
rv.set(result.into());
return;
}

let Ok(Some(row)) = statement.read_row(scope) else {
statement.reset();
statement.is_iter_finished = true;

let values = &[
v8::Boolean::new(scope, true).into(),
v8::undefined(scope).into(),
];
let null = v8::null(scope).into();
let result =
v8::Object::with_prototype_and_properties(scope, null, names, values);
rv.set(result.into());
return;
};

let values = &[v8::Boolean::new(scope, false).into(), row.into()];
let null = v8::null(scope).into();
let result =
v8::Object::with_prototype_and_properties(scope, null, names, values);
rv.set(result.into());
};

let iterate_return = |scope: &mut v8::HandleScope,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue| {
let context = v8::Local::<v8::External>::try_from(args.data())
.expect("Iterator#return expected external data");
// SAFETY: `context` is a valid pointer to a StatementSync instance
let statement = unsafe { &mut *(context.value() as *mut StatementSync) };

statement.is_iter_finished = true;
statement.reset();

let names = &[
DONE.v8_string(scope).unwrap().into(),
VALUE.v8_string(scope).unwrap().into(),
];
let values = &[
v8::Boolean::new(scope, true).into(),
v8::undefined(scope).into(),
];

let null = v8::null(scope).into();
let result =
v8::Object::with_prototype_and_properties(scope, null, names, values);
rv.set(result.into());
};

let external = v8::External::new(scope, self as *const _ as _);
let next_func = v8::Function::builder(iterate_next)
.data(external.into())
.build(scope)
.expect("Failed to create Iterator#next function");
let return_func = v8::Function::builder(iterate_return)
.data(external.into())
.build(scope)
.expect("Failed to create Iterator#return function");

let global = scope.get_current_context().global(scope);
let iter_str = ITERATOR.v8_string(scope).unwrap();
let js_iterator: v8::Local<v8::Object> = {
global
.get(scope, iter_str.into())
.unwrap()
.try_into()
.unwrap()
};

let proto_str = PROTOTYPE.v8_string(scope).unwrap();
let js_iterator_proto = js_iterator.get(scope, proto_str.into()).unwrap();

let names = &[
NEXT.v8_string(scope).unwrap().into(),
RETURN.v8_string(scope).unwrap().into(),
];
let values = &[next_func.into(), return_func.into()];
let iterator = v8::Object::with_prototype_and_properties(
scope,
js_iterator_proto,
names,
values,
);

Ok(iterator)
}

#[fast]
fn set_read_big_ints(&self, enabled: bool) {
self.use_big_ints.set(enabled);
Expand Down
20 changes: 20 additions & 0 deletions tests/unit_node/sqlite_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,23 @@ CREATE TABLE two(id int PRIMARY KEY) STRICT;`);

db.close();
});

Deno.test("[node/sqlite] StatementSync#iterate", () => {
const db = new DatabaseSync(":memory:");
const stmt = db.prepare("SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3");
// @ts-ignore: types are not up to date
const iter = stmt.iterate();

const result = [];
for (const row of iter) {
result.push(row);
}

assertEquals(result, stmt.all());

const { done, value } = iter.next();
assertEquals(done, true);
assertEquals(value, undefined);

db.close();
});

0 comments on commit 4ab380e

Please sign in to comment.