sparrowdb_execution/json.rs
1//! JSON serialization helpers for execution types.
2//!
3//! Shared by `sparrowdb-cli` (NDJSON serve mode) and `sparrowdb-node`
4//! (napi-rs bindings) to avoid duplicating the same conversion logic.
5
6use crate::types::{QueryResult, Value};
7
8/// Convert a scalar [`Value`] to a [`serde_json::Value`].
9///
10/// `NodeRef` and `EdgeRef` are serialized as tagged objects with string ids:
11/// `{"$type": "node", "id": "123"}` / `{"$type": "edge", "id": "456"}`.
12/// IDs are strings (not numbers) to preserve u64 precision across the JS boundary.
13///
14/// `Int64` values within `±(2^53-1)` are plain JSON numbers; values outside
15/// that range are serialized as decimal strings to avoid silent precision loss.
16pub fn value_to_json(v: &Value) -> serde_json::Value {
17 match v {
18 Value::Null => serde_json::Value::Null,
19 // JavaScript's Number can only represent integers exactly up to 2^53-1.
20 // Serialize Int64 values outside the safe range as strings so callers
21 // don't silently receive wrong values.
22 Value::Int64(i) => {
23 const MAX_SAFE: i64 = (1_i64 << 53) - 1;
24 const MIN_SAFE: i64 = -MAX_SAFE;
25 if *i >= MIN_SAFE && *i <= MAX_SAFE {
26 serde_json::json!(i)
27 } else {
28 serde_json::json!(i.to_string())
29 }
30 }
31 Value::Float64(f) => serde_json::json!(f),
32 Value::Bool(b) => serde_json::json!(b),
33 Value::String(s) => serde_json::json!(s),
34 // IDs are u64, which exceeds JavaScript's safe integer range (2^53-1).
35 // Serialize as strings so precision is preserved across the JSON boundary.
36 Value::NodeRef(n) => serde_json::json!({"$type": "node", "id": n.0.to_string()}),
37 Value::EdgeRef(e) => serde_json::json!({"$type": "edge", "id": e.0.to_string()}),
38 Value::List(items) => serde_json::Value::Array(items.iter().map(value_to_json).collect()),
39 Value::Map(entries) => {
40 let obj: serde_json::Map<String, serde_json::Value> = entries
41 .iter()
42 .map(|(k, v)| (k.clone(), value_to_json(v)))
43 .collect();
44 serde_json::Value::Object(obj)
45 }
46 }
47}
48
49/// Materialize a [`QueryResult`] into a JSON object:
50///
51/// ```json
52/// {
53/// "columns": ["n.name", "n.age"],
54/// "rows": [{"n.name": "Alice", "n.age": 30}, ...]
55/// }
56/// ```
57pub fn query_result_to_json(result: &QueryResult) -> serde_json::Value {
58 let rows: Vec<serde_json::Value> = result
59 .rows
60 .iter()
61 .map(|row| {
62 let obj: serde_json::Map<String, serde_json::Value> = result
63 .columns
64 .iter()
65 .zip(row.iter())
66 .map(|(col, val)| (col.clone(), value_to_json(val)))
67 .collect();
68 serde_json::Value::Object(obj)
69 })
70 .collect();
71
72 serde_json::json!({
73 "columns": result.columns,
74 "rows": rows,
75 })
76}