lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit fe7e40a9e3eaeddc138a869277f3412c09fe1fca
parent 3c92cc8fb4b33d1aac8d4487e95b8238adcbeb4f
Author: triesap <tyson@radroots.org>
Date:   Wed, 12 Nov 2025 21:49:45 +0000

workspace: add `radroots-sql-core` crate with cross-platform sql execution

Diffstat:
MCargo.lock | 73++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
MCargo.toml | 2++
Asql-core/Cargo.toml | 23+++++++++++++++++++++++
Asql-core/src/error.rs | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asql-core/src/executor_embedded.rs | 42++++++++++++++++++++++++++++++++++++++++++
Asql-core/src/executor_sqlite.rs | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asql-core/src/executor_wasm.rs | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asql-core/src/lib.rs | 37+++++++++++++++++++++++++++++++++++++
Msql-wasm-core/Cargo.toml | 4++--
Dsql-wasm-core/src/error.rs | 38--------------------------------------
Msql-wasm-core/src/lib.rs | 1-
Msql-wasm-core/src/utils.rs | 33+++++++++++----------------------
12 files changed, 429 insertions(+), 64 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -650,6 +650,18 @@ dependencies = [ ] [[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -840,6 +852,15 @@ dependencies = [ ] [[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1223,6 +1244,17 @@ dependencies = [ ] [[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "linux-raw-sys" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1538,6 +1570,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] name = "poly1305" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1775,6 +1813,18 @@ dependencies = [ ] [[package]] +name = "radroots-sql-core" +version = "0.1.0" +dependencies = [ + "radroots-sql-wasm-bridge", + "rusqlite", + "serde-wasm-bindgen", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", +] + +[[package]] name = "radroots-sql-wasm-bridge" version = "0.1.0" dependencies = [ @@ -1788,6 +1838,7 @@ version = "0.1.0" dependencies = [ "chrono", "js-sys", + "radroots-sql-core", "radroots-sql-wasm-bridge", "serde", "serde-wasm-bindgen", @@ -2006,6 +2057,20 @@ dependencies = [ ] [[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink 0.9.1", + "libsqlite3-sys", + "smallvec", +] + +[[package]] name = "rust-ini" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2878,6 +2943,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3294,7 +3365,7 @@ checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" dependencies = [ "arraydeque", "encoding_rs", - "hashlink", + "hashlink 0.8.4", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml @@ -12,6 +12,7 @@ members = [ "runtime", "sql-wasm-bridge", "sql-wasm-core", + "sql-core", "tangle-schema", "trade", "types", @@ -37,6 +38,7 @@ radroots-net = { path = "net", version = "0.1.0", default-features = false } radroots-net-core = { path = "net-core", version = "0.1.0", default-features = false } radroots-sql-wasm-bridge = { path = "sql-wasm-bridge", version = "0.1.0" } radroots-sql-wasm-core = { path = "sql-wasm-core", version = "0.1.0" } +radroots-sql-core = { path = "sql-core", version = "0.1.0" } radroots-tangle-schema = { path = "tangle-schema", version = "0.1.0", default-features = false } radroots-trade = { path = "trade", version = "0.1.0", default-features = false } radroots-types = { path = "types", version = "0.1.0", default-features = false } diff --git a/sql-core/Cargo.toml b/sql-core/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "radroots-sql-core" +version.workspace = true +edition.workspace = true +authors = ["Radroots Authors"] +rust-version.workspace = true +license.workspace = true + +[lib] +crate-type = ["rlib"] + +[features] +web = ["dep:radroots-sql-wasm-bridge", "dep:serde-wasm-bindgen", "dep:wasm-bindgen"] +native = ["dep:rusqlite"] +embedded = [] + +[dependencies] +serde_json = { workspace = true } +thiserror = { workspace = true } +radroots-sql-wasm-bridge = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } +serde-wasm-bindgen = { workspace = true, optional = true } +rusqlite = { workspace = true, features = ["bundled"], optional = true } diff --git a/sql-core/src/error.rs b/sql-core/src/error.rs @@ -0,0 +1,55 @@ +#![cfg_attr(any(feature = "embedded", target_os = "espidf"), no_std)] + +#[cfg(any(feature = "embedded", target_os = "espidf"))] +extern crate alloc; + +use thiserror::Error; + +#[cfg(any(feature = "embedded", target_os = "espidf"))] +use alloc::string::String; + +#[derive(Error, Debug, Clone)] +pub enum SqlError { + #[error("invalid argument: {0}")] + InvalidArgument(String), + #[error("{0} not found")] + NotFound(String), + #[error("serialization error: {0}")] + SerializationError(String), + #[error("invalid query: {0}")] + InvalidQuery(String), + #[error("internal error")] + Internal, + #[error("unsupported on this platform")] + UnsupportedPlatform, +} + +impl SqlError { + pub fn code(&self) -> &'static str { + match self { + SqlError::InvalidArgument(_) => "ERR_INVALID_ARGUMENT", + SqlError::NotFound(_) => "ERR_NOT_FOUND", + SqlError::SerializationError(_) => "ERR_SERIALIZATION", + SqlError::InvalidQuery(_) => "ERR_INVALID_QUERY", + SqlError::Internal => "ERR_INTERNAL", + SqlError::UnsupportedPlatform => "ERR_UNSUPPORTED_PLATFORM", + } + } + + pub fn to_json(&self) -> serde_json::Value { + serde_json::json!({ "code": self.code(), "message": self.to_string() }) + } +} + +impl From<serde_json::Error> for SqlError { + fn from(e: serde_json::Error) -> Self { + SqlError::SerializationError(e.to_string()) + } +} + +#[cfg(feature = "native")] +impl From<rusqlite::Error> for SqlError { + fn from(e: rusqlite::Error) -> Self { + SqlError::InvalidQuery(e.to_string()) + } +} diff --git a/sql-core/src/executor_embedded.rs b/sql-core/src/executor_embedded.rs @@ -0,0 +1,42 @@ +#![cfg(any(feature = "embedded", target_os = "espidf"))] +#![no_std] + +extern crate alloc; + +use alloc::string::String; + +use crate::{ExecOutcome, SqlExecutor, error::SqlError}; + +#[derive(Clone, Debug)] +pub struct EmbeddedSqlExecutor; + +impl EmbeddedSqlExecutor { + pub fn new() -> Self { + Self + } +} + +impl SqlExecutor for EmbeddedSqlExecutor { + fn exec(&self, _sql: &str, _params_json: &str) -> Result<ExecOutcome, SqlError> { + Ok(ExecOutcome { + changes: 0, + last_insert_id: 0, + }) + } + + fn query_raw(&self, _sql: &str, _params_json: &str) -> Result<String, SqlError> { + Ok(String::from("[]")) + } + + fn begin(&self) -> Result<(), SqlError> { + Ok(()) + } + + fn commit(&self) -> Result<(), SqlError> { + Ok(()) + } + + fn rollback(&self) -> Result<(), SqlError> { + Ok(()) + } +} diff --git a/sql-core/src/executor_sqlite.rs b/sql-core/src/executor_sqlite.rs @@ -0,0 +1,130 @@ +use crate::{ExecOutcome, SqlExecutor, error::SqlError}; +use rusqlite::{Connection, Row, params_from_iter}; +use serde_json::{Map, Value}; +use std::path::Path; +use std::sync::{Arc, Mutex}; + +pub struct SqliteExecutor { + conn: Arc<Mutex<Connection>>, +} + +impl SqliteExecutor { + pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, SqlError> { + let conn = Connection::open(path).map_err(SqlError::from)?; + Ok(Self { + conn: Arc::new(Mutex::new(conn)), + }) + } + + pub fn open_memory() -> Result<Self, SqlError> { + let conn = Connection::open_in_memory().map_err(SqlError::from)?; + Ok(Self { + conn: Arc::new(Mutex::new(conn)), + }) + } + + fn parse_params(&self, params_json: &str) -> Result<Vec<rusqlite::types::Value>, SqlError> { + let vals: Vec<Value> = serde_json::from_str(params_json) + .map_err(|e| SqlError::SerializationError(e.to_string()))?; + vals.into_iter() + .map(|v| match v { + Value::Null => Ok(rusqlite::types::Value::Null), + Value::Bool(b) => Ok(rusqlite::types::Value::from(if b { 1 } else { 0 })), + Value::Number(n) => { + if let Some(i) = n.as_i64() { + Ok(rusqlite::types::Value::from(i)) + } else if let Some(u) = n.as_u64() { + Ok(rusqlite::types::Value::from(u as i64)) + } else if let Some(f) = n.as_f64() { + Ok(rusqlite::types::Value::from(f)) + } else { + Err(SqlError::InvalidArgument("unsupported number".to_string())) + } + } + Value::String(s) => Ok(rusqlite::types::Value::from(s)), + other => Err(SqlError::InvalidArgument(format!( + "unsupported bind value: {}", + other + ))), + }) + .collect() + } + + fn row_to_json(row: &Row) -> rusqlite::Result<Value> { + let stmt = row.as_ref(); + let mut obj = Map::new(); + for i in 0..stmt.column_count() { + let name = stmt.column_name(i).unwrap_or("").to_string(); + let v = row.get_ref(i)?; + let j = match v { + rusqlite::types::ValueRef::Null => Value::Null, + rusqlite::types::ValueRef::Integer(i) => Value::from(i), + rusqlite::types::ValueRef::Real(f) => Value::from(f), + rusqlite::types::ValueRef::Text(s) => { + let s = std::str::from_utf8(s).map_err(|e| { + rusqlite::Error::FromSqlConversionFailure( + i, + rusqlite::types::Type::Text, + Box::new(e), + ) + })?; + Value::from(s.to_string()) + } + rusqlite::types::ValueRef::Blob(_) => Value::Null, + }; + obj.insert(name, j); + } + Ok(Value::Object(obj)) + } +} + +impl SqlExecutor for SqliteExecutor { + fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { + let binds = self.parse_params(params_json)?; + let n = { + let conn = self.conn.lock().map_err(|_| SqlError::Internal)?; + conn.execute(sql, params_from_iter(binds.into_iter())) + .map_err(SqlError::from)? + }; + let last_insert_id = { + let conn = self.conn.lock().map_err(|_| SqlError::Internal)?; + conn.last_insert_rowid() + }; + Ok(ExecOutcome { + changes: n as i64, + last_insert_id, + }) + } + + fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { + let binds = self.parse_params(params_json)?; + let rows = { + let conn = self.conn.lock().map_err(|_| SqlError::Internal)?; + let mut stmt = conn.prepare(sql).map_err(SqlError::from)?; + let mapped = stmt.query_map(params_from_iter(binds.into_iter()), |row| { + Self::row_to_json(row) + })?; + let collected = mapped.collect::<Result<Vec<_>, _>>()?; + collected + }; + Ok(Value::from(rows).to_string()) + } + + fn begin(&self) -> Result<(), SqlError> { + let conn = self.conn.lock().map_err(|_| SqlError::Internal)?; + conn.execute("BEGIN", []).map_err(SqlError::from)?; + Ok(()) + } + + fn commit(&self) -> Result<(), SqlError> { + let conn = self.conn.lock().map_err(|_| SqlError::Internal)?; + conn.execute("COMMIT", []).map_err(SqlError::from)?; + Ok(()) + } + + fn rollback(&self) -> Result<(), SqlError> { + let conn = self.conn.lock().map_err(|_| SqlError::Internal)?; + conn.execute("ROLLBACK", []).map_err(SqlError::from)?; + Ok(()) + } +} diff --git a/sql-core/src/executor_wasm.rs b/sql-core/src/executor_wasm.rs @@ -0,0 +1,55 @@ +use crate::{ExecOutcome, SqlExecutor, error::SqlError}; + +pub struct WasmSqlExecutor; + +impl WasmSqlExecutor { + pub fn new() -> Self { + Self + } +} + +impl Default for WasmSqlExecutor { + fn default() -> Self { + Self::new() + } +} + +impl SqlExecutor for WasmSqlExecutor { + fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { + let js = radroots_sql_wasm_bridge::exec(sql, params_json); + let v: serde_json::Value = serde_wasm_bindgen::from_value(js) + .map_err(|e| SqlError::SerializationError(e.to_string()))?; + let changes = v.get("changes").and_then(|x| x.as_i64()).unwrap_or(0); + let last_insert_id = v + .get("last_insert_id") + .or_else(|| v.get("lastInsertRowid")) + .and_then(|x| x.as_i64()) + .unwrap_or(0); + Ok(ExecOutcome { + changes, + last_insert_id, + }) + } + + fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError> { + let js = radroots_sql_wasm_bridge::query(sql, params_json); + let v: serde_json::Value = serde_wasm_bindgen::from_value(js) + .map_err(|e| SqlError::SerializationError(e.to_string()))?; + Ok(v.to_string()) + } + + fn begin(&self) -> Result<(), SqlError> { + radroots_sql_wasm_bridge::begin_tx(); + Ok(()) + } + + fn commit(&self) -> Result<(), SqlError> { + radroots_sql_wasm_bridge::commit_tx(); + Ok(()) + } + + fn rollback(&self) -> Result<(), SqlError> { + radroots_sql_wasm_bridge::rollback_tx(); + Ok(()) + } +} diff --git a/sql-core/src/lib.rs b/sql-core/src/lib.rs @@ -0,0 +1,37 @@ +#![cfg_attr(any(feature = "embedded", target_os = "espidf"), no_std)] + +#[cfg(any(feature = "embedded", target_os = "espidf"))] +extern crate alloc; + +pub mod error; + +#[cfg(feature = "web")] +mod executor_wasm; +#[cfg(feature = "web")] +pub use executor_wasm::WasmSqlExecutor; + +#[cfg(feature = "native")] +mod executor_sqlite; +#[cfg(feature = "native")] +pub use executor_sqlite::SqliteExecutor; + +#[cfg(feature = "embedded")] +mod executor_embedded; +#[cfg(feature = "embedded")] +pub use executor_embedded::EmbeddedSqlExecutor; + +use error::SqlError; + +#[derive(Clone, Copy, Debug)] +pub struct ExecOutcome { + pub changes: i64, + pub last_insert_id: i64, +} + +pub trait SqlExecutor: Send + Sync { + fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError>; + fn query_raw(&self, sql: &str, params_json: &str) -> Result<String, SqlError>; + fn begin(&self) -> Result<(), SqlError>; + fn commit(&self) -> Result<(), SqlError>; + fn rollback(&self) -> Result<(), SqlError>; +} diff --git a/sql-wasm-core/Cargo.toml b/sql-wasm-core/Cargo.toml @@ -13,6 +13,7 @@ crate-type = ["cdylib", "rlib"] default = [] [dependencies] +radroots-sql-core = { workspace = true } radroots-sql-wasm-bridge = { workspace = true } chrono = { workspace = true, features = ["serde"] } serde = { workspace = true, features = ["derive"] } @@ -22,4 +23,4 @@ thiserror = { workspace = true } wasm-bindgen = { workspace = true } js-sys = { workspace = true } ts-rs = { workspace = true } -uuid = { workspace = true, features = ["v4", "fast-rng", "js"] } -\ No newline at end of file +uuid = { workspace = true, features = ["v4", "fast-rng", "js"] } diff --git a/sql-wasm-core/src/error.rs b/sql-wasm-core/src/error.rs @@ -1,38 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Debug, Clone)] -pub enum SqlWasmError { - #[error("invalid argument: {0}")] - InvalidArgument(String), - #[error("{0} not found")] - NotFound(String), - #[error("serialization error: {0}")] - SerializationError(String), - #[error("invalid query: {0}")] - InvalidQuery(String), - #[error("internal error")] - Internal, -} - -impl SqlWasmError { - pub fn code(&self) -> &'static str { - match self { - SqlWasmError::InvalidArgument(_) => "ERR_INVALID_ARGUMENT", - SqlWasmError::NotFound(_) => "ERR_NOT_FOUND", - SqlWasmError::SerializationError(_) => "ERR_SERIALIZATION", - SqlWasmError::InvalidQuery(_) => "ERR_INVALID_QUERY", - SqlWasmError::Internal => "ERR_INTERNAL", - } - } -} - -#[cfg(target_arch = "wasm32")] -impl SqlWasmError { - pub fn to_js_value(self) -> wasm_bindgen::JsValue { - let o = serde_json::json!({ - "code": self.code(), - "message": self.to_string() - }); - wasm_bindgen::JsValue::from_str(&o.to_string()) - } -} diff --git a/sql-wasm-core/src/lib.rs b/sql-wasm-core/src/lib.rs @@ -1,7 +1,6 @@ #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; -pub mod error; pub mod utils; #[cfg(target_arch = "wasm32")] diff --git a/sql-wasm-core/src/utils.rs b/sql-wasm-core/src/utils.rs @@ -1,13 +1,12 @@ use chrono::{SecondsFormat, Utc}; +use radroots_sql_core::error::SqlError; use serde::Deserialize; use serde::Serialize; use serde_json::{Map, Value}; use uuid::Uuid; -use crate::error::SqlWasmError; - -pub fn parse_json<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, SqlWasmError> { - serde_json::from_str::<T>(s).map_err(|e| SqlWasmError::SerializationError(e.to_string())) +pub fn parse_json<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, SqlError> { + serde_json::from_str::<T>(s).map_err(SqlError::from) } pub fn uuidv4() -> String { @@ -18,21 +17,19 @@ pub fn time_created_on() -> String { Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true) } -pub fn to_object_map<T: Serialize>(opts: T) -> Result<Map<String, Value>, SqlWasmError> { - let v = - serde_json::to_value(opts).map_err(|e| SqlWasmError::SerializationError(e.to_string()))?; +pub fn to_object_map<T: Serialize>(opts: T) -> Result<Map<String, Value>, SqlError> { + let v = serde_json::to_value(opts).map_err(SqlError::from)?; let obj = v .as_object() - .ok_or_else(|| SqlWasmError::SerializationError("Expected an object".to_string()))?; + .ok_or_else(|| SqlError::SerializationError("Expected an object".to_string()))?; Ok(obj.clone()) } -pub fn to_partial_object_map<T: Serialize>(opts: T) -> Result<Map<String, Value>, SqlWasmError> { - let v = - serde_json::to_value(opts).map_err(|e| SqlWasmError::SerializationError(e.to_string()))?; +pub fn to_partial_object_map<T: Serialize>(opts: T) -> Result<Map<String, Value>, SqlError> { + let v = serde_json::to_value(opts).map_err(SqlError::from)?; let obj = v .as_object() - .ok_or_else(|| SqlWasmError::SerializationError("Expected an object".to_string()))?; + .ok_or_else(|| SqlError::SerializationError("Expected an object".to_string()))?; let mut filtered = Map::new(); for (k, v) in obj.iter() { if !v.is_null() { @@ -44,13 +41,7 @@ pub fn to_partial_object_map<T: Serialize>(opts: T) -> Result<Map<String, Value> pub fn to_db_bind_value(value: &Value) -> Value { match value { - Value::Bool(b) => { - if *b { - Value::from(1) - } else { - Value::from(0) - } - } + Value::Bool(b) => Value::from(i64::from(*b)), Value::Number(n) => { if let Some(f) = n.as_f64() { Value::from(f) @@ -71,9 +62,7 @@ pub fn to_db_bind_value(value: &Value) -> Value { } } -pub fn build_where_clause_eq<T: Serialize>( - filter: &T, -) -> Result<(String, Vec<Value>), SqlWasmError> { +pub fn build_where_clause_eq<T: Serialize>(filter: &T) -> Result<(String, Vec<Value>), SqlError> { let obj = to_partial_object_map(filter)?; if obj.is_empty() { return Ok((String::new(), Vec::new()));