diff --git a/bindings/javascript/index.d.ts b/bindings/javascript/index.d.ts index f38dfcf6d..f9447e696 100644 --- a/bindings/javascript/index.d.ts +++ b/bindings/javascript/index.d.ts @@ -94,8 +94,17 @@ export declare class Statement { * * `value` - The value to bind. */ bindAt(index: number, value: unknown): void - step(): unknown + /** + * Step the statement and return result code: + * 1 = Row available, 2 = Done, 3 = I/O needed + */ + step(): number + /** Get the current row data according to the presentation mode */ + row(): unknown + /** Sets the presentation mode to raw. */ raw(raw?: boolean | undefined | null): void + /** Sets the presentation mode to pluck. */ pluck(pluck?: boolean | undefined | null): void + /** Finalizes the statement. */ finalize(): void } diff --git a/bindings/javascript/perf/perf-better-sqlite3.js b/bindings/javascript/perf/perf-better-sqlite3.js index 4ebb34d1e..4b9348bcc 100644 --- a/bindings/javascript/perf/perf-better-sqlite3.js +++ b/bindings/javascript/perf/perf-better-sqlite3.js @@ -4,15 +4,23 @@ import Database from 'better-sqlite3'; const db = new Database(':memory:'); -db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"); +db.exec("CREATE TABLE users (id INTEGER, name TEXT, email TEXT)"); db.exec("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"); -const stmt = db.prepare("SELECT * FROM users WHERE id = ?"); +const stmtSelect = db.prepare("SELECT * FROM users WHERE id = ?"); +const rawStmtSelect = db.prepare("SELECT * FROM users WHERE id = ?").raw(); +const stmtInsert = db.prepare("INSERT INTO users (id, name, email) VALUES (?, ?, ?)"); -group('Statement', () => { - bench('Statement.get() bind parameters', () => { - stmt.get(1); - }); +bench('Statement.get() with bind parameters [expanded]', () => { + stmtSelect.get(1); +}); + +bench('Statement.git() with bind parameters [raw]', () => { + rawStmtSelect.get(1); +}); + +bench('Statement.run() with bind parameters', () => { + stmtInsert.run([1, 'foobar', 'foobar@example.com']); }); await run({ diff --git a/bindings/javascript/perf/perf-turso.js b/bindings/javascript/perf/perf-turso.js index 8987cafd6..0c31ad124 100644 --- a/bindings/javascript/perf/perf-turso.js +++ b/bindings/javascript/perf/perf-turso.js @@ -4,15 +4,23 @@ import Database from '@tursodatabase/turso'; const db = new Database(':memory:'); -db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)"); +db.exec("CREATE TABLE users (id INTEGER, name TEXT, email TEXT)"); db.exec("INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.org')"); -const stmt = db.prepare("SELECT * FROM users WHERE id = ?"); +const stmtSelect = db.prepare("SELECT * FROM users WHERE id = ?"); +const rawStmtSelect = db.prepare("SELECT * FROM users WHERE id = ?").raw(); +const stmtInsert = db.prepare("INSERT INTO users (id, name, email) VALUES (?, ?, ?)"); -group('Statement', () => { - bench('Statement.get() bind parameters', () => { - stmt.get(1); - }); +bench('Statement.get() with bind parameters [expanded]', () => { + stmtSelect.get(1); +}); + +bench('Statement.get() with bind parameters [raw]', () => { + rawStmtSelect.get(1); +}); + +bench('Statement.run() with bind parameters', () => { + stmtInsert.run([1, 'foobar', 'foobar@example.com']); }); await run({ diff --git a/bindings/javascript/promise.js b/bindings/javascript/promise.js index 6e9347a45..7a54871c3 100644 --- a/bindings/javascript/promise.js +++ b/bindings/javascript/promise.js @@ -5,6 +5,11 @@ const { bindParams } = require("./bind.js"); const SqliteError = require("./sqlite-error.js"); +// Step result constants +const STEP_ROW = 1; +const STEP_DONE = 2; +const STEP_IO = 3; + const convertibleErrorTypes = { TypeError }; const CONVERTIBLE_ERROR_PREFIX = "[TURSO_CONVERT_TYPE]"; @@ -258,14 +263,18 @@ class Statement { bindParams(this.stmt, bindParameters); while (true) { - const result = this.stmt.step(); - if (result.io) { + const stepResult = this.stmt.step(); + if (stepResult === STEP_IO) { await this.db.db.ioLoopAsync(); continue; } - if (result.done) { + if (stepResult === STEP_DONE) { break; } + if (stepResult === STEP_ROW) { + // For run(), we don't need the row data, just continue + continue; + } } const lastInsertRowid = this.db.db.lastInsertRowid(); @@ -284,15 +293,17 @@ class Statement { bindParams(this.stmt, bindParameters); while (true) { - const result = this.stmt.step(); - if (result.io) { + const stepResult = this.stmt.step(); + if (stepResult === STEP_IO) { await this.db.db.ioLoopAsync(); continue; } - if (result.done) { + if (stepResult === STEP_DONE) { return undefined; } - return result.value; + if (stepResult === STEP_ROW) { + return this.stmt.row(); + } } } @@ -316,15 +327,17 @@ class Statement { const rows = []; while (true) { - const result = this.stmt.step(); - if (result.io) { + const stepResult = this.stmt.step(); + if (stepResult === STEP_IO) { await this.db.db.ioLoopAsync(); continue; } - if (result.done) { + if (stepResult === STEP_DONE) { break; } - rows.push(result.value); + if (stepResult === STEP_ROW) { + rows.push(this.stmt.row()); + } } return rows; } diff --git a/bindings/javascript/src/lib.rs b/bindings/javascript/src/lib.rs index 611494b4a..4add30661 100644 --- a/bindings/javascript/src/lib.rs +++ b/bindings/javascript/src/lib.rs @@ -15,6 +15,11 @@ use napi::{Env, Task}; use napi_derive::napi; use std::{cell::RefCell, num::NonZeroUsize, sync::Arc}; +/// Step result constants +const STEP_ROW: u32 = 1; +const STEP_DONE: u32 = 2; +const STEP_IO: u32 = 3; + /// The presentation mode for rows. #[derive(Debug, Clone)] enum PresentationMode { @@ -289,92 +294,89 @@ impl Statement { Ok(()) } + /// Step the statement and return result code: + /// 1 = Row available, 2 = Done, 3 = I/O needed #[napi] - pub fn step<'env>(&self, env: &'env Env) -> Result> { + pub fn step(&self) -> Result { let mut stmt_ref = self.stmt.borrow_mut(); let stmt = stmt_ref .as_mut() .ok_or_else(|| Error::new(Status::GenericFailure, "Statement has been finalized"))?; - let mut result = Object::new(env)?; - match stmt.step() { - Ok(turso_core::StepResult::Row) => { - result.set_named_property("done", false)?; - - let row_data = stmt - .row() - .ok_or_else(|| Error::new(Status::GenericFailure, "No row data available"))?; - - let mode = self.mode.borrow(); - let row_value = - match *mode { - PresentationMode::Raw => { - let mut raw_array = env.create_array(row_data.len() as u32)?; - for (idx, value) in row_data.get_values().enumerate() { - let js_value = to_js_value(env, value)?; - raw_array.set(idx as u32, js_value)?; - } - raw_array.coerce_to_object()?.to_unknown() - } - PresentationMode::Pluck => { - let (_, value) = row_data.get_values().enumerate().next().ok_or( - napi::Error::new( - napi::Status::GenericFailure, - "Pluck mode requires at least one column in the result", - ), - )?; - to_js_value(env, value)? - } - PresentationMode::Expanded => { - let row = Object::new(env)?; - let raw_row = row.raw(); - let raw_env = env.raw(); - for idx in 0..row_data.len() { - let value = row_data.get_value(idx); - let column_name = &self.column_names[idx]; - let js_value = to_js_value(env, value)?; - unsafe { - napi::sys::napi_set_named_property( - raw_env, - raw_row, - column_name.as_ptr(), - js_value.raw(), - ); - } - } - row.to_unknown() - } - }; - - result.set_named_property("value", row_value)?; - } - Ok(turso_core::StepResult::Done) => { - result.set_named_property("done", true)?; - result.set_named_property("value", Null)?; - } - Ok(turso_core::StepResult::IO) => { - result.set_named_property("io", true)?; - result.set_named_property("value", Null)?; - } - Ok(turso_core::StepResult::Interrupt) => { - return Err(Error::new( - Status::GenericFailure, - "Statement was interrupted", - )); - } + Ok(turso_core::StepResult::Row) => Ok(STEP_ROW), + Ok(turso_core::StepResult::Done) => Ok(STEP_DONE), + Ok(turso_core::StepResult::IO) => Ok(STEP_IO), + Ok(turso_core::StepResult::Interrupt) => Err(Error::new( + Status::GenericFailure, + "Statement was interrupted", + )), Ok(turso_core::StepResult::Busy) => { - return Err(Error::new(Status::GenericFailure, "Database is busy")); - } - Err(e) => { - return Err(Error::new( - Status::GenericFailure, - format!("Step failed: {e}"), - )) + Err(Error::new(Status::GenericFailure, "Database is busy")) } + Err(e) => Err(Error::new( + Status::GenericFailure, + format!("Step failed: {e}"), + )), } + } - Ok(result.to_unknown()) + /// Get the current row data according to the presentation mode + #[napi] + pub fn row<'env>(&self, env: &'env Env) -> Result> { + let stmt_ref = self.stmt.borrow(); + let stmt = stmt_ref + .as_ref() + .ok_or_else(|| Error::new(Status::GenericFailure, "Statement has been finalized"))?; + + let row_data = stmt + .row() + .ok_or_else(|| Error::new(Status::GenericFailure, "No row data available"))?; + + let mode = self.mode.borrow(); + let row_value = match *mode { + PresentationMode::Raw => { + let mut raw_array = env.create_array(row_data.len() as u32)?; + for (idx, value) in row_data.get_values().enumerate() { + let js_value = to_js_value(env, value)?; + raw_array.set(idx as u32, js_value)?; + } + raw_array.coerce_to_object()?.to_unknown() + } + PresentationMode::Pluck => { + let (_, value) = + row_data + .get_values() + .enumerate() + .next() + .ok_or(napi::Error::new( + napi::Status::GenericFailure, + "Pluck mode requires at least one column in the result", + ))?; + to_js_value(env, value)? + } + PresentationMode::Expanded => { + let row = Object::new(env)?; + let raw_row = row.raw(); + let raw_env = env.raw(); + for idx in 0..row_data.len() { + let value = row_data.get_value(idx); + let column_name = &self.column_names[idx]; + let js_value = to_js_value(env, value)?; + unsafe { + napi::sys::napi_set_named_property( + raw_env, + raw_row, + column_name.as_ptr(), + js_value.raw(), + ); + } + } + row.to_unknown() + } + }; + + Ok(row_value) } /// Sets the presentation mode to raw. diff --git a/bindings/javascript/sync.js b/bindings/javascript/sync.js index 1cf5954ac..bca456232 100644 --- a/bindings/javascript/sync.js +++ b/bindings/javascript/sync.js @@ -5,6 +5,11 @@ const { bindParams } = require("./bind.js"); const SqliteError = require("./sqlite-error.js"); +// Step result constants +const STEP_ROW = 1; +const STEP_DONE = 2; +const STEP_IO = 3; + const convertibleErrorTypes = { TypeError }; const CONVERTIBLE_ERROR_PREFIX = "[TURSO_CONVERT_TYPE]"; @@ -257,14 +262,18 @@ class Statement { this.stmt.reset(); bindParams(this.stmt, bindParameters); for (;;) { - const result = this.stmt.step(); - if (result.io) { + const stepResult = this.stmt.step(); + if (stepResult === STEP_IO) { this.db.db.ioLoopSync(); continue; } - if (result.done) { + if (stepResult === STEP_DONE) { break; } + if (stepResult === STEP_ROW) { + // For run(), we don't need the row data, just continue + continue; + } } const lastInsertRowid = this.db.db.lastInsertRowid(); @@ -282,15 +291,17 @@ class Statement { this.stmt.reset(); bindParams(this.stmt, bindParameters); for (;;) { - const result = this.stmt.step(); - if (result.io) { + const stepResult = this.stmt.step(); + if (stepResult === STEP_IO) { this.db.db.ioLoopSync(); continue; } - if (result.done) { + if (stepResult === STEP_DONE) { return undefined; } - return result.value; + if (stepResult === STEP_ROW) { + return this.stmt.row(); + } } } @@ -313,15 +324,17 @@ class Statement { bindParams(this.stmt, bindParameters); const rows = []; for (;;) { - const result = this.stmt.step(); - if (result.io) { + const stepResult = this.stmt.step(); + if (stepResult === STEP_IO) { this.db.db.ioLoopSync(); continue; } - if (result.done) { + if (stepResult === STEP_DONE) { break; } - rows.push(result.value); + if (stepResult === STEP_ROW) { + rows.push(this.stmt.row()); + } } return rows; }