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:
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)
-}