diff --git a/cli/bench/sqlite.js b/cli/bench/sqlite.js new file mode 100644 index 0000000000..d5f8aca4d6 --- /dev/null +++ b/cli/bench/sqlite.js @@ -0,0 +1,36 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +// deno-lint-ignore-file no-console + +import { DatabaseSync } from "node:sqlite"; +import fs from "node:fs"; + +function bench(name, fun, count = 10000) { + const start = Date.now(); + for (let i = 0; i < count; i++) fun(); + const elapsed = Date.now() - start; + const rate = Math.floor(count / (elapsed / 1000)); + console.log(` ${name}: time ${elapsed} ms rate ${rate}`); +} + +for (const name of [":memory:", "test.db"]) { + console.log(`Benchmarking ${name}`); + try { + fs.unlinkSync(name); + } catch (e) { + // Ignore + } + + const db = new DatabaseSync(name); + db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)"); + + bench("prepare", () => db.prepare("SELECT * FROM test")); + bench("exec", () => db.exec("INSERT INTO test (name) VALUES ('foo')")); + + const stmt = db.prepare("SELECT * FROM test"); + bench("get", () => stmt.get()); + + const stmt2 = db.prepare("SELECT * FROM test WHERE id = ?"); + bench("get (integer bind)", () => stmt2.get(1)); + + bench("all", () => stmt.all(), 1000); +} diff --git a/ext/node/ops/sqlite/mod.rs b/ext/node/ops/sqlite/mod.rs index 110399ffb5..fe75c1fe85 100644 --- a/ext/node/ops/sqlite/mod.rs +++ b/ext/node/ops/sqlite/mod.rs @@ -24,6 +24,8 @@ pub enum SqliteError { AlreadyOpen, #[error("Failed to prepare statement")] PrepareFailed, + #[error("Invalid constructor")] + InvalidConstructor, #[error(transparent)] Other(deno_core::error::AnyError), } diff --git a/ext/node/ops/sqlite/statement.rs b/ext/node/ops/sqlite/statement.rs index 157321815a..92026e97b9 100644 --- a/ext/node/ops/sqlite/statement.rs +++ b/ext/node/ops/sqlite/statement.rs @@ -6,6 +6,7 @@ use std::rc::Rc; use deno_core::op2; use deno_core::v8; use deno_core::GarbageCollected; +use libsqlite3_sys as ffi; use serde::Serialize; use super::SqliteError; @@ -18,150 +19,194 @@ pub struct RunStatementResult { } pub struct StatementSync { - pub inner: *mut libsqlite3_sys::sqlite3_stmt, + pub inner: *mut ffi::sqlite3_stmt, pub db: Rc>>, } +impl Drop for StatementSync { + fn drop(&mut self) { + // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt + // no other references to this pointer exist. + unsafe { + ffi::sqlite3_finalize(self.inner); + } + } +} + impl GarbageCollected for StatementSync {} -fn read_entry<'a>( - raw: *mut libsqlite3_sys::sqlite3_stmt, - scope: &mut v8::HandleScope<'a>, -) -> Result>, SqliteError> { - let result = v8::Object::new(scope); - unsafe { - let r = libsqlite3_sys::sqlite3_step(raw); - - if r == libsqlite3_sys::SQLITE_DONE { - return Ok(None); - } - if r != libsqlite3_sys::SQLITE_ROW { - return Err(SqliteError::FailedStep); - } - - let columns = libsqlite3_sys::sqlite3_column_count(raw); - - for i in 0..columns { - let name = libsqlite3_sys::sqlite3_column_name(raw, i); - let name = std::ffi::CStr::from_ptr(name).to_string_lossy().to_string(); - let value = match libsqlite3_sys::sqlite3_column_type(raw, i) { - libsqlite3_sys::SQLITE_INTEGER => { - let value = libsqlite3_sys::sqlite3_column_int64(raw, i); - v8::Integer::new(scope, value as _).into() - } - libsqlite3_sys::SQLITE_FLOAT => { - let value = libsqlite3_sys::sqlite3_column_double(raw, i); - v8::Number::new(scope, value).into() - } - libsqlite3_sys::SQLITE_TEXT => { - let value = libsqlite3_sys::sqlite3_column_text(raw, i); - let value = std::ffi::CStr::from_ptr(value as _) - .to_string_lossy() - .to_string(); - v8::String::new_from_utf8( - scope, - value.as_bytes(), - v8::NewStringType::Normal, - ) - .unwrap() - .into() - } - libsqlite3_sys::SQLITE_BLOB => { - let value = libsqlite3_sys::sqlite3_column_blob(raw, i); - let size = libsqlite3_sys::sqlite3_column_bytes(raw, i); - let value = - std::slice::from_raw_parts(value as *const u8, size as usize); - let value = - v8::ArrayBuffer::new_backing_store_from_vec(value.to_vec()) - .make_shared(); - v8::ArrayBuffer::with_backing_store(scope, &value).into() - } - libsqlite3_sys::SQLITE_NULL => v8::null(scope).into(), - _ => { - return Err(SqliteError::UnknownColumnType); - } - }; - - let name = v8::String::new_from_utf8( - scope, - name.as_bytes(), - v8::NewStringType::Normal, - ) - .unwrap() - .into(); - result.set(scope, name, value); +impl StatementSync { + // Clear the prepared statement back to its initial state. + fn reset(&self) { + // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt. + unsafe { + ffi::sqlite3_reset(self.inner); } } - Ok(Some(result)) -} - -fn bind_params( - scope: &mut v8::HandleScope, - raw: *mut libsqlite3_sys::sqlite3_stmt, - params: Option<&v8::FunctionCallbackArguments>, -) -> Result<(), SqliteError> { - if let Some(params) = params { - let len = params.length(); - for i in 0..len { - let value = params.get(i); - - if value.is_number() { - let value = value.number_value(scope).unwrap(); - unsafe { - libsqlite3_sys::sqlite3_bind_double(raw, i as i32 + 1, value); - } - } else if value.is_string() { - let value = value.to_rust_string_lossy(scope); - unsafe { - libsqlite3_sys::sqlite3_bind_text( - raw, - i as i32 + 1, - value.as_ptr() as *const i8, - value.len() as i32, - libsqlite3_sys::SQLITE_TRANSIENT(), - ); - } - } else if value.is_null() { - unsafe { - libsqlite3_sys::sqlite3_bind_null(raw, i as i32 + 1); - } - } else { - return Err(SqliteError::FailedBind("Unsupported type")); + // Evaluate the prepared statement. + fn step(&self) -> Result { + let raw = self.inner; + // SAFETY: `self.inner` is a valid pointer to a sqlite3_stmt. + unsafe { + let r = ffi::sqlite3_step(raw); + if r == ffi::SQLITE_DONE { + return Ok(true); + } + if r != ffi::SQLITE_ROW { + return Err(SqliteError::FailedStep); } } + + Ok(false) } - Ok(()) + // Read the current row of the prepared statement. + fn read_row<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + ) -> Result>, SqliteError> { + let result = v8::Object::new(scope); + let raw = self.inner; + unsafe { + if self.step()? { + return Ok(None); + } + + let columns = ffi::sqlite3_column_count(raw); + + for i in 0..columns { + let name = ffi::sqlite3_column_name(raw, i); + let name = std::ffi::CStr::from_ptr(name).to_string_lossy(); + let value = match ffi::sqlite3_column_type(raw, i) { + ffi::SQLITE_INTEGER => { + let value = ffi::sqlite3_column_int64(raw, i); + v8::Integer::new(scope, value as _).into() + } + ffi::SQLITE_FLOAT => { + let value = ffi::sqlite3_column_double(raw, i); + v8::Number::new(scope, value).into() + } + ffi::SQLITE_TEXT => { + let value = ffi::sqlite3_column_text(raw, i); + let value = std::ffi::CStr::from_ptr(value as _) + .to_string_lossy() + .to_string(); + v8::String::new_from_utf8( + scope, + value.as_bytes(), + v8::NewStringType::Normal, + ) + .unwrap() + .into() + } + ffi::SQLITE_BLOB => { + let value = ffi::sqlite3_column_blob(raw, i); + let size = ffi::sqlite3_column_bytes(raw, i); + let value = + std::slice::from_raw_parts(value as *const u8, size as usize); + let value = + v8::ArrayBuffer::new_backing_store_from_vec(value.to_vec()) + .make_shared(); + v8::ArrayBuffer::with_backing_store(scope, &value).into() + } + ffi::SQLITE_NULL => v8::null(scope).into(), + _ => { + return Err(SqliteError::UnknownColumnType); + } + }; + + let name = v8::String::new_from_utf8( + scope, + name.as_bytes(), + v8::NewStringType::Normal, + ) + .unwrap() + .into(); + result.set(scope, name, value); + } + } + + Ok(Some(result)) + } + + fn bind_params( + &self, + scope: &mut v8::HandleScope, + params: Option<&v8::FunctionCallbackArguments>, + ) -> Result<(), SqliteError> { + let raw = self.inner; + + if let Some(params) = params { + let len = params.length(); + for i in 0..len { + let value = params.get(i); + + if value.is_number() { + let value = value.number_value(scope).unwrap(); + unsafe { + ffi::sqlite3_bind_double(raw, i as i32 + 1, value); + } + } else if value.is_string() { + let value = value.to_rust_string_lossy(scope); + unsafe { + ffi::sqlite3_bind_text( + raw, + i as i32 + 1, + value.as_ptr() as *const i8, + value.len() as i32, + ffi::SQLITE_TRANSIENT(), + ); + } + } else if value.is_null() { + unsafe { + ffi::sqlite3_bind_null(raw, i as i32 + 1); + } + } else if value.is_array_buffer_view() { + let value: v8::Local = value.try_into().unwrap(); + let data = value.data(); + let size = value.byte_length(); + + unsafe { + ffi::sqlite3_bind_blob( + raw, + i as i32 + 1, + data, + size as i32, + ffi::SQLITE_TRANSIENT(), + ); + } + } else { + return Err(SqliteError::FailedBind("Unsupported type")); + } + } + } + + Ok(()) + } } #[op2] impl StatementSync { #[constructor] #[cppgc] - fn new(_: bool) -> StatementSync { - unimplemented!() + fn new(_: bool) -> Result { + Err(SqliteError::InvalidConstructor) } fn get<'a>( &self, scope: &mut v8::HandleScope<'a>, #[varargs] params: Option<&v8::FunctionCallbackArguments>, - ) -> Result, SqliteError> { - let raw = self.inner; + ) -> Result, SqliteError> { + self.bind_params(scope, params)?; - unsafe { - libsqlite3_sys::sqlite3_reset(raw); - } + let entry = self.read_row(scope)?; + let result = entry + .map(|r| r.into()) + .unwrap_or_else(|| v8::undefined(scope).into()); - bind_params(scope, raw, params)?; - - let result = read_entry(raw, scope) - .map(|r| r.unwrap_or_else(|| v8::Object::new(scope)))?; - - unsafe { - libsqlite3_sys::sqlite3_reset(raw); - } + self.reset(); Ok(result) } @@ -172,34 +217,17 @@ impl StatementSync { scope: &mut v8::HandleScope, #[varargs] params: Option<&v8::FunctionCallbackArguments>, ) -> Result { - let raw = self.inner; let db = self.db.borrow(); - let db = db.as_ref().unwrap(); + let db = db.as_ref().ok_or(SqliteError::InUse)?; - let last_insert_rowid; - let changes; + self.bind_params(scope, params)?; + self.step()?; - unsafe { - libsqlite3_sys::sqlite3_reset(raw); - - bind_params(scope, raw, params)?; - loop { - let r = libsqlite3_sys::sqlite3_step(raw); - if r == libsqlite3_sys::SQLITE_DONE { - break; - } - if r != libsqlite3_sys::SQLITE_ROW { - return Err(SqliteError::FailedStep); - } - } - - last_insert_rowid = db.last_insert_rowid(); - changes = db.changes(); - } + self.reset(); Ok(RunStatementResult { - last_insert_rowid, - changes, + last_insert_rowid: db.last_insert_rowid(), + changes: db.changes(), }) } @@ -208,20 +236,15 @@ impl StatementSync { scope: &mut v8::HandleScope<'a>, #[varargs] params: Option<&v8::FunctionCallbackArguments>, ) -> Result, SqliteError> { - let raw = self.inner; - let mut arr = vec![]; - unsafe { - libsqlite3_sys::sqlite3_reset(raw); - bind_params(scope, raw, params)?; - while let Some(result) = read_entry(raw, scope)? { - arr.push(result.into()); - } - - libsqlite3_sys::sqlite3_reset(raw); + self.bind_params(scope, params)?; + while let Some(result) = self.read_row(scope)? { + arr.push(result.into()); } + self.reset(); + let arr = v8::Array::new_with_elements(scope, &arr); Ok(arr) }