Files
Nathan Whitaker 6a9d607c22 fix(ext/node): handle sqlite conversion failures (#36024)
This makes `node:sqlite` treat JavaScript conversion failures as normal,
catchable errors.

V8 property and index access can execute JavaScript and fail, while C
string conversion rejects embedded null bytes. The affected statement
binding, tag store, function and aggregate option, iterator setup, and
session option paths now propagate those failures instead of assuming
conversion always succeeds. Result and error properties are also defined
directly to avoid invoking inherited setters.

After a failed conversion, the database remains usable by subsequent
operations.
2026-07-14 14:18:47 +02:00

1192 lines
37 KiB
Rust

// Copyright 2018-2026 the Deno authors. MIT license.
use std::cell::Cell;
use std::cell::RefCell;
use std::rc::Rc;
use deno_core::GarbageCollected;
use deno_core::ToV8;
use deno_core::op2;
use deno_core::v8;
use deno_core::v8::GetPropertyNamesArgs;
use deno_core::v8_static_strings;
use rusqlite::ffi;
use super::SqliteError;
use super::validators;
// ECMA-262, 15th edition, 21.1.2.6. Number.MAX_SAFE_INTEGER (2^53-1)
const MAX_SAFE_JS_INTEGER: i64 = 9007199254740991;
pub struct RunStatementResult {
last_insert_rowid: i64,
changes: u64,
use_big_ints: bool,
}
impl<'a> ToV8<'a> for RunStatementResult {
type Error = SqliteError;
fn to_v8(
self,
scope: &mut v8::PinScope<'a, '_>,
) -> Result<v8::Local<'a, v8::Value>, SqliteError> {
v8_static_strings! {
LAST_INSERT_ROW_ID = "lastInsertRowid",
CHANGES = "changes",
}
let obj = v8::Object::new(scope);
let last_insert_row_id_str = LAST_INSERT_ROW_ID.v8_string(scope).unwrap();
let last_insert_row_id = if self.use_big_ints {
v8::BigInt::new_from_i64(scope, self.last_insert_rowid).into()
} else {
v8::Number::new(scope, self.last_insert_rowid as f64).into()
};
// create_data_property defines own data properties directly, avoiding the
// generic [[Set]] runtime path (property-key lookup + hidden-class
// transitions) that showed up as ~half the cost of a `run()` in profiles.
obj
.create_data_property(
scope,
last_insert_row_id_str.into(),
last_insert_row_id,
)
.unwrap();
let changes_str = CHANGES.v8_string(scope).unwrap();
let changes = if self.use_big_ints {
v8::BigInt::new_from_u64(scope, self.changes).into()
} else {
v8::Number::new(scope, self.changes as f64).into()
};
obj
.create_data_property(scope, changes_str.into(), changes)
.unwrap();
Ok(obj.into())
}
}
pub type InnerStatementPtr = Rc<Cell<Option<*mut ffi::sqlite3_stmt>>>;
pub trait StatementExecution {
fn stmt_ptr(&self) -> Result<*mut ffi::sqlite3_stmt, SqliteError>;
fn step(&self) -> Result<bool, SqliteError>;
fn return_arrays(&self) -> bool;
fn use_big_ints(&self) -> bool;
fn check_bind_result(&self, r: i32) -> Result<(), SqliteError>;
fn column_count(&self) -> Result<i32, SqliteError> {
let raw = self.stmt_ptr()?;
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
// as it lives as long as the StatementSync instance.
let count = unsafe { ffi::sqlite3_column_count(raw) };
Ok(count)
}
fn column_name(&self, index: i32) -> Result<&[u8], SqliteError> {
let raw = self.stmt_ptr()?;
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
// as it lives as long as the StatementSync instance.
unsafe {
let name = ffi::sqlite3_column_name(raw, index);
Ok(std::ffi::CStr::from_ptr(name as _).to_bytes())
}
}
fn column_value<'a>(
&self,
index: i32,
scope: &mut v8::PinScope<'a, '_>,
) -> Result<v8::Local<'a, v8::Value>, SqliteError> {
let raw = self.stmt_ptr()?;
let use_big_ints = self.use_big_ints();
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
// as it lives as long as the statement instance.
unsafe {
Ok(match ffi::sqlite3_column_type(raw, index) {
ffi::SQLITE_INTEGER => {
let value = ffi::sqlite3_column_int64(raw, index);
if use_big_ints {
v8::BigInt::new_from_i64(scope, value).into()
} else if value.abs() <= MAX_SAFE_JS_INTEGER {
v8::Number::new(scope, value as f64).into()
} else {
return Err(SqliteError::NumberTooLarge(value));
}
}
ffi::SQLITE_FLOAT => {
let value = ffi::sqlite3_column_double(raw, index);
v8::Number::new(scope, value).into()
}
ffi::SQLITE_TEXT => {
let value = ffi::sqlite3_column_text(raw, index);
let value = std::ffi::CStr::from_ptr(value as _);
v8::String::new_from_utf8(
scope,
value.to_bytes(),
v8::NewStringType::Normal,
)
.unwrap()
.into()
}
ffi::SQLITE_BLOB => {
let value = ffi::sqlite3_column_blob(raw, index);
let size = ffi::sqlite3_column_bytes(raw, index);
let ab = if size == 0 {
v8::ArrayBuffer::new(scope, 0)
} else {
let value =
std::slice::from_raw_parts(value as *const u8, size as usize);
let bs =
v8::ArrayBuffer::new_backing_store_from_vec(value.to_vec())
.make_shared();
v8::ArrayBuffer::with_backing_store(scope, &bs)
};
v8::Uint8Array::new(scope, ab, 0, size as _).unwrap().into()
}
ffi::SQLITE_NULL => v8::null(scope).into(),
_ => v8::undefined(scope).into(),
})
}
}
fn bind_value(
&self,
scope: &mut v8::PinScope<'_, '_>,
value: v8::Local<v8::Value>,
index: i32,
) -> Result<(), SqliteError> {
let raw = self.stmt_ptr()?;
let r = if value.is_number() {
let value = value.number_value(scope).unwrap();
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
// as it lives as long as the statement instance.
unsafe { ffi::sqlite3_bind_double(raw, index, value) }
} else if value.is_string() {
let value = value.to_rust_string_lossy(scope);
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
// as it lives as long as the statement instance.
//
// SQLITE_TRANSIENT is used to indicate that SQLite should make a copy of the data.
unsafe {
ffi::sqlite3_bind_text(
raw,
index,
value.as_ptr() as *const _,
value.len() as i32,
ffi::SQLITE_TRANSIENT(),
)
}
} else if value.is_null() {
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
// as it lives as long as the statement instance.
unsafe { ffi::sqlite3_bind_null(raw, index) }
} else if value.is_array_buffer_view() {
let value: v8::Local<v8::ArrayBufferView> = value.try_into().unwrap();
let mut data = value.data();
let mut size = value.byte_length();
// data may be NULL if length is 0 or ab is detached. we need to pass a valid pointer
// to sqlite3_bind_blob, so we use a static empty array in this case.
if data.is_null() {
static EMPTY: [u8; 0] = [];
data = EMPTY.as_ptr() as *mut _;
size = 0;
}
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
// as it lives as long as the statement instance.
//
// SQLITE_TRANSIENT is used to indicate that SQLite should make a copy of the data.
unsafe {
ffi::sqlite3_bind_blob(
raw,
index,
data,
size as i32,
ffi::SQLITE_TRANSIENT(),
)
}
} else if value.is_big_int() {
let value: v8::Local<v8::BigInt> = value.try_into().unwrap();
let (as_int, lossless) = value.i64_value();
if !lossless {
return Err(SqliteError::InvalidBindValue(
"BigInt value is too large to bind",
));
}
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
// as it lives as long as the statement instance.
unsafe { ffi::sqlite3_bind_int64(raw, index, as_int) }
} else {
return Err(SqliteError::InvalidBindType(index));
};
self.check_bind_result(r)
}
fn read_row<'a>(
&self,
scope: &mut v8::PinScope<'a, '_>,
) -> Result<Option<v8::Local<'a, v8::Value>>, SqliteError> {
if self.step()? {
return Ok(None);
}
let num_cols = self.column_count()?;
let mut names = Vec::with_capacity(num_cols as usize);
let mut values = Vec::with_capacity(num_cols as usize);
for i in 0..num_cols {
let name = self.column_name(i)?;
let value = self.column_value(i, scope)?;
let name =
v8::String::new_from_utf8(scope, name, v8::NewStringType::Normal)
.unwrap()
.into();
names.push(name);
values.push(value);
}
if self.return_arrays() {
let result = v8::Array::new_with_elements(scope, &values);
Ok(Some(result.into()))
} else {
let null = v8::null(scope).into();
let result =
v8::Object::with_prototype_and_properties(scope, null, &names, &values);
Ok(Some(result.into()))
}
}
}
#[derive(Debug)]
pub struct StatementSync {
pub inner: InnerStatementPtr,
pub db: Rc<RefCell<Option<rusqlite::Connection>>>,
pub statements: Rc<RefCell<Vec<InnerStatementPtr>>>,
pub ignore_next_sqlite_error: Rc<Cell<bool>>,
pub return_arrays: Cell<bool>,
pub use_big_ints: Cell<bool>,
pub allow_bare_named_params: Cell<bool>,
pub allow_unknown_named_params: Cell<bool>,
pub is_iter_finished: Cell<bool>,
pub iter_generation: Cell<u64>,
/// Tracks all allocated `IteratorContext` pointers so they can be freed
/// when the statement is dropped (handles abandoned iterators that never
/// call `return()`).
pub(crate) iter_contexts: RefCell<Vec<*mut IteratorContext>>,
}
pub(crate) struct IteratorContext {
statement: *const StatementSync,
statement_ref: v8::Global<v8::Value>,
expected_generation: u64,
finished: Cell<bool>,
finalized_functions: Cell<u8>,
next_func: RefCell<Option<v8::Weak<v8::Function>>>,
return_func: RefCell<Option<v8::Weak<v8::Function>>>,
}
impl Drop for StatementSync {
fn drop(&mut self) {
// Free all tracked IteratorContext allocations to prevent memory leaks
// from abandoned iterators that never called `return()`.
for ctx_ptr in self.iter_contexts.borrow().iter() {
// SAFETY: Each pointer was allocated via `Box::into_raw` in `iterate()`
// and has not been freed yet (freed contexts are removed from the vec).
unsafe {
drop(Box::from_raw(*ctx_ptr));
}
}
let mut statements = self.statements.borrow_mut();
let mut finalized_stmt = None;
if let Some(pos) = statements
.iter()
.position(|stmt| Rc::ptr_eq(stmt, &self.inner))
{
let stmt = statements.remove(pos);
finalized_stmt = stmt.get();
stmt.set(None);
}
if let Some(ptr) = finalized_stmt {
// SAFETY: `ptr` is a valid pointer to a sqlite3_stmt.
unsafe {
ffi::sqlite3_finalize(ptr);
}
}
}
}
fn release_iterator_context(ctx_ptr: *mut IteratorContext) {
// SAFETY: `ctx_ptr` was allocated with `Box::into_raw` in `iterate()` and
// remains valid until both callback functions have been finalized.
let ctx = unsafe { &*ctx_ptr };
let finalized_functions = ctx.finalized_functions.get() + 1;
ctx.finalized_functions.set(finalized_functions);
if finalized_functions != 2 {
return;
}
// SAFETY: `ctx.statement_ref` keeps the JS wrapper, and therefore this
// cppgc object, alive until the context is dropped below.
let statement = unsafe { &*ctx.statement };
statement
.iter_contexts
.borrow_mut()
.retain(|ptr| *ptr != ctx_ptr);
// SAFETY: Both callback functions are gone, so no `v8::External` can reach
// this context again.
unsafe {
drop(Box::from_raw(ctx_ptr));
}
}
pub(crate) fn check_error_code(
r: i32,
db: *mut ffi::sqlite3,
) -> Result<(), SqliteError> {
if r != ffi::SQLITE_OK {
// SAFETY: lifetime of the connection is guaranteed by reference
// counting.
let err_message = unsafe { ffi::sqlite3_errmsg(db) };
if !err_message.is_null() {
// SAFETY: `err_msg` is a valid pointer to a null-terminated string.
let err_message = unsafe { std::ffi::CStr::from_ptr(err_message) }
.to_string_lossy()
.into_owned();
// SAFETY: `err_str` is a valid pointer to a null-terminated string.
let err_str = unsafe { std::ffi::CStr::from_ptr(ffi::sqlite3_errstr(r)) }
.to_string_lossy()
.into_owned();
return Err(SqliteError::SqliteSysError {
message: err_message,
errcode: r as _,
errstr: err_str,
});
}
}
Ok(())
}
pub(crate) fn check_error_code2(r: i32) -> Result<(), SqliteError> {
// SAFETY: `ffi::sqlite3_errstr` is a valid function that returns a pointer to a null-terminated
// string.
let err_str = unsafe { std::ffi::CStr::from_ptr(ffi::sqlite3_errstr(r)) }
.to_string_lossy()
.into_owned();
Err(SqliteError::SqliteSysError {
message: err_str.clone(),
errcode: r as _,
errstr: err_str,
})
}
// SAFETY: we're sure this can be GCed
unsafe impl GarbageCollected for StatementSync {
fn trace(&self, _visitor: &mut deno_core::v8::cppgc::Visitor) {}
fn get_name(&self) -> &'static std::ffi::CStr {
c"StatementSync"
}
}
impl StatementExecution for StatementSync {
fn stmt_ptr(&self) -> Result<*mut ffi::sqlite3_stmt, SqliteError> {
let ptr = self.inner.get();
match ptr {
Some(p) => Ok(p),
None => Err(SqliteError::StatementFinalized),
}
}
fn step(&self) -> Result<bool, SqliteError> {
let raw = self.stmt_ptr()?;
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
// as it lives as long as the StatementSync instance.
unsafe {
let r = ffi::sqlite3_step(raw);
if r == ffi::SQLITE_DONE {
return Ok(true);
}
if r != ffi::SQLITE_ROW {
self.check_error_code_impl(r)?;
}
}
Ok(false)
}
fn return_arrays(&self) -> bool {
self.return_arrays.get()
}
fn use_big_ints(&self) -> bool {
self.use_big_ints.get()
}
fn check_bind_result(&self, r: i32) -> Result<(), SqliteError> {
self.check_error_code_impl(r)
}
}
impl StatementSync {
fn assert_statement_finalized(&self) -> Result<(), SqliteError> {
if self.inner.get().is_none() {
return Err(SqliteError::StatementFinalized);
}
Ok(())
}
// Clear the prepared statement back to its initial state.
fn reset(&self) -> Result<(), SqliteError> {
let raw = self.stmt_ptr()?;
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
// as it lives as long as the StatementSync instance.
let r = unsafe { ffi::sqlite3_reset(raw) };
self.check_error_code_impl(r)
}
fn invalidate_iter(&self) {
self
.iter_generation
.set(self.iter_generation.get().wrapping_add(1));
}
fn check_error_code_impl(&self, r: i32) -> Result<(), SqliteError> {
if r != ffi::SQLITE_OK {
if self.ignore_next_sqlite_error.get() {
self.ignore_next_sqlite_error.set(false);
return Ok(());
}
let db = self.db.borrow();
let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
// SAFETY: db.handle() is valid
unsafe {
check_error_code(r, db.handle())?;
}
}
Ok(())
}
// Bind the parameters to the prepared statement.
fn bind_params(
&self,
scope: &mut v8::PinScope<'_, '_>,
params: Option<&v8::FunctionCallbackArguments>,
) -> Result<(), SqliteError> {
let raw = self.stmt_ptr()?;
// Reset the prepared statement to its initial state.
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
unsafe {
let r = ffi::sqlite3_clear_bindings(raw);
self.check_error_code_impl(r)?;
}
let mut anon_start = 0;
if let Some(params) = params {
let param0 = params.get(0);
if param0.is_object() && !param0.is_array_buffer_view() {
let obj = v8::Local::<v8::Object>::try_from(param0).unwrap();
let keys = obj
.get_property_names(scope, GetPropertyNamesArgs::default())
.ok_or(validators::Error::V8Exception)?;
// Allow specifying named parameters without the SQLite prefix character to improve
// ergonomics. This can be disabled with `StatementSync#setAllowBareNamedParams`.
let mut bare_named_params = std::collections::HashMap::new();
if self.allow_bare_named_params.get() {
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt.
let param_count = unsafe { ffi::sqlite3_bind_parameter_count(raw) };
for i in 1..=param_count {
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt.
let full_name = unsafe {
let name = ffi::sqlite3_bind_parameter_name(raw, i);
if name.is_null() {
continue;
}
std::ffi::CStr::from_ptr(name).to_bytes()
};
let bare_name = &full_name[1..];
let e = bare_named_params.insert(bare_name, i);
if let Some(existing_index) = e {
let bare_name_str = std::str::from_utf8(bare_name)?;
let full_name_str = std::str::from_utf8(full_name)?;
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt.
unsafe {
let existing_full_name =
ffi::sqlite3_bind_parameter_name(raw, existing_index);
let existing_full_name_str =
std::ffi::CStr::from_ptr(existing_full_name).to_str()?;
return Err(SqliteError::DuplicateNamedParameter(
bare_name_str.to_string(),
existing_full_name_str.to_string(),
full_name_str.to_string(),
));
}
}
}
}
let len = keys.length();
for j in 0..len {
let key = keys
.get_index(scope, j)
.ok_or(validators::Error::V8Exception)?;
let key_str = key.to_rust_string_lossy(scope);
let key_c = std::ffi::CString::new(key_str.as_bytes())?;
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt.
let mut r = unsafe {
ffi::sqlite3_bind_parameter_index(raw, key_c.as_ptr() as *const _)
};
if r == 0 {
let lookup = bare_named_params.get(key_c.as_bytes());
if let Some(index) = lookup {
r = *index;
}
if r == 0 {
if self.allow_unknown_named_params.get() {
continue;
}
return Err(SqliteError::UnknownNamedParameter(key_str));
}
}
let value =
obj.get(scope, key).ok_or(validators::Error::V8Exception)?;
self.bind_value(scope, value, r)?;
}
anon_start += 1;
}
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt.
let sql_param_count = unsafe { ffi::sqlite3_bind_parameter_count(raw) };
let mut positional_idx = 1;
for i in anon_start..params.length() {
// Find the next positional parameter slot.
// Skip named parameters (:name, $name, @name) but include anonymous (?)
// and numbered (?NNN) parameters.
while positional_idx <= sql_param_count {
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt.
let name_ptr =
unsafe { ffi::sqlite3_bind_parameter_name(raw, positional_idx) };
if name_ptr.is_null()
// SAFETY: short-circuiting guarantees name_ptr is non-null here
|| unsafe { *name_ptr as u8 == b'?' }
{
break;
}
// Named parameter (:name, $name, @name) - skip it
positional_idx += 1;
}
let value = params.get(i);
self.bind_value(scope, value, positional_idx)?;
positional_idx += 1;
}
}
Ok(())
}
}
struct ResetGuard<'a>(&'a StatementSync);
impl Drop for ResetGuard<'_> {
fn drop(&mut self) {
let _ = self.0.reset();
}
}
// Represents a single prepared statement. Cannot be initialized directly via constructor.
// Instances are created using `DatabaseSync#prepare`.
//
// A prepared statement is an efficient binary representation of the SQL used to create it.
#[op2]
impl StatementSync {
#[constructor]
#[cppgc]
fn new(_: bool) -> Result<StatementSync, SqliteError> {
Err(SqliteError::InvalidConstructor)
}
// Executes a prepared statement and returns the first result as an object.
//
// The prepared statement does not return any results, this method returns undefined.
// Optionally, parameters can be bound to the prepared statement.
#[reentrant]
fn get<'a>(
&self,
scope: &mut v8::PinScope<'a, '_>,
#[varargs] params: Option<&v8::FunctionCallbackArguments>,
) -> Result<v8::Local<'a, v8::Value>, SqliteError> {
self.invalidate_iter();
self.reset()?;
self.bind_params(scope, params)?;
let _reset = ResetGuard(self);
let entry = self.read_row(scope)?;
let result = entry.unwrap_or_else(|| v8::undefined(scope).into());
Ok(result)
}
// Executes a prepared statement and returns an object summarizing the resulting
// changes.
//
// Optionally, parameters can be bound to the prepared statement.
#[reentrant]
fn run(
&self,
scope: &mut v8::PinScope<'_, '_>,
#[varargs] params: Option<&v8::FunctionCallbackArguments>,
) -> Result<RunStatementResult, SqliteError> {
self.invalidate_iter();
let db = self.db.borrow();
let db = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
self.bind_params(scope, params)?;
let reset = ResetGuard(self);
self.step()?;
// Reset to return correct change metadata.
drop(reset);
Ok(RunStatementResult {
last_insert_rowid: db.last_insert_rowid(),
changes: db.changes(),
use_big_ints: self.use_big_ints.get(),
})
}
// Executes a prepared statement and returns all results as an array of objects.
//
// If the prepared statement does not return any results, this method returns an empty array.
// Optionally, parameters can be bound to the prepared statement.
#[reentrant]
fn all<'a>(
&self,
scope: &mut v8::PinScope<'a, '_>,
#[varargs] params: Option<&v8::FunctionCallbackArguments>,
) -> Result<v8::Local<'a, v8::Array>, SqliteError> {
self.invalidate_iter();
self.bind_params(scope, params)?;
let _reset = ResetGuard(self);
// Column names are constant across every row of the result set. Build the
// V8 name strings once here and reuse them for all rows, instead of
// re-creating them per row (as the generic `read_row` does): this drops a
// `sqlite3_column_name` FFI call and a string allocation per column per
// row, and gives every row object the same shape (hidden class).
let num_cols = self.column_count()?;
let return_arrays = self.return_arrays();
let mut names = Vec::with_capacity(num_cols as usize);
if !return_arrays {
for i in 0..num_cols {
let name = self.column_name(i)?;
names.push(
v8::String::new_from_utf8(scope, name, v8::NewStringType::Normal)
.unwrap()
.into(),
);
}
}
let null = v8::null(scope).into();
let mut arr = vec![];
while !self.step()? {
let mut values = Vec::with_capacity(num_cols as usize);
for i in 0..num_cols {
values.push(self.column_value(i, scope)?);
}
let row = if return_arrays {
v8::Array::new_with_elements(scope, &values).into()
} else {
v8::Object::with_prototype_and_properties(scope, null, &names, &values)
.into()
};
arr.push(row);
}
let arr = v8::Array::new_with_elements(scope, &arr);
Ok(arr)
}
#[reentrant]
fn iterate<'a>(
&self,
scope: &mut v8::PinScope<'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",
__STATEMENT_REF = "__statement_ref",
}
self.invalidate_iter();
self.reset()?;
self.bind_params(scope, params)?;
let args = params.expect("StatementSync#iterate expected callback args");
let statement_ref: v8::Local<v8::Value> = args.this().into();
// Capture the current generation so the iterator can detect invalidation.
// Store a strong handle to the JS wrapper in the callback data itself so
// detached `next`/`return` functions keep the native statement alive.
let iter_ctx = Box::into_raw(Box::new(IteratorContext {
statement: self as *const StatementSync,
statement_ref: v8::Global::new(scope, statement_ref),
expected_generation: self.iter_generation.get(),
finished: Cell::new(false),
finalized_functions: Cell::new(0),
next_func: RefCell::new(None),
return_func: RefCell::new(None),
}));
// Track the allocation so it can be freed on Drop if the isolate tears down
// before the weak function finalizers run.
self.iter_contexts.borrow_mut().push(iter_ctx);
let iterate_next = |scope: &mut v8::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue| {
let external = v8::Local::<v8::External>::try_from(args.data())
.expect("Iterator#next expected external data");
// SAFETY: `external` wraps a pointer allocated via `Box::into_raw` in
// `iterate()`. It remains valid while this callback function is live
// because the context is released by weak finalizers on the generated
// `next` and `return` functions.
let ctx = unsafe { &*(external.value() as *const IteratorContext) };
// SAFETY: The context holds a strong reference to the statement's JS
// wrapper, which prevents the StatementSync cppgc object from being
// garbage collected while detached callbacks are alive.
let statement = unsafe { &*ctx.statement };
let names = &[
DONE.v8_string(scope).unwrap().into(),
VALUE.v8_string(scope).unwrap().into(),
];
// If this iterator already finished, return done regardless of
// whether the statement was subsequently reset.
if ctx.finished.get() {
let values =
&[v8::Boolean::new(scope, true).into(), v8::null(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;
}
// Check if iterator was invalidated by get/all/run/iterate
if statement.iter_generation.get() != ctx.expected_generation {
let msg =
v8::String::new(scope, "This iterator was invalidated because the statement was reset by calling get(), all(), run(), or iterate() on the same statement object.")
.unwrap();
let err = v8::Exception::error(scope, msg);
let code_key = v8::String::new(scope, "code").unwrap();
let code_val = v8::String::new(scope, "ERR_INVALID_STATE").unwrap();
err.to_object(scope).unwrap().set(
scope,
code_key.into(),
code_val.into(),
);
scope.throw_exception(err);
return;
}
let Ok(Some(row)) = statement.read_row(scope) else {
let _ = statement.reset();
statement.is_iter_finished.set(true);
ctx.finished.set(true);
let values =
&[v8::Boolean::new(scope, true).into(), v8::null(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];
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::PinScope<'_, '_>,
args: v8::FunctionCallbackArguments,
mut rv: v8::ReturnValue| {
let external = v8::Local::<v8::External>::try_from(args.data())
.expect("Iterator#return expected external data");
let ctx_ptr = external.value() as *const IteratorContext;
// SAFETY: `ctx_ptr` was allocated via `Box::into_raw` in `iterate()`
// and remains valid while this callback function is live because the
// generated functions' weak finalizers release it.
let ctx = unsafe { &*ctx_ptr };
// SAFETY: The statement pointer is kept alive by `ctx.statement_ref`.
let statement = unsafe { &*ctx.statement };
// Note: We do NOT free the IteratorContext here because `next()` may
// still be called after `return()` (e.g., the JS protocol allows it).
// It will be freed after both generated callback functions are collected.
ctx.finished.set(true);
statement.is_iter_finished.set(true);
let _ = 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::null(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, iter_ctx 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 weak_next = v8::Weak::with_finalizer(
scope,
next_func,
Box::new(move |_| release_iterator_context(iter_ctx)),
);
let weak_return = v8::Weak::with_finalizer(
scope,
return_func,
Box::new(move |_| release_iterator_context(iter_ctx)),
);
// SAFETY: `iter_ctx` was allocated above and is kept alive by the weak
// handles stored inside it.
unsafe {
*(*iter_ctx).next_func.borrow_mut() = Some(weak_next);
*(*iter_ctx).return_func.borrow_mut() = Some(weak_return);
}
let global = scope.get_current_context().global(scope);
let iter_str = ITERATOR.v8_string(scope).unwrap();
let js_iterator = global
.get(scope, iter_str.into())
.ok_or(validators::Error::V8Exception)?;
let js_iterator =
v8::Local::<v8::Object>::try_from(js_iterator).map_err(|_| {
validators::Error::InvalidArgType("Iterator must be an object.".into())
})?;
let proto_str = PROTOTYPE.v8_string(scope).unwrap();
let js_iterator_proto = js_iterator
.get(scope, proto_str.into())
.ok_or(validators::Error::V8Exception)?;
if !js_iterator_proto.is_object() && !js_iterator_proto.is_null() {
return Err(
validators::Error::InvalidArgType(
"Iterator.prototype must be an object or null.".into(),
)
.into(),
);
}
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,
);
// SAFETY: `iter_ctx` was allocated above and remains alive at least until
// both generated callback functions are finalized.
let statement_ref =
v8::Local::new(scope, unsafe { &(*iter_ctx).statement_ref });
let attrs = v8::PropertyAttribute::READ_ONLY
| v8::PropertyAttribute::DONT_ENUM
| v8::PropertyAttribute::DONT_DELETE;
let statement_ref_key = __STATEMENT_REF.v8_string(scope).unwrap().into();
iterator
.define_own_property(scope, statement_ref_key, statement_ref, attrs)
.unwrap();
self.is_iter_finished.set(false);
Ok(iterator)
}
#[fast]
#[undefined]
fn set_allow_bare_named_parameters(
&self,
#[validate(validators::allow_bare_named_params_bool)] enabled: bool,
) -> Result<(), SqliteError> {
self.assert_statement_finalized()?;
self.allow_bare_named_params.set(enabled);
Ok(())
}
#[fast]
#[undefined]
fn set_allow_unknown_named_parameters(
&self,
#[validate(validators::allow_unknown_named_params_bool)] enabled: bool,
) -> Result<(), SqliteError> {
self.assert_statement_finalized()?;
self.allow_unknown_named_params.set(enabled);
Ok(())
}
#[fast]
#[undefined]
fn set_read_big_ints(
&self,
#[validate(validators::read_big_ints_bool)] enabled: bool,
) -> Result<(), SqliteError> {
self.assert_statement_finalized()?;
self.use_big_ints.set(enabled);
Ok(())
}
#[fast]
#[undefined]
fn set_return_arrays(
&self,
#[validate(validators::return_arrays_bool)] enabled: bool,
) -> Result<(), SqliteError> {
self.assert_statement_finalized()?;
self.return_arrays.set(enabled);
Ok(())
}
#[getter]
#[rename("sourceSQL")]
#[string]
fn source_sql(&self) -> Result<String, SqliteError> {
let inner = self.stmt_ptr()?;
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
// as it lives as long as the StatementSync instance.
let source_sql = unsafe {
let raw = ffi::sqlite3_sql(inner);
std::ffi::CStr::from_ptr(raw as _)
.to_string_lossy()
.into_owned()
};
Ok(source_sql)
}
#[getter]
#[rename("expandedSQL")]
#[string]
fn expanded_sql(&self) -> Result<String, SqliteError> {
let inner = self.stmt_ptr()?;
// SAFETY: `inner` is a valid pointer to a sqlite3_stmt
// as it lives as long as the StatementSync instance.
unsafe {
let raw = ffi::sqlite3_expanded_sql(inner);
if raw.is_null() {
return Err(SqliteError::InvalidExpandedSql);
}
let sql = std::ffi::CStr::from_ptr(raw as _)
.to_string_lossy()
.into_owned();
ffi::sqlite3_free(raw as _);
Ok(sql)
}
}
fn columns<'a>(
&self,
scope: &mut v8::PinScope<'a, '_>,
) -> Result<v8::Local<'a, v8::Array>, SqliteError> {
v8_static_strings! {
NAME = "name",
COLUMN = "column",
TABLE = "table",
DATABASE = "database",
TYPE = "type",
}
let column_count = self.column_count()?;
let mut columns = Vec::with_capacity(column_count as usize);
// Pre-create property keys
let name_key = NAME.v8_string(scope).unwrap().into();
let column_key = COLUMN.v8_string(scope).unwrap().into();
let table_key = TABLE.v8_string(scope).unwrap().into();
let database_key = DATABASE.v8_string(scope).unwrap().into();
let type_key = TYPE.v8_string(scope).unwrap().into();
let keys = &[name_key, column_key, table_key, database_key, type_key];
let raw = self.stmt_ptr()?;
for i in 0..column_count {
// name: The name of the column in the result set
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
let name = unsafe {
let name_ptr = ffi::sqlite3_column_name(raw, i);
if !name_ptr.is_null() {
let name_cstr = std::ffi::CStr::from_ptr(name_ptr as _);
v8::String::new_from_utf8(
scope,
name_cstr.to_bytes(),
v8::NewStringType::Normal,
)
.unwrap()
.into()
} else {
v8::null(scope).into()
}
};
// column: The unaliased name of the column in the origin table
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
let column = unsafe {
let column_ptr = ffi::sqlite3_column_origin_name(raw, i);
if !column_ptr.is_null() {
let column_cstr = std::ffi::CStr::from_ptr(column_ptr as _);
v8::String::new_from_utf8(
scope,
column_cstr.to_bytes(),
v8::NewStringType::Normal,
)
.unwrap()
.into()
} else {
v8::null(scope).into()
}
};
// table: The unaliased name of the origin table
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
let table = unsafe {
let table_ptr = ffi::sqlite3_column_table_name(raw, i);
if !table_ptr.is_null() {
let table_cstr = std::ffi::CStr::from_ptr(table_ptr as _);
v8::String::new_from_utf8(
scope,
table_cstr.to_bytes(),
v8::NewStringType::Normal,
)
.unwrap()
.into()
} else {
v8::null(scope).into()
}
};
// database: The unaliased name of the origin database
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
let database = unsafe {
let database_ptr = ffi::sqlite3_column_database_name(raw, i);
if !database_ptr.is_null() {
let database_cstr = std::ffi::CStr::from_ptr(database_ptr as _);
v8::String::new_from_utf8(
scope,
database_cstr.to_bytes(),
v8::NewStringType::Normal,
)
.unwrap()
.into()
} else {
v8::null(scope).into()
}
};
// type: The declared data type of the column
// SAFETY: `raw` is a valid pointer to a sqlite3_stmt
let col_type = unsafe {
let type_ptr = ffi::sqlite3_column_decltype(raw, i);
if !type_ptr.is_null() {
let type_cstr = std::ffi::CStr::from_ptr(type_ptr as _);
v8::String::new_from_utf8(
scope,
type_cstr.to_bytes(),
v8::NewStringType::Normal,
)
.unwrap()
.into()
} else {
v8::null(scope).into()
}
};
let values = &[name, column, table, database, col_type];
let null = v8::null(scope).into();
let obj =
v8::Object::with_prototype_and_properties(scope, null, keys, values);
columns.push(obj.into());
}
Ok(v8::Array::new_with_elements(scope, &columns))
}
}