diff --git a/Cargo.toml b/Cargo.toml index b2944c6..be64823 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,17 +9,25 @@ readme = "README.md" [features] default = ["all"] -all = ["timers", "url", "console"] +all = ["timers", "url", "console", "sqlite"] -timers = [] +timers = ["tokio/time"] url = [] console = [] +sqlite = ["libsqlite3-sys"] [dependencies] either = "1" log = { version = "0.4" } -rquickjs = { version = "0.6", features = ["either", "macro", "futures"] } +rquickjs = { version = "0.6", features = [ + "array-buffer", + "either", + "macro", + "futures", +] } tokio = { version = "1" } +libsqlite3-sys = { version = "0.26", optional = true } +tracing = "0.1" [dev-dependencies] futures = { version = "0.3" } diff --git a/README.md b/README.md index 3fe24e2..31a12b6 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ You should prefer to use modules from [AWS LLRT](https://github.com/awslabs/llrt | Console | ✔︎ | ✔︎⚠️ | `console` | | Timers | ✔︎ | ✔︎⚠️ | `timers` | | URL | ✔︎ | ✔︎⚠️ | `url` | +| SQlite | ⏱ | ✔︎ | `sqlite` | | Other modules | ✔︎ | ✘ | N/A | _⚠️ = partially supported in Rquickjs Extra_ diff --git a/src/ffi/c_string.rs b/src/ffi/c_string.rs new file mode 100644 index 0000000..5f326fb --- /dev/null +++ b/src/ffi/c_string.rs @@ -0,0 +1,49 @@ +use std::ffi::c_char; +use std::mem; + +use rquickjs::{qjs, Error, Result, String, Value}; + +#[derive(Debug)] +pub struct CString<'js> { + ptr: *const c_char, + len: usize, + value: Value<'js>, +} + +impl<'js> CString<'js> { + pub fn from_string(string: String<'js>) -> Result { + let mut len = mem::MaybeUninit::uninit(); + let ptr = unsafe { + qjs::JS_ToCStringLen( + string.ctx().as_raw().as_ptr(), + len.as_mut_ptr(), + string.as_raw(), + ) + }; + if ptr.is_null() { + // Might not ever happen but I am not 100% sure + // so just incase check it. + return Err(Error::Unknown); + } + let len = unsafe { len.assume_init() }; + Ok(Self { + ptr, + len, + value: string.into_value(), + }) + } + + pub fn as_ptr(&self) -> *const c_char { + self.ptr + } + + pub fn len(&self) -> usize { + self.len + } +} + +impl<'js> Drop for CString<'js> { + fn drop(&mut self) { + unsafe { qjs::JS_FreeCString(self.value.ctx().as_raw().as_ptr(), self.ptr) }; + } +} diff --git a/src/ffi/c_vec.rs b/src/ffi/c_vec.rs new file mode 100644 index 0000000..746edc0 --- /dev/null +++ b/src/ffi/c_vec.rs @@ -0,0 +1,30 @@ +use rquickjs::{Result, TypedArray, Value}; + +use crate::utils::result::ResultExt; + +#[derive(Debug)] +pub struct CVec<'js> { + ptr: *const u8, + len: usize, + #[allow(dead_code)] + value: Value<'js>, +} + +impl<'js> CVec<'js> { + pub fn from_array(array: TypedArray<'js, u8>) -> Result { + let raw = array.as_raw().or_throw(array.ctx())?; + Ok(Self { + ptr: raw.ptr.as_ptr(), + len: raw.len, + value: array.into_value(), + }) + } + + pub fn as_ptr(&self) -> *const u8 { + self.ptr + } + + pub fn len(&self) -> usize { + self.len + } +} diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs new file mode 100644 index 0000000..12c85c5 --- /dev/null +++ b/src/ffi/mod.rs @@ -0,0 +1,5 @@ +pub use self::c_string::CString; +pub use self::c_vec::CVec; + +mod c_string; +mod c_vec; diff --git a/src/lib.rs b/src/lib.rs index 1893324..e9b3ec8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ pub use self::modules::*; +mod ffi; mod modules; #[cfg(test)] mod test; +mod utils; diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 4013737..91e8cc6 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -1,5 +1,7 @@ #[cfg(feature = "console")] pub mod console; +#[cfg(feature = "sqlite")] +pub mod sqlite; #[cfg(feature = "timers")] pub mod timers; #[cfg(feature = "url")] diff --git a/src/modules/sqlite/argument.rs b/src/modules/sqlite/argument.rs new file mode 100644 index 0000000..2757359 --- /dev/null +++ b/src/modules/sqlite/argument.rs @@ -0,0 +1,67 @@ +use rquickjs::{Ctx, Exception, FromJs, Result, TypedArray, Value}; + +use crate::ffi::{CString, CVec}; +use crate::utils::result::ResultExt; + +/// Sqlite [dynamic type value](http://sqlite.org/datatype3.html). The Value's type is typically +/// dictated by SQLite (not by the caller). +#[derive(Debug)] +pub enum Argument<'js> { + Null, + Integer(i64), + Real(f64), + Text(CString<'js>), + Blob(CVec<'js>), +} + +/// SQLite data types. +/// See [Fundamental Datatypes](https://sqlite.org/c3ref/c_blob.html). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Type { + Null, + Integer, + Real, + Text, + Blob, +} + +impl<'js> Argument<'js> { + #[inline] + #[must_use] + pub fn data_type(&self) -> Type { + match *self { + Self::Null => Type::Null, + Self::Integer(_) => Type::Integer, + Self::Real(_) => Type::Real, + Self::Text(_) => Type::Text, + Self::Blob { .. } => Type::Blob, + } + } +} + +impl<'js> FromJs<'js> for Argument<'js> { + fn from_js(ctx: &Ctx<'js>, value: rquickjs::Value<'js>) -> Result { + if value.is_undefined() || value.is_null() { + return Ok(Argument::Null); + } else if let Some(int) = value.as_int() { + return Ok(Argument::Integer(int as i64)); + } else if let Some(big_int) = value.as_big_int() { + return Ok(Argument::Integer(big_int.clone().to_i64()?)); + } else if let Some(float) = value.as_float() { + return Ok(Argument::Real(float)); + } else if let Some(string) = value.as_string() { + return Ok(Argument::Text(CString::from_string(string.clone())?)); + } else if let Some(object) = value.as_object() { + if object.as_typed_array::().is_some() { + // Lifetime issue: https://github.com/DelSkayn/rquickjs/issues/356 + return Ok(Argument::Blob(CVec::from_array( + TypedArray::::from_value(value.clone()).or_throw(ctx)?, + )?)); + } + } + Err(Exception::throw_type( + ctx, + &["Value of type '", value.type_name(), "' is not supported"].concat(), + )) + } +} diff --git a/src/modules/sqlite/database.rs b/src/modules/sqlite/database.rs new file mode 100644 index 0000000..4ebed47 --- /dev/null +++ b/src/modules/sqlite/database.rs @@ -0,0 +1,128 @@ +use rquickjs::{function::Opt, Ctx, Exception, FromJs, Object, Result, Value}; + +use super::{utils, DatabaseRaw}; +use crate::utils::result::ResultExt; + +#[rquickjs::class] +#[derive(rquickjs::class::Trace)] +pub struct Database { + #[qjs(skip_trace)] + location: String, + #[qjs(skip_trace)] + raw: Option, +} + +impl Database { + fn raw(&self, ctx: &Ctx<'_>) -> Result<&DatabaseRaw> { + self.raw + .as_ref() + .ok_or_else(|| Exception::throw_message(ctx, "Database is not open")) + } + + async fn open_(ctx: &Ctx<'_>, location: &str) -> Result { + let location = location.to_owned(); + let raw = utils::asyncify(ctx, move || DatabaseRaw::open(&location)).await?; + Ok(raw) + } +} + +#[rquickjs::methods(rename_all = "camelCase")] +impl Database { + #[qjs(constructor)] + pub fn new(location: String, options: Opt) -> Self { + Self { + location, + raw: None, + } + } + + async fn open(&mut self, ctx: Ctx<'_>) -> rquickjs::Result<()> { + self.raw = Some(Self::open_(&ctx, &self.location).await?); + Ok(()) + } + + async fn prepare(&self, sql: String) -> Result<()> { + todo!() + } + + async fn exec(&self, ctx: Ctx<'_>, sql: String) -> Result<()> { + let raw = self.raw(&ctx)?.clone(); + let sql = sql.to_owned(); + utils::asyncify(&ctx, move || { + raw.execute(&sql)?; + Ok(()) + }) + .await?; + Ok(()) + } + + async fn close(&mut self, ctx: Ctx<'_>) -> Result<()> { + match self.raw.take() { + Some(raw) => { + utils::asyncify(&ctx, move || raw.close()).await?; + Ok(()) + } + None => Err(Exception::throw_message( + &ctx, + "Connection is already closed", + )), + } + } +} + +pub struct ConnectionOptions {} + +impl Default for ConnectionOptions { + fn default() -> Self { + Self {} + } +} + +impl<'js> FromJs<'js> for ConnectionOptions { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let obj = value.get::>()?; + Ok(Self {}) + } +} + +#[cfg(test)] +mod tests { + use rquickjs::CatchResultExt; + + use crate::sqlite::SqliteModule; + use crate::test::{call_test, test_async_with, ModuleEvaluator}; + + #[tokio::test] + async fn test_open_exec() { + test_async_with(|ctx| { + Box::pin(async move { + ModuleEvaluator::eval_rust::(ctx.clone(), "sqlite") + .await + .unwrap(); + + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test", + r#" + import { Database } from "sqlite"; + + export async function test() { + const db = new Database(":memory:"); + await db.open(); + await db.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT);"); + await db.exec("INSERT INTO test (name) VALUES ('test');"); + return "ok"; + } + "#, + ) + .await + .catch(&ctx) + .unwrap(); + + let result = call_test::(&ctx, &module, ()).await; + assert_eq!(result, "ok"); + }) + }) + .await; + } +} diff --git a/src/modules/sqlite/database_raw.rs b/src/modules/sqlite/database_raw.rs new file mode 100644 index 0000000..25a05a4 --- /dev/null +++ b/src/modules/sqlite/database_raw.rs @@ -0,0 +1,190 @@ +use std::{os::raw::c_int, ptr}; + +use super::{error, ffi, utils, StatementRaw}; + +#[derive(Clone, Copy, Debug)] +pub struct OpenFlags(c_int); + +impl OpenFlags { + #[inline] + pub fn new() -> Self { + OpenFlags(0) + } + + pub fn with_create(mut self) -> Self { + self.0 |= ffi::SQLITE_OPEN_CREATE; + self + } + + pub fn with_full_mutex(mut self) -> Self { + self.0 |= ffi::SQLITE_OPEN_FULLMUTEX; + self + } + + pub fn with_read_write(mut self) -> Self { + self.0 |= ffi::SQLITE_OPEN_READWRITE; + self + } + + pub fn with_uri(mut self) -> Self { + self.0 |= ffi::SQLITE_OPEN_URI; + self + } + + pub fn with_extended_result_code(mut self) -> Self { + self.0 |= ffi::SQLITE_OPEN_URI; + self + } +} + +impl Default for OpenFlags { + #[inline] + fn default() -> Self { + Self::new() + .with_create() + .with_full_mutex() + .with_read_write() + .with_uri() + } +} + +/// A connection to a SQLite database. +/// We make a few assumptions about the SQLite library: +/// - SQlite was compiled in serialized mode so that we can safely share the connection +/// and related objects between threads. +/// - SQlite is a recent version that supports extended result codes. +pub struct DatabaseRaw { + db: *mut ffi::sqlite3, + owned: bool, +} + +impl DatabaseRaw { + pub unsafe fn new(db: *mut ffi::sqlite3, owned: bool) -> Self { + Self { db, owned } + } + + pub fn open(path: &str) -> error::Result { + Self::open_with_flags(path, OpenFlags::default()) + } + + pub fn open_with_flags(path: &str, mut flags: OpenFlags) -> error::Result { + let c_path = utils::str_to_cstring(path); + + flags = flags.with_extended_result_code(); + + unsafe { + let mut db: *mut ffi::sqlite3 = ptr::null_mut(); + let r = ffi::sqlite3_open_v2(c_path.as_ptr(), &mut db, flags.0, std::ptr::null()); + if r != ffi::SQLITE_OK { + let e = if db.is_null() { + error::error_from_code(r, Some(c_path.to_string_lossy().to_string())) + } else { + let mut e = error::error_from_handle(db, r); + if e.inner.code == ffi::ErrorCode::CannotOpen { + e.message = Some(format!( + "{}: {}", + e.message.unwrap_or_else(|| "Cannot open".to_string()), + c_path.to_string_lossy() + )); + } + ffi::sqlite3_close(db); + e + }; + + return Err(e); + } + + // Attempt to turn on extended results code. Don't fail if we can't. + ffi::sqlite3_extended_result_codes(db, 1); + + // Set busy timeout to 5 seconds + let r = ffi::sqlite3_busy_timeout(db, 5000); + if r != ffi::SQLITE_OK { + let e = error::error_from_handle(db, r); + ffi::sqlite3_close(db); + return Err(e); + } + + Ok(Self::new(db, true)) + } + } + + pub fn decode_result(&self, code: c_int) -> error::Result<()> { + unsafe { Self::decode_result_raw(self.db, code) } + } + + #[inline] + unsafe fn decode_result_raw(db: *mut ffi::sqlite3, code: c_int) -> error::Result<()> { + if code == ffi::SQLITE_OK { + Ok(()) + } else { + Err(error::error_from_handle(db, code)) + } + } + + pub fn close(mut self) -> error::Result<()> { + self.close_() + } + + fn close_(&mut self) -> error::Result<()> { + if self.db.is_null() { + return Ok(()); + } + if !self.owned { + self.db = ptr::null_mut(); + return Ok(()); + } + unsafe { + let r = ffi::sqlite3_close_v2(self.db); + let r = Self::decode_result_raw(self.db, r); + if r.is_ok() { + self.db = ptr::null_mut(); + } + r + } + } + + pub fn execute(&self, sql: &str) -> error::Result<()> { + let c_sql = utils::str_to_cstring(sql); + unsafe { + let r = ffi::sqlite3_exec( + self.db, + c_sql.as_ptr(), + None, + std::ptr::null_mut(), + std::ptr::null_mut(), + ); + Self::decode_result_raw(self.db, r) + } + } + + pub fn prepare(&mut self, sql: &str) -> error::Result { + let mut c_stmt: *mut ffi::sqlite3_stmt = ptr::null_mut(); + let c_sql = utils::str_to_cstring(sql); + unsafe { + let r = + ffi::sqlite3_prepare_v2(self.db, c_sql.as_ptr(), -1, &mut c_stmt, ptr::null_mut()); + Self::decode_result_raw(self.db, r)?; + Ok(StatementRaw::new(c_stmt, self.clone())) + } + } +} + +unsafe impl Send for DatabaseRaw {} + +impl Clone for DatabaseRaw { + fn clone(&self) -> Self { + Self { + db: self.db, + owned: false, + } + } +} + +impl Drop for DatabaseRaw { + fn drop(&mut self) { + if let Err(err) = self.close_() { + tracing::error!("Error closing SQLite connection: {}", err); + } + } +} diff --git a/src/modules/sqlite/database_sync.rs b/src/modules/sqlite/database_sync.rs new file mode 100644 index 0000000..de2a3ce --- /dev/null +++ b/src/modules/sqlite/database_sync.rs @@ -0,0 +1,129 @@ +use rquickjs::{function::Opt, Ctx, Exception, FromJs, Object, Result, Value}; + +use super::DatabaseRaw; +use crate::utils::result::ResultExt; + +#[rquickjs::class] +#[derive(rquickjs::class::Trace)] +pub struct DatabaseSync { + #[qjs(skip_trace)] + location: String, + #[qjs(skip_trace)] + raw: Option, +} + +impl DatabaseSync { + fn raw(&self, ctx: &Ctx<'_>) -> Result<&DatabaseRaw> { + self.raw + .as_ref() + .ok_or_else(|| Exception::throw_message(ctx, "Database is not open")) + } + + fn open_(ctx: &Ctx<'_>, location: &str) -> Result { + DatabaseRaw::open(location).or_throw(ctx) + } + + fn prepare_(ctx: Ctx<'_>, db: DatabaseRaw, sql: &str) -> Result<()> { + Ok(()) + } +} + +#[rquickjs::methods(rename_all = "camelCase")] +impl DatabaseSync { + #[qjs(constructor)] + pub fn new(ctx: Ctx<'_>, location: String, options: Opt) -> Result { + let options = options.0.unwrap_or_default(); + + let raw = if options.open { + Some(Self::open_(&ctx, &location)?) + } else { + None + }; + + Ok(Self { location, raw }) + } + + fn open(&mut self, ctx: Ctx<'_>) -> rquickjs::Result<()> { + self.raw = Some(Self::open_(&ctx, &self.location)?); + Ok(()) + } + + fn prepare(&self, sql: String) -> Result<()> { + todo!() + } + + fn exec(&self, ctx: Ctx<'_>, sql: String) -> Result<()> { + let raw = self.raw(&ctx)?; + raw.execute(&sql).or_throw(&ctx) + } + + fn close(&mut self, ctx: Ctx<'_>) -> Result<()> { + match self.raw.take() { + Some(raw) => raw.close().or_throw(&ctx), + None => Err(Exception::throw_message( + &ctx, + "Connection is already closed", + )), + } + } +} + +pub struct ConnectionOptions { + open: bool, +} + +impl Default for ConnectionOptions { + fn default() -> Self { + Self { open: true } + } +} + +impl<'js> FromJs<'js> for ConnectionOptions { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let obj = value.get::>()?; + Ok(Self { + open: obj.get("open").unwrap_or(true), + }) + } +} + +#[cfg(test)] +mod tests { + use rquickjs::CatchResultExt; + + use crate::sqlite::SqliteModule; + use crate::test::{call_test, test_async_with, ModuleEvaluator}; + + #[tokio::test] + async fn test_open_exec() { + test_async_with(|ctx| { + Box::pin(async move { + ModuleEvaluator::eval_rust::(ctx.clone(), "sqlite") + .await + .unwrap(); + + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test", + r#" + import { DatabaseSync } from "sqlite"; + + export function test() { + const db = new DatabaseSync(":memory:"); + db.exec("CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT);"); + db.exec("INSERT INTO test (name) VALUES ('test');"); + return "ok"; + } + "#, + ) + .await + .catch(&ctx) + .unwrap(); + + let result = call_test::(&ctx, &module, ()).await; + assert_eq!(result, "ok"); + }) + }) + .await; + } +} diff --git a/src/modules/sqlite/error.rs b/src/modules/sqlite/error.rs new file mode 100644 index 0000000..ade5f8f --- /dev/null +++ b/src/modules/sqlite/error.rs @@ -0,0 +1,44 @@ +use std::{error, ffi::c_int, fmt}; + +use super::{ffi, utils}; + +#[derive(Debug)] +pub struct Error { + pub inner: ffi::Error, + pub message: Option, +} + +impl fmt::Display for Error { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + self.inner.fmt(formatter) + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + Some(&self.inner) + } +} + +#[cold] +pub fn error_from_code(code: c_int, message: Option) -> Error { + Error { + inner: ffi::Error::new(code), + message, + } +} + +#[cold] +pub unsafe fn error_from_handle(db: *mut ffi::sqlite3, code: c_int) -> Error { + let message = if db.is_null() { + None + } else { + Some(utils::c_str_to_string(ffi::sqlite3_errmsg(db))) + }; + Error { + inner: ffi::Error::new(code), + message, + } +} + +pub type Result = std::result::Result; diff --git a/src/modules/sqlite/ffi.rs b/src/modules/sqlite/ffi.rs new file mode 100644 index 0000000..6e2f671 --- /dev/null +++ b/src/modules/sqlite/ffi.rs @@ -0,0 +1,9 @@ +pub use libsqlite3_sys::*; + +// Not exposed by libsqlite3_sys since they consider they don't +// need it, but we do are in a GC context and we can't easily +// predict if a statement will still be alive when the connection +// is closed or recycled. +extern "C" { + pub fn sqlite3_close_v2(arg1: *mut sqlite3) -> ::std::os::raw::c_int; +} diff --git a/src/modules/sqlite/mod.rs b/src/modules/sqlite/mod.rs new file mode 100644 index 0000000..4934a5c --- /dev/null +++ b/src/modules/sqlite/mod.rs @@ -0,0 +1,44 @@ +use rquickjs::{ + module::{Declarations, Exports, ModuleDef}, + Class, Ctx, Result, +}; + +use self::argument::Argument; +use self::database::Database; +use self::database_raw::DatabaseRaw; +use self::database_sync::DatabaseSync; +use self::statement_raw::StatementRaw; +use crate::utils::module::export_default; + +mod argument; +mod database; +mod database_raw; +mod database_sync; +mod error; +mod ffi; +mod statement; +mod statement_raw; +mod statement_sync; +mod utils; + +pub struct SqliteModule; + +impl ModuleDef for SqliteModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare(stringify!(Database))?; + declare.declare(stringify!(DatabaseSync))?; + declare.declare("default")?; + + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + export_default(ctx, exports, |default| { + Class::::define(default)?; + Class::::define(default)?; + + Ok(()) + })?; + Ok(()) + } +} diff --git a/src/modules/sqlite/statement.rs b/src/modules/sqlite/statement.rs new file mode 100644 index 0000000..b90652d --- /dev/null +++ b/src/modules/sqlite/statement.rs @@ -0,0 +1,14 @@ +use std::{ + cell::{Ref, RefCell}, + rc::Rc, +}; + +#[rquickjs::class] +#[derive(rquickjs::class::Trace)] +pub struct Statement {} + +impl Statement { + pub fn new() -> Self { + todo!() + } +} diff --git a/src/modules/sqlite/statement_raw.rs b/src/modules/sqlite/statement_raw.rs new file mode 100644 index 0000000..f3ee5bd --- /dev/null +++ b/src/modules/sqlite/statement_raw.rs @@ -0,0 +1,78 @@ +use std::{ + ffi::{c_int, c_void}, + ptr, +}; + +use super::{error, ffi, utils, Argument, DatabaseRaw}; + +pub struct StatementRaw { + stmt: *mut ffi::sqlite3_stmt, + db: DatabaseRaw, +} + +impl StatementRaw { + pub fn new(stmt: *mut ffi::sqlite3_stmt, db: DatabaseRaw) -> Self { + Self { stmt, db } + } + + #[inline] + pub fn step(&self) -> error::Result<()> { + let r = unsafe { ffi::sqlite3_step(self.stmt) }; + self.db.decode_result(r) + } + + #[inline] + pub fn reset(&mut self) -> error::Result<()> { + let r = unsafe { ffi::sqlite3_reset(self.stmt) }; + self.db.decode_result(r) + } + + #[inline] + pub fn finalize(mut self) -> error::Result<()> { + self.finalize_() + } + + #[inline] + fn finalize_(&mut self) -> error::Result<()> { + let r = unsafe { ffi::sqlite3_finalize(self.stmt) }; + self.stmt = ptr::null_mut(); + self.db.decode_result(r) + } + + pub fn bind(&self, index: usize, value: Argument) -> error::Result<()> { + let r = match value { + Argument::Null => unsafe { ffi::sqlite3_bind_null(self.stmt, index as c_int) }, + Argument::Integer(i) => unsafe { + ffi::sqlite3_bind_int64(self.stmt, index as c_int, i) + }, + Argument::Real(r) => unsafe { ffi::sqlite3_bind_double(self.stmt, index as c_int, r) }, + Argument::Text(s) => unsafe { + let len = utils::len_as_c_int(s.len())?; + ffi::sqlite3_bind_text( + self.stmt, + index as c_int, + s.as_ptr(), + len, + ffi::SQLITE_TRANSIENT(), + ) + }, + Argument::Blob(b) => unsafe { + let length = utils::len_as_c_int(b.len())?; + ffi::sqlite3_bind_blob( + self.stmt, + index as c_int, + b.as_ptr().cast::(), + length, + ffi::SQLITE_TRANSIENT(), + ) + }, + }; + self.db.decode_result(r) + } +} + +impl Drop for StatementRaw { + fn drop(&mut self) { + let _ = self.finalize_(); + } +} diff --git a/src/modules/sqlite/statement_sync.rs b/src/modules/sqlite/statement_sync.rs new file mode 100644 index 0000000..c4a3e68 --- /dev/null +++ b/src/modules/sqlite/statement_sync.rs @@ -0,0 +1,68 @@ +use std::{ + cell::{Ref, RefCell}, + rc::Rc, +}; + +use crate::utils::result::ResultExt; +use either::Either; +use rquickjs::{ + function::{Opt, Rest}, + Ctx, Exception, Object, Result, +}; + +use super::{Argument, StatementRaw}; + +#[rquickjs::class] +#[derive(rquickjs::class::Trace)] +pub struct StatementSync { + #[qjs(skip_trace)] + raw: Option, +} + +impl StatementSync { + pub fn new() -> Self { + todo!() + } + + fn raw(&self, ctx: &Ctx<'_>) -> Result<&StatementRaw> { + self.raw + .as_ref() + .ok_or_else(|| Exception::throw_message(ctx, "Statement has been finalized")) + } +} + +#[rquickjs::methods(rename_all = "camelCase")] +impl StatementSync { + fn get<'js>( + &self, + ctx: Ctx<'js>, + named_or_anon_params: Either, Object<'js>>, + anon_params: Rest>, + ) -> Result>> { + let raw = self.raw(&ctx)?; + match named_or_anon_params { + Either::Left(value) => { + let mut index = 1; + raw.bind(index, value).or_throw(&ctx)?; + for value in anon_params.0 { + index += 1; + raw.bind(index, value).or_throw(&ctx)?; + } + } + Either::Right(obj) => { + todo!() + } + } + todo!() + } + + fn finalize(&mut self, ctx: Ctx<'_>) -> Result<()> { + match self.raw.take() { + Some(raw) => raw.finalize().or_throw(&ctx), + None => Err(Exception::throw_message( + &ctx, + "Statement is already finalized", + )), + } + } +} diff --git a/src/modules/sqlite/utils.rs b/src/modules/sqlite/utils.rs new file mode 100644 index 0000000..6d58b2a --- /dev/null +++ b/src/modules/sqlite/utils.rs @@ -0,0 +1,45 @@ +use std::ffi::{c_char, c_int, CStr, CString}; + +use crate::utils::result::ResultExt; + +use super::{error, ffi}; + +#[inline] +pub unsafe fn c_str_to_string(str: *const c_char) -> String { + CStr::from_ptr(str).to_string_lossy().into_owned() +} + +#[inline] +pub fn str_to_cstring(str: &str) -> CString { + CString::new(str).expect("Failed to convert string to CString") +} + +#[inline] +pub fn str_to_c_char(str: &str) -> error::Result<(*const c_char, c_int)> { + let len = len_as_c_int(str.len())?; + let ptr = str.as_ptr().cast::(); + Ok((ptr, len)) +} + +pub fn len_as_c_int(len: usize) -> error::Result { + if len >= (c_int::MAX as usize) { + Err(error::Error { + inner: ffi::Error::new(ffi::SQLITE_TOOBIG), + message: None, + }) + } else { + Ok(len as c_int) + } +} + +#[inline] +pub async fn asyncify<'js, F, R>(ctx: &rquickjs::Ctx<'js>, f: F) -> rquickjs::Result +where + F: FnOnce() -> error::Result + Send + 'static, + R: Send + 'static, +{ + tokio::task::spawn_blocking(f) + .await + .or_throw_msg(ctx, "Cannot send task to worker pool")? + .or_throw(ctx) +} diff --git a/src/modules/sqlite/value.rs b/src/modules/sqlite/value.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/test.rs b/src/test.rs index 082cd5f..5af1675 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,4 +1,10 @@ -use rquickjs::{async_with, AsyncContext, AsyncRuntime, Ctx}; +use rquickjs::{ + async_with, + function::IntoArgs, + module::{Evaluated, ModuleDef}, + promise::MaybePromise, + AsyncContext, AsyncRuntime, CatchResultExt, CaughtError, Ctx, FromJs, Function, Module, Result, +}; pub fn test_with(func: F) where @@ -22,3 +28,53 @@ where }) .await; } + +pub async fn call_test<'js, T, A>(ctx: &Ctx<'js>, module: &Module<'js, Evaluated>, args: A) -> T +where + T: FromJs<'js>, + A: IntoArgs<'js>, +{ + call_test_err(ctx, module, args).await.unwrap() +} + +pub async fn call_test_err<'js, T, A>( + ctx: &Ctx<'js>, + module: &Module<'js, Evaluated>, + args: A, +) -> std::result::Result> +where + T: FromJs<'js>, + A: IntoArgs<'js>, +{ + module + .get::<_, Function>("test") + .catch(ctx)? + .call::<_, MaybePromise>(args) + .catch(ctx)? + .into_future::() + .await + .catch(ctx) +} + +pub struct ModuleEvaluator; + +impl ModuleEvaluator { + pub async fn eval_js<'js>( + ctx: Ctx<'js>, + name: &str, + source: &str, + ) -> Result> { + let (module, module_eval) = Module::declare(ctx, name, source)?.eval()?; + module_eval.into_future::<()>().await?; + Ok(module) + } + + pub async fn eval_rust<'js, M>(ctx: Ctx<'js>, name: &str) -> Result> + where + M: ModuleDef, + { + let (module, module_eval) = Module::evaluate_def::(ctx, name)?; + module_eval.into_future::<()>().await?; + Ok(module) + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..a5fc50c --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod module; +pub mod result; diff --git a/src/utils/module.rs b/src/utils/module.rs new file mode 100644 index 0000000..846dc06 --- /dev/null +++ b/src/utils/module.rs @@ -0,0 +1,22 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Source: https://github.com/awslabs/llrt/blob/07eb540a204dcdce44143220876630804f381ca6/llrt_utils/src/module.rs +use rquickjs::{module::Exports, Ctx, Object, Result, Value}; + +pub fn export_default<'js, F>(ctx: &Ctx<'js>, exports: &Exports<'js>, f: F) -> Result<()> +where + F: FnOnce(&Object<'js>) -> Result<()>, +{ + let default = Object::new(ctx.clone())?; + f(&default)?; + + for name in default.keys::() { + let name = name?; + let value: Value = default.get(&name)?; + exports.export(name, value)?; + } + + exports.export("default", default)?; + + Ok(()) +} diff --git a/src/utils/result.rs b/src/utils/result.rs new file mode 100644 index 0000000..c787463 --- /dev/null +++ b/src/utils/result.rs @@ -0,0 +1,71 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// Source: https://github.com/awslabs/llrt/blob/07eb540a204dcdce44143220876630804f381ca6/llrt_utils/src/result.rs +use std::{fmt::Write, result::Result as StdResult}; + +use rquickjs::{Ctx, Exception, Result}; + +pub trait ResultExt { + fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result; + fn or_throw_range(self, ctx: &Ctx, msg: Option<&str>) -> Result; + fn or_throw_type(self, ctx: &Ctx, msg: Option<&str>) -> Result; + fn or_throw(self, ctx: &Ctx) -> Result; +} + +impl ResultExt for StdResult { + fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result { + self.map_err(|e| { + let mut message = String::with_capacity(100); + message.push_str(msg); + message.push_str(". "); + write!(message, "{}", e).unwrap(); + Exception::throw_message(ctx, &message) + }) + } + + fn or_throw_range(self, ctx: &Ctx, msg: Option<&str>) -> Result { + self.map_err(|e| { + let mut message = String::with_capacity(100); + if let Some(msg) = msg { + message.push_str(msg); + message.push_str(". "); + } + write!(message, "{}", e).unwrap(); + Exception::throw_range(ctx, &message) + }) + } + + fn or_throw_type(self, ctx: &Ctx, msg: Option<&str>) -> Result { + self.map_err(|e| { + let mut message = String::with_capacity(100); + if let Some(msg) = msg { + message.push_str(msg); + message.push_str(". "); + } + write!(message, "{}", e).unwrap(); + Exception::throw_type(ctx, &message) + }) + } + + fn or_throw(self, ctx: &Ctx) -> Result { + self.map_err(|err| Exception::throw_message(ctx, &err.to_string())) + } +} + +impl ResultExt for Option { + fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result { + self.ok_or_else(|| Exception::throw_message(ctx, msg)) + } + + fn or_throw_range(self, ctx: &Ctx, msg: Option<&str>) -> Result { + self.ok_or_else(|| Exception::throw_range(ctx, msg.unwrap_or(""))) + } + + fn or_throw_type(self, ctx: &Ctx, msg: Option<&str>) -> Result { + self.ok_or_else(|| Exception::throw_type(ctx, msg.unwrap_or(""))) + } + + fn or_throw(self, ctx: &Ctx) -> Result { + self.ok_or_else(|| Exception::throw_message(ctx, "Value is not present")) + } +}