lib

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

commit 5449ef4f2e1d4663f4352016a342f2677f0fc925
parent fe7e40a9e3eaeddc138a869277f3412c09fe1fca
Author: triesap <tyson@radroots.org>
Date:   Thu, 13 Nov 2025 00:20:44 +0000

workspace: integrate serialization, uuid, and time utilities into the SQL core

Diffstat:
MCargo.lock | 3+++
Msql-core/Cargo.toml | 3+++
Msql-core/src/executor_sqlite.rs | 14+++++---------
Msql-core/src/lib.rs | 3+++
Asql-core/src/utils.rs | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msql-wasm-core/src/lib.rs | 25++++++++++++++++++++++++-
Dsql-wasm-core/src/utils.rs | 116-------------------------------------------------------------------------------
7 files changed, 193 insertions(+), 126 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1816,11 +1816,14 @@ dependencies = [ name = "radroots-sql-core" version = "0.1.0" dependencies = [ + "chrono", "radroots-sql-wasm-bridge", "rusqlite", + "serde", "serde-wasm-bindgen", "serde_json", "thiserror 1.0.69", + "uuid", "wasm-bindgen", ] diff --git a/sql-core/Cargo.toml b/sql-core/Cargo.toml @@ -21,3 +21,6 @@ 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 } +chrono = { workspace = true } +serde = { workspace = true } +uuid = { workspace = true } diff --git a/sql-core/src/executor_sqlite.rs b/sql-core/src/executor_sqlite.rs @@ -81,15 +81,11 @@ impl SqliteExecutor { 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() - }; + let mut conn = self.conn.lock().map_err(|_| SqlError::Internal)?; + let n = conn + .execute(sql, params_from_iter(binds.into_iter())) + .map_err(SqlError::from)?; + let last_insert_id = conn.last_insert_rowid(); Ok(ExecOutcome { changes: n as i64, last_insert_id, diff --git a/sql-core/src/lib.rs b/sql-core/src/lib.rs @@ -20,6 +20,9 @@ mod executor_embedded; #[cfg(feature = "embedded")] pub use executor_embedded::EmbeddedSqlExecutor; +#[cfg(not(any(feature = "embedded", target_os = "espidf")))] +pub mod utils; + use error::SqlError; #[derive(Clone, Copy, Debug)] diff --git a/sql-core/src/utils.rs b/sql-core/src/utils.rs @@ -0,0 +1,155 @@ +use chrono::{SecondsFormat, Utc}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use uuid::Uuid; + +use crate::error::SqlError; + +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 { + Uuid::new_v4().to_string() +} + +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>, SqlError> { + let v = serde_json::to_value(opts).map_err(SqlError::from)?; + let obj = v + .as_object() + .ok_or_else(|| SqlError::SerializationError(String::from("Expected an object")))?; + Ok(obj.clone()) +} + +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(|| SqlError::SerializationError(String::from("Expected an object")))?; + let mut filtered = Map::new(); + for (k, v) in obj.iter() { + if !v.is_null() { + filtered.insert(k.clone(), v.clone()); + } + } + Ok(filtered) +} + +pub fn to_db_bind_value(value: &Value) -> Value { + match value { + Value::Bool(b) => Value::from(i64::from(*b)), + Value::Number(n) => { + if let Some(f) = n.as_f64() { + Value::from(f) + } else if let Some(i) = n.as_i64() { + Value::from(i) + } else if let Some(u) = n.as_u64() { + if u <= u32::MAX as u64 { + Value::from(u as u32) + } else { + Value::from(u) + } + } else { + Value::Null + } + } + Value::String(s) => Value::from(s.clone()), + _ => Value::Null, + } +} + +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())); + } + let mut clauses = Vec::with_capacity(obj.len()); + let mut binds = Vec::with_capacity(obj.len()); + for (k, v) in obj { + clauses.push(format!("{k} = ?")); + binds.push(to_db_bind_value(&v)); + } + Ok((format!(" WHERE {}", clauses.join(" AND ")), binds)) +} + +pub fn build_insert_query_with_meta( + table: &str, + meta: &[(&str, Value)], + fields: &Map<String, Value>, +) -> (String, Vec<Value>) { + let mut cols: Vec<String> = meta.iter().map(|(k, _)| k.to_string()).collect(); + cols.extend(fields.keys().cloned()); + let meta_binds: Vec<Value> = meta.iter().map(|(_, v)| to_db_bind_value(v)).collect(); + let field_binds: Vec<Value> = fields.values().map(to_db_bind_value).collect(); + let placeholders = (0..cols.len()) + .map(|_| "?") + .collect::<Vec<&str>>() + .join(","); + let sql = format!( + "INSERT INTO {table} ({}) VALUES ({});", + cols.join(","), + placeholders + ); + let mut binds = Vec::with_capacity(cols.len()); + binds.extend(meta_binds); + binds.extend(field_binds); + (sql, binds) +} + +pub fn build_select_query_with_meta<T: Serialize>( + table: &str, + filter: Option<&T>, +) -> (String, Vec<Value>) { + let (where_clause, binds) = match filter { + Some(f) => match build_where_clause_eq(f) { + Ok(t) => t, + Err(_) => (String::new(), Vec::new()), + }, + None => (String::new(), Vec::new()), + }; + let sql = format!("SELECT * FROM {table}{where_clause};"); + (sql, binds) +} + +pub fn parse_query_value(v: &Value) -> Result<Value, SqlError> { + Ok(match v { + Value::Bool(b) => { + if *b { + serde_json::json!(1) + } else { + serde_json::json!(0) + } + } + Value::Null => Value::Null, + Value::Number(_) | Value::String(_) => v.clone(), + other => { + return Err(SqlError::InvalidArgument(other.to_string())); + } + }) +} + +pub fn to_params_json<T: Serialize>(v: T) -> Result<String, SqlError> { + serde_json::to_string(&v).map_err(SqlError::from) +} + +pub fn with_transaction<E, F, T>(exec: &E, f: F) -> Result<T, SqlError> +where + E: crate::SqlExecutor, + F: FnOnce() -> Result<T, SqlError>, +{ + exec.begin()?; + match f() { + Ok(v) => { + exec.commit()?; + Ok(v) + } + Err(e) => { + let _ = exec.rollback(); + Err(e) + } + } +} diff --git a/sql-wasm-core/src/lib.rs b/sql-wasm-core/src/lib.rs @@ -1,7 +1,30 @@ #[cfg(target_arch = "wasm32")] +use radroots_sql_core::error::SqlError; + +#[cfg(target_arch = "wasm32")] +use radroots_sql_core::utils; + +#[cfg(target_arch = "wasm32")] +use serde::de::DeserializeOwned; + +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsValue; +#[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; -pub mod utils; +#[cfg(target_arch = "wasm32")] +pub fn parse_json<T: DeserializeOwned>(s: &str) -> Result<T, SqlError> { + utils::parse_json(s) +} + +#[cfg(target_arch = "wasm32")] +pub fn err_js(err: SqlError) -> JsValue { + let value = err.to_json(); + match serde_wasm_bindgen::to_value(&value) { + Ok(v) => v, + Err(_) => JsValue::from_str(&err.to_string()), + } +} #[cfg(target_arch = "wasm32")] #[wasm_bindgen(js_name = exec_sql)] diff --git a/sql-wasm-core/src/utils.rs b/sql-wasm-core/src/utils.rs @@ -1,116 +0,0 @@ -use chrono::{SecondsFormat, Utc}; -use radroots_sql_core::error::SqlError; -use serde::Deserialize; -use serde::Serialize; -use serde_json::{Map, Value}; -use uuid::Uuid; - -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 { - Uuid::new_v4().to_string() -} - -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>, SqlError> { - let v = serde_json::to_value(opts).map_err(SqlError::from)?; - let obj = v - .as_object() - .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>, SqlError> { - let v = serde_json::to_value(opts).map_err(SqlError::from)?; - let obj = v - .as_object() - .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() { - filtered.insert(k.clone(), v.clone()); - } - } - Ok(filtered) -} - -pub fn to_db_bind_value(value: &Value) -> Value { - match value { - Value::Bool(b) => Value::from(i64::from(*b)), - Value::Number(n) => { - if let Some(f) = n.as_f64() { - Value::from(f) - } else if let Some(i) = n.as_i64() { - Value::from(i) - } else if let Some(u) = n.as_u64() { - if u <= u32::MAX as u64 { - Value::from(u as u32) - } else { - Value::from(u) - } - } else { - Value::Null - } - } - Value::String(s) => Value::from(s.clone()), - _ => Value::Null, - } -} - -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())); - } - let mut clauses = Vec::with_capacity(obj.len()); - let mut binds = Vec::with_capacity(obj.len()); - for (k, v) in obj { - clauses.push(format!("{k} = ?")); - binds.push(to_db_bind_value(&v)); - } - Ok((format!(" WHERE {}", clauses.join(" AND ")), binds)) -} - -pub fn build_insert_query_with_meta( - table: &str, - meta: &[(&str, Value)], - fields: &Map<String, Value>, -) -> (String, Vec<Value>) { - let mut cols: Vec<String> = meta.iter().map(|(k, _)| k.to_string()).collect(); - cols.extend(fields.keys().cloned()); - let meta_binds: Vec<Value> = meta.iter().map(|(_, v)| to_db_bind_value(v)).collect(); - let field_binds: Vec<Value> = fields.values().map(to_db_bind_value).collect(); - let placeholders = (0..cols.len()) - .map(|_| "?") - .collect::<Vec<&str>>() - .join(","); - let sql = format!( - "INSERT INTO {table} ({}) VALUES ({});", - cols.join(","), - placeholders - ); - let mut binds = Vec::with_capacity(cols.len()); - binds.extend(meta_binds); - binds.extend(field_binds); - (sql, binds) -} - -pub fn build_select_query_with_meta<T: Serialize>( - table: &str, - filter: Option<&T>, -) -> (String, Vec<Value>) { - let (where_clause, binds) = match filter { - Some(f) => match build_where_clause_eq(f) { - Ok(t) => t, - Err(_) => (String::new(), Vec::new()), - }, - None => (String::new(), Vec::new()), - }; - let sql = format!("SELECT * FROM {table}{where_clause};"); - (sql, binds) -}