Files
turso/bindings/javascript/src/lib.rs
2025-08-19 16:35:44 +03:00

541 lines
17 KiB
Rust

//! 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<Arc<turso_core::Database>>,
io: Arc<dyn turso_core::IO>,
conn: Arc<turso_core::Connection>,
is_memory: bool,
is_open: Cell<bool>,
}
#[napi]
impl Database {
/// Creates a new database instance.
///
/// # Arguments
/// * `path` - The path to the database file.
#[napi(constructor)]
pub fn new(path: String) -> Result<Self> {
let is_memory = path == ":memory:";
let io: Arc<dyn turso_core::IO> = 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<Arc<turso_core::Database>>,
io: Arc<dyn turso_core::IO>,
conn: Arc<turso_core::Connection>,
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<Statement> {
let stmt = self
.conn
.prepare(&sql)
.map_err(|e| Error::new(Status::GenericFailure, format!("{e}")))?;
let column_names: Vec<std::ffi::CString> = (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<i64> {
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<i64> {
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<i64> {
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<void>")]
pub fn io_loop_async(&self) -> AsyncTask<IoLoopTask> {
let io = self.io.clone();
AsyncTask::new(IoLoopTask { io })
}
}
/// A prepared statement.
#[napi]
pub struct Statement {
stmt: RefCell<Option<turso_core::Statement>>,
column_names: Vec<std::ffi::CString>,
mode: RefCell<PresentationMode>,
}
#[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<u32> {
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<Option<String>> {
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::<i64>().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::<Buffer>() } {
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<u32> {
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<Unknown<'env>> {
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<bool>) {
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<bool>) {
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<dyn turso_core::IO>,
}
impl Task for IoLoopTask {
type Output = ();
type JsValue = ();
fn compute(&mut self) -> napi::Result<Self::Output> {
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<Self::JsValue> {
Ok(())
}
}
/// Convert a Turso value to a JavaScript value.
fn to_js_value<'a>(env: &'a napi::Env, value: &turso_core::Value) -> napi::Result<Unknown<'a>> {
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<dyn turso_core::File>,
}
unsafe impl Send for DatabaseFile {}
unsafe impl Sync for DatabaseFile {}
impl DatabaseFile {
pub fn new(file: Arc<dyn turso_core::File>) -> Self {
Self { file }
}
}
impl turso_core::DatabaseStorage for DatabaseFile {
fn read_header(&self, c: turso_core::Completion) -> turso_core::Result<turso_core::Completion> {
self.file.pread(0, c)
}
fn read_page(
&self,
page_idx: usize,
c: turso_core::Completion,
) -> turso_core::Result<turso_core::Completion> {
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<turso_core::Buffer>,
c: turso_core::Completion,
) -> turso_core::Result<turso_core::Completion> {
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<Arc<turso_core::Buffer>>,
c: turso_core::Completion,
) -> turso_core::Result<turso_core::Completion> {
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<turso_core::Completion> {
self.file.sync(c)
}
fn size(&self) -> turso_core::Result<u64> {
self.file.size()
}
fn truncate(
&self,
len: usize,
c: turso_core::Completion,
) -> turso_core::Result<turso_core::Completion> {
let c = self.file.truncate(len, c)?;
Ok(c)
}
}