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:
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()));