use std::sync::Arc;
use ciborium::Value as CborValue;
use serde_json::{Value, json};
use crate::{Engine, RecordId, RecordRange, Result, SessionState, SurrealError, Table};
pub struct SurrealClient {
engine: Arc<tokio::sync::Mutex<Box<dyn Engine>>>,
session: SessionState,
incremental_id: Arc<std::sync::atomic::AtomicU64>,
debug: bool,
}
impl Clone for SurrealClient {
fn clone(&self) -> Self {
Self {
engine: self.engine.clone(),
session: self.session.clone(),
incremental_id: self.incremental_id.clone(),
debug: self.debug,
}
}
}
impl SurrealClient {
pub fn new(
engine: Box<dyn Engine>,
namespace: Option<String>,
database: Option<String>,
) -> Self {
let mut session = SessionState::new();
session.set_target(namespace, database);
Self {
engine: Arc::new(tokio::sync::Mutex::new(engine)),
session,
incremental_id: Arc::new(std::sync::atomic::AtomicU64::new(0)),
debug: false,
}
}
pub fn with_debug(mut self, enabled: bool) -> Self {
self.debug = enabled;
self
}
pub fn is_debug(&self) -> bool {
self.debug
}
pub async fn let_var(&mut self, key: &str, value: Value) -> Result<()> {
let mut engine = self.engine.lock().await;
let params = json!([key, value]);
engine.send_message("let", params).await?;
self.session.set_param(key.to_string(), value);
Ok(())
}
pub async fn unset(&mut self, key: &str) -> Result<()> {
let mut engine = self.engine.lock().await;
let params = json!([key]);
engine.send_message("unset", params).await?;
self.session.unset_param(key);
Ok(())
}
pub async fn create(&self, resource: &str, data: Option<Value>) -> Result<Value> {
let mut engine = self.engine.lock().await;
let params = if let Some(data) = data {
json!([resource, data])
} else {
json!([resource])
};
engine.send_message("create", params).await
}
pub async fn select(&self, resource: &str) -> Result<Value> {
let mut engine = self.engine.lock().await;
let params = json!([resource]);
engine.send_message("select", params).await
}
pub async fn select_all(&self, table: Table) -> Result<Value> {
self.select(&table.to_string()).await
}
pub async fn select_record(&self, record: RecordId) -> Result<Value> {
self.select(&record.to_string()).await
}
pub async fn select_range(&self, range: RecordRange) -> Result<Value> {
self.select(&range.to_string()).await
}
pub async fn update(&self, resource: &str, data: Option<Value>) -> Result<Value> {
let mut engine = self.engine.lock().await;
let params = if let Some(data) = data {
json!([resource, data])
} else {
json!([resource])
};
engine.send_message("update", params).await
}
pub async fn update_record(&self, record: RecordId, data: Value) -> Result<Value> {
self.update(&record.to_string(), Some(data)).await
}
pub async fn update_all(&self, table: Table, data: Value) -> Result<Value> {
self.update(&table.to_string(), Some(data)).await
}
pub async fn upsert(&self, resource: &str, data: Option<Value>) -> Result<Value> {
let mut engine = self.engine.lock().await;
let params = if let Some(data) = data {
json!([resource, data])
} else {
json!([resource])
};
engine.send_message("upsert", params).await
}
pub async fn upsert_record(&self, record: RecordId, data: Value) -> Result<Value> {
self.upsert(&record.to_string(), Some(data)).await
}
pub async fn merge(&self, resource: &str, data: Value) -> Result<Value> {
let mut engine = self.engine.lock().await;
let params = json!([resource, data]);
engine.send_message("merge", params).await
}
pub async fn merge_record(&self, record: RecordId, data: Value) -> Result<Value> {
self.merge(&record.to_string(), data).await
}
pub async fn merge_all(&self, table: Table, data: Value) -> Result<Value> {
self.merge(&table.to_string(), data).await
}
pub async fn patch(&self, resource: &str, patches: Vec<Value>) -> Result<Value> {
let mut engine = self.engine.lock().await;
let params = json!([resource, patches]);
engine.send_message("patch", params).await
}
pub async fn delete(&self, resource: &str) -> Result<Value> {
let mut engine = self.engine.lock().await;
let params = json!([resource]);
engine.send_message("delete", params).await
}
pub async fn delete_record(&self, record: RecordId) -> Result<Value> {
self.delete(&record.to_string()).await
}
pub async fn delete_all(&self, table: Table) -> Result<Value> {
self.delete(&table.to_string()).await
}
pub async fn insert(&self, table: &str, data: Value) -> Result<Value> {
let mut engine = self.engine.lock().await;
let params = json!([table, data]);
engine.send_message("insert", params).await
}
pub async fn insert_many(&self, table: Table, data: Vec<Value>) -> Result<Value> {
self.insert(&table.to_string(), Value::Array(data)).await
}
pub async fn relate(
&self,
from: &str,
relation: &str,
to: &str,
data: Option<Value>,
) -> Result<Value> {
let mut engine = self.engine.lock().await;
let params = if let Some(data) = data {
json!([from, relation, to, data])
} else {
json!([from, relation, to])
};
engine.send_message("relate", params).await
}
pub async fn relate_records(
&self,
from: RecordId,
relation: Table,
to: RecordId,
data: Option<Value>,
) -> Result<Value> {
self.relate(
&from.to_string(),
&relation.to_string(),
&to.to_string(),
data,
)
.await
}
pub async fn run(&self, func: &str, args: Option<Value>) -> Result<Value> {
let mut engine = self.engine.lock().await;
let params = if let Some(args) = args {
json!([func, args])
} else {
json!([func])
};
engine.send_message("run", params).await
}
pub async fn query(&self, sql: &str, variables: Option<Value>) -> Result<Value> {
if self.debug {
if let Some(ref vars) = variables {
println!("🔍 SQL: {}", sql);
println!(
"📊 Params: {}",
serde_json::to_string_pretty(vars).unwrap_or_default()
);
} else {
println!("🔍 SQL: {}", sql);
}
}
let mut engine = self.engine.lock().await;
let params = if let Some(vars) = variables {
json!([sql, vars])
} else {
json!([sql])
};
let response = engine.send_message("query", params).await?;
if self.debug {
let icon = if let Value::Array(ref results) = response {
if results
.iter()
.any(|r| r.get("status").and_then(|s| s.as_str()) == Some("ERR"))
{
"❌"
} else {
"✅"
}
} else {
"✅"
};
println!(
"{} Response: {}",
icon,
serde_json::to_string_pretty(&response).unwrap_or_default()
);
}
match response {
Value::Array(results) => {
Ok(Value::Array(results))
}
other => Ok(other),
}
}
pub async fn info(&self) -> Result<Value> {
let mut engine = self.engine.lock().await;
let params = json!([]);
engine.send_message("info", params).await
}
pub async fn version(&self) -> Result<String> {
let mut engine = self.engine.lock().await;
let params = json!([]);
let response = engine.send_message("version", params).await?;
match response {
Value::String(version) => Ok(version),
_ => Err(SurrealError::Protocol(
"Invalid version response format".to_string(),
)),
}
}
pub async fn close(self) -> Result<()> {
Ok(())
}
pub async fn import(&self, _content: &str, _username: &str, _password: &str) -> Result<Value> {
Err(SurrealError::Protocol(
"Import is not supported in minimal engine implementation".to_string(),
))
}
pub async fn export(&self, _username: &str, _password: &str) -> Result<String> {
Err(SurrealError::Protocol(
"Export is not supported in minimal engine implementation".to_string(),
))
}
pub async fn import_ml(
&self,
_content: &str,
_username: Option<&str>,
_password: Option<&str>,
) -> Result<Value> {
Err(SurrealError::Protocol(
"ML import is not supported in minimal engine implementation".to_string(),
))
}
pub async fn export_ml(
&self,
_name: &str,
_version: Option<&str>,
_username: Option<&str>,
_password: Option<&str>,
) -> Result<String> {
Err(SurrealError::Protocol(
"ML export is not supported in minimal engine implementation".to_string(),
))
}
pub async fn query_cbor(&self, sql: &str, variables: CborValue) -> Result<CborValue> {
let mut engine = self.engine.lock().await;
if self.debug {
println!("SQL: {}", sql);
println!("Params: {:?}", variables);
}
let params = CborValue::Array(vec![CborValue::Text(sql.to_string()), variables]);
let response = engine.send_message_cbor("query", params).await?;
if self.debug {
println!("✅ CBOR Response: {:?}", response);
}
Ok(response)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
struct MockEngine;
#[async_trait::async_trait]
impl Engine for MockEngine {
async fn send_message(&mut self, _method: &str, _params: Value) -> Result<Value> {
Ok(Value::String("mock_response".to_string()))
}
async fn send_message_cbor(
&mut self,
_method: &str,
_params: CborValue,
) -> Result<CborValue> {
Ok(CborValue::Text("mock_response".to_string()))
}
}
#[tokio::test]
async fn test_surrealdb_creation() {
let engine = Box::new(MockEngine);
let _client = SurrealClient::new(engine, None, None);
}
#[tokio::test]
async fn test_connect_and_operations() {
let engine = Box::new(MockEngine);
let client = SurrealClient::new(
engine,
Some("test_ns".to_string()),
Some("test_db".to_string()),
);
let result = client.select("user").await.unwrap();
assert_eq!(result, Value::String("mock_response".to_string()));
let result = client
.create("user", Some(json!({"name": "John"})))
.await
.unwrap();
assert_eq!(result, Value::String("mock_response".to_string()));
}
#[tokio::test]
async fn test_crud_operations() {
let engine = Box::new(MockEngine);
let client = SurrealClient::new(
engine,
Some("test_ns".to_string()),
Some("test_db".to_string()),
);
let create_result = client
.create("users", Some(json!({"name": "Alice", "age": 30})))
.await
.unwrap();
assert_eq!(create_result, Value::String("mock_response".to_string()));
let read_result = client.select("users").await.unwrap();
assert_eq!(read_result, Value::String("mock_response".to_string()));
let update_result = client
.update("users:alice", Some(json!({"age": 31})))
.await
.unwrap();
assert_eq!(update_result, Value::String("mock_response".to_string()));
let delete_result = client.delete("users:alice").await.unwrap();
assert_eq!(delete_result, Value::String("mock_response".to_string()));
let insert_result = client
.insert("users", json!({"name": "Bob", "age": 25}))
.await
.unwrap();
assert_eq!(insert_result, Value::String("mock_response".to_string()));
let merge_result = client
.merge("users:bob", json!({"city": "New York"}))
.await
.unwrap();
assert_eq!(merge_result, Value::String("mock_response".to_string()));
let upsert_result = client
.upsert("users:charlie", Some(json!({"name": "Charlie", "age": 28})))
.await
.unwrap();
assert_eq!(upsert_result, Value::String("mock_response".to_string()));
}
#[tokio::test]
async fn test_bakery_queries_with_parameters() {
let engine = Box::new(MockEngine);
let mut client = SurrealClient::new(
engine,
Some("bakery".to_string()),
Some("inventory".to_string()),
);
client.let_var("min_stock", json!(10)).await.unwrap();
client.let_var("category", json!("bread")).await.unwrap();
let query =
"SELECT * FROM products WHERE stock_level < $min_stock AND category = $category";
let result = client.query(query, None).await.unwrap();
assert_eq!(result, Value::String("mock_response".to_string()));
let variables = json!({
"supplier": "FreshBake Co",
"min_price": 5.0
});
let query_with_params = "SELECT * FROM products WHERE supplier = $supplier AND price >= $min_price ORDER BY price DESC";
let result = client
.query(query_with_params, Some(variables))
.await
.unwrap();
assert_eq!(result, Value::String("mock_response".to_string()));
let analytics_query = r#"
SELECT
category,
COUNT() as total_products,
SUM(stock_level) as total_stock,
AVG(price) as avg_price,
MAX(price) as max_price,
MIN(price) as min_price
FROM products
WHERE stock_level > 0
GROUP BY category
ORDER BY total_stock DESC
"#;
let result = client.query(analytics_query, None).await.unwrap();
assert_eq!(result, Value::String("mock_response".to_string()));
let relation_query = r#"
SELECT *,
->supplied_by->suppliers.* as supplier_info
FROM products
WHERE category = 'pastries'
"#;
let result = client.query(relation_query, None).await.unwrap();
assert_eq!(result, Value::String("mock_response".to_string()));
let time_query = r#"
SELECT *
FROM orders
WHERE created_at >= time::now() - 7d
ORDER BY created_at DESC
LIMIT 50
"#;
let result = client.query(time_query, None).await.unwrap();
assert_eq!(result, Value::String("mock_response".to_string()));
client.unset("min_stock").await.unwrap();
client.unset("category").await.unwrap();
}
#[tokio::test]
async fn test_complex_analytics_queries() {
let engine = Box::new(MockEngine);
let client = SurrealClient::new(
engine,
Some("analytics".to_string()),
Some("business".to_string()),
);
let revenue_query = r#"
SELECT
date::format(created_at, '%Y-%m') as month,
SUM(total_amount) as monthly_revenue,
COUNT() as order_count,
AVG(total_amount) as avg_order_value
FROM orders
WHERE created_at >= time::now() - 12mo
GROUP BY month
ORDER BY month DESC
"#;
let result = client.query(revenue_query, None).await.unwrap();
assert_eq!(result, Value::String("mock_response".to_string()));
let segmentation_query = r#"
SELECT
CASE
WHEN total_spent >= 1000 THEN 'Premium'
WHEN total_spent >= 500 THEN 'Regular'
ELSE 'Basic'
END as segment,
COUNT() as customer_count,
AVG(total_spent) as avg_spent,
SUM(total_spent) as segment_revenue
FROM (
SELECT
customer_id,
SUM(total_amount) as total_spent
FROM orders
GROUP BY customer_id
) as customer_totals
GROUP BY segment
ORDER BY avg_spent DESC
"#;
let result = client.query(segmentation_query, None).await.unwrap();
assert_eq!(result, Value::String("mock_response".to_string()));
let performance_query = r#"
SELECT
p.id,
p.name,
p.category,
COUNT(oi.id) as times_ordered,
SUM(oi.quantity) as total_quantity_sold,
SUM(oi.price * oi.quantity) as total_revenue,
p.stock_level as current_stock,
CASE
WHEN p.stock_level = 0 THEN 'Out of Stock'
WHEN p.stock_level < 10 THEN 'Low Stock'
WHEN p.stock_level < 50 THEN 'Medium Stock'
ELSE 'High Stock'
END as stock_status
FROM products p
LEFT JOIN order_items oi ON oi.product_id = p.id
GROUP BY p.id, p.name, p.category, p.stock_level
ORDER BY total_revenue DESC
"#;
let result = client.query(performance_query, None).await.unwrap();
assert_eq!(result, Value::String("mock_response".to_string()));
}
}