//! JavaScript bindings for the Turso library. //! //! These bindings provide a thin layer that exposes Turso's Rust API to JavaScript, //! maintaining close alignment with the underlying implementation while offering //! the following core database operations: //! //! - Opening and closing database connections //! - Preparing SQL statements //! - Binding parameters to prepared statements //! - Iterating through query results //! - Managing the I/O event loop use napi::bindgen_prelude::*; use napi::{Env, Task}; use napi_derive::napi; use std::{ cell::{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 { Expanded, Raw, Pluck, } /// A database connection. #[napi] #[derive(Clone)] pub struct Database { _db: Option>, io: Arc, conn: Arc, is_memory: bool, is_open: Cell, } #[napi] impl Database { /// Creates a new database instance. /// /// # Arguments /// * `path` - The path to the database file. #[napi(constructor)] pub fn new(path: String) -> Result { let is_memory = path == ":memory:"; let io: Arc = if is_memory { Arc::new(turso_core::MemoryIO::new()) } else { Arc::new(turso_core::PlatformIO::new().map_err(|e| { Error::new(Status::GenericFailure, format!("Failed to create IO: {e}")) })?) }; let file = io .open_file(&path, turso_core::OpenFlags::Create, false) .map_err(|e| Error::new(Status::GenericFailure, format!("Failed to open file: {e}")))?; let db_file = Arc::new(DatabaseFile::new(file)); let db = turso_core::Database::open(io.clone(), &path, db_file, false, true).map_err(|e| { Error::new( Status::GenericFailure, format!("Failed to open database: {e}"), ) })?; let conn = db .connect() .map_err(|e| Error::new(Status::GenericFailure, format!("Failed to connect: {e}")))?; Ok(Self::create(Some(db), io, conn, is_memory)) } pub fn create( db: Option>, io: Arc, conn: Arc, is_memory: bool, ) -> Self { Database { _db: db, io, conn, is_memory, is_open: Cell::new(true), } } /// Returns whether the database is in memory-only mode. #[napi(getter)] pub fn memory(&self) -> bool { self.is_memory } /// Returns whether the database connection is open. #[napi(getter)] pub fn open(&self) -> bool { self.is_open.get() } /// Executes a batch of SQL statements. /// /// # Arguments /// /// * `sql` - The SQL statements to execute. /// /// # Returns #[napi] pub fn batch(&self, sql: String) -> Result<()> { self.conn.prepare_execute_batch(&sql).map_err(|e| { Error::new( Status::GenericFailure, format!("Failed to execute batch: {e}"), ) })?; Ok(()) } /// Prepares a statement for execution. /// /// # Arguments /// /// * `sql` - The SQL statement to prepare. /// /// # Returns /// /// A `Statement` instance. #[napi] pub fn prepare(&self, sql: String) -> Result { let stmt = self .conn .prepare(&sql) .map_err(|e| Error::new(Status::GenericFailure, format!("{e}")))?; let column_names: Vec = (0..stmt.num_columns()) .map(|i| std::ffi::CString::new(stmt.get_column_name(i).to_string()).unwrap()) .collect(); Ok(Statement { stmt: RefCell::new(Some(stmt)), column_names, mode: RefCell::new(PresentationMode::Expanded), }) } /// Returns the rowid of the last row inserted. /// /// # Returns /// /// The rowid of the last row inserted. #[napi] pub fn last_insert_rowid(&self) -> Result { Ok(self.conn.last_insert_rowid()) } /// Returns the number of changes made by the last statement. /// /// # Returns /// /// The number of changes made by the last statement. #[napi] pub fn changes(&self) -> Result { Ok(self.conn.changes()) } /// Returns the total number of changes made by all statements. /// /// # Returns /// /// The total number of changes made by all statements. #[napi] pub fn total_changes(&self) -> Result { Ok(self.conn.total_changes()) } /// Closes the database connection. /// /// # Returns /// /// `Ok(())` if the database is closed successfully. #[napi] pub fn close(&self) -> Result<()> { self.is_open.set(false); // Database close is handled automatically when dropped Ok(()) } /// Runs the I/O loop synchronously. #[napi] pub fn io_loop_sync(&self) -> Result<()> { self.io .run_once() .map_err(|e| Error::new(Status::GenericFailure, format!("IO error: {e}")))?; Ok(()) } /// Runs the I/O loop asynchronously, returning a Promise. #[napi(ts_return_type = "Promise")] pub fn io_loop_async(&self) -> AsyncTask { let io = self.io.clone(); AsyncTask::new(IoLoopTask { io }) } } /// A prepared statement. #[napi] pub struct Statement { stmt: RefCell>, column_names: Vec, mode: RefCell, } #[napi] impl Statement { #[napi] pub fn reset(&self) -> Result<()> { let mut stmt = self.stmt.borrow_mut(); let stmt = stmt .as_mut() .ok_or_else(|| Error::new(Status::GenericFailure, "Statement has been finalized"))?; stmt.reset(); Ok(()) } /// Returns the number of parameters in the statement. #[napi] pub fn parameter_count(&self) -> Result { let stmt = self.stmt.borrow(); let stmt = stmt .as_ref() .ok_or_else(|| Error::new(Status::GenericFailure, "Statement has been finalized"))?; Ok(stmt.parameters_count() as u32) } /// Returns the name of a parameter at a specific 1-based index. /// /// # Arguments /// /// * `index` - The 1-based parameter index. #[napi] pub fn parameter_name(&self, index: u32) -> Result> { let stmt = self.stmt.borrow(); let stmt = stmt .as_ref() .ok_or_else(|| Error::new(Status::GenericFailure, "Statement has been finalized"))?; let non_zero_idx = NonZeroUsize::new(index as usize).ok_or_else(|| { Error::new(Status::InvalidArg, "Parameter index must be greater than 0") })?; Ok(stmt.parameters().name(non_zero_idx).map(|s| s.to_string())) } /// Binds a parameter at a specific 1-based index with explicit type. /// /// # Arguments /// /// * `index` - The 1-based parameter index. /// * `value_type` - The type constant (0=null, 1=int, 2=float, 3=text, 4=blob). /// * `value` - The value to bind. #[napi] pub fn bind_at(&self, index: u32, value: Unknown) -> Result<()> { let mut stmt = self.stmt.borrow_mut(); let stmt = stmt .as_mut() .ok_or_else(|| Error::new(Status::GenericFailure, "Statement has been finalized"))?; let non_zero_idx = NonZeroUsize::new(index as usize).ok_or_else(|| { Error::new(Status::InvalidArg, "Parameter index must be greater than 0") })?; let value_type = value.get_type()?; let turso_value = match value_type { ValueType::Null => turso_core::Value::Null, ValueType::Number => { let n: f64 = unsafe { value.cast()? }; if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { turso_core::Value::Integer(n as i64) } else { turso_core::Value::Float(n) } } ValueType::BigInt => { let bigint_str = value.coerce_to_string()?.into_utf8()?.as_str()?.to_owned(); let bigint_value = bigint_str.parse::().map_err(|e| { Error::new(Status::NumberExpected, format!("Failed to parse BigInt: {e}")) })?; turso_core::Value::Integer(bigint_value) } ValueType::String => { let s = value.coerce_to_string()?.into_utf8()?; turso_core::Value::Text(s.as_str()?.to_owned().into()) } ValueType::Boolean => { let b: bool = unsafe { value.cast()? }; turso_core::Value::Integer(if b { 1 } else { 0 }) } ValueType::Object => { // Try to cast as Buffer first, fallback to string conversion if let Ok(buffer) = unsafe { value.cast::() } { turso_core::Value::Blob(buffer.to_vec()) } else { let s = value.coerce_to_string()?.into_utf8()?; turso_core::Value::Text(s.as_str()?.to_owned().into()) } } _ => { // Fallback to string conversion for unknown types let s = value.coerce_to_string()?.into_utf8()?; turso_core::Value::Text(s.as_str()?.to_owned().into()) } }; stmt.bind_at(non_zero_idx, turso_value); Ok(()) } /// Step the statement and return result code: /// 1 = Row available, 2 = Done, 3 = I/O needed #[napi] 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"))?; match stmt.step() { 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) => { Err(Error::new(Status::GenericFailure, "Database is busy")) } Err(e) => Err(Error::new( Status::GenericFailure, format!("Step failed: {e}"), )), } } /// 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. #[napi] pub fn raw(&mut self, raw: Option) { self.mode = RefCell::new(match raw { Some(false) => PresentationMode::Expanded, _ => PresentationMode::Raw, }); } /// Sets the presentation mode to pluck. #[napi] pub fn pluck(&mut self, pluck: Option) { self.mode = RefCell::new(match pluck { Some(false) => PresentationMode::Expanded, _ => PresentationMode::Pluck, }); } /// Finalizes the statement. #[napi] pub fn finalize(&self) -> Result<()> { self.stmt.borrow_mut().take(); Ok(()) } } /// Async task for running the I/O loop. pub struct IoLoopTask { // this field is set in the turso-sync-engine package pub io: Arc, } impl Task for IoLoopTask { type Output = (); type JsValue = (); fn compute(&mut self) -> napi::Result { self.io.run_once().map_err(|e| { napi::Error::new(napi::Status::GenericFailure, format!("IO error: {e}")) })?; Ok(()) } fn resolve(&mut self, _env: Env, _output: Self::Output) -> napi::Result { Ok(()) } } /// Convert a Turso value to a JavaScript value. fn to_js_value<'a>(env: &'a napi::Env, value: &turso_core::Value) -> napi::Result> { match value { turso_core::Value::Null => ToNapiValue::into_unknown(Null, env), turso_core::Value::Integer(i) => ToNapiValue::into_unknown(i, env), turso_core::Value::Float(f) => ToNapiValue::into_unknown(f, env), turso_core::Value::Text(s) => ToNapiValue::into_unknown(s.as_str(), env), turso_core::Value::Blob(b) => ToNapiValue::into_unknown(b, env), } } struct DatabaseFile { file: Arc, } unsafe impl Send for DatabaseFile {} unsafe impl Sync for DatabaseFile {} impl DatabaseFile { pub fn new(file: Arc) -> Self { Self { file } } } impl turso_core::DatabaseStorage for DatabaseFile { fn read_header(&self, c: turso_core::Completion) -> turso_core::Result { self.file.pread(0, c) } fn read_page( &self, page_idx: usize, c: turso_core::Completion, ) -> turso_core::Result { let r = c.as_read(); let size = r.buf().len(); assert!(page_idx > 0); if !(512..=65536).contains(&size) || size & (size - 1) != 0 { return Err(turso_core::LimboError::NotADB); } let pos = (page_idx - 1) * size; self.file.pread(pos, c) } fn write_page( &self, page_idx: usize, buffer: Arc, c: turso_core::Completion, ) -> turso_core::Result { let size = buffer.len(); let pos = (page_idx - 1) * size; self.file.pwrite(pos, buffer, c) } fn write_pages( &self, page_idx: usize, page_size: usize, buffers: Vec>, c: turso_core::Completion, ) -> turso_core::Result { let pos = page_idx.saturating_sub(1) * page_size; let c = self.file.pwritev(pos, buffers, c)?; Ok(c) } fn sync(&self, c: turso_core::Completion) -> turso_core::Result { self.file.sync(c) } fn size(&self) -> turso_core::Result { self.file.size() } fn truncate( &self, len: usize, c: turso_core::Completion, ) -> turso_core::Result { let c = self.file.truncate(len, c)?; Ok(c) } }