1use super::packstream::{Value, MAX_PACKSTREAM_DEPTH};
29use serde_json::Value as Json;
30
31pub const TAG_NODE: u8 = 0x4E;
33pub const TAG_RELATIONSHIP: u8 = 0x52;
34pub const TAG_UNBOUND_RELATIONSHIP: u8 = 0x72;
35pub const TAG_PATH: u8 = 0x50;
36
37pub fn json_to_value(json: &Json) -> Result<Value, String> {
39 to_value(json, 1)
40}
41
42fn to_value(json: &Json, depth: usize) -> Result<Value, String> {
43 Ok(match json {
44 Json::Null => Value::Null,
45 Json::Bool(b) => Value::Bool(*b),
46 Json::Number(n) => {
47 if let Some(i) = n.as_i64() {
48 Value::Int(i)
49 } else if n.is_u64() {
50 return Err(format!(
51 "integer {n} does not fit a PackStream INT_64 (max {})",
52 i64::MAX
53 ));
54 } else {
55 Value::Float(n.as_f64().unwrap_or(f64::NAN))
56 }
57 }
58 Json::String(s) => Value::String(s.clone()),
59 Json::Array(items) => {
60 check_depth(depth)?;
61 Value::List(
62 items
63 .iter()
64 .map(|v| to_value(v, depth + 1))
65 .collect::<Result<_, _>>()?,
66 )
67 }
68 Json::Object(map) => {
69 check_depth(depth)?;
70 if map.len() == 1 {
71 if let Some(node) = map.get("$node") {
72 return node_value(node, depth + 1).map(|n| n.value);
73 }
74 if let Some(rel) = map.get("$relationship") {
75 return relationship_value(rel, depth + 1).map(|r| r.bound);
76 }
77 if let Some(path) = map.get("$path") {
78 return path_value(path, depth + 1);
79 }
80 }
81 Value::Map(
82 map.iter()
83 .map(|(k, v)| Ok((k.clone(), to_value(v, depth + 1)?)))
84 .collect::<Result<_, String>>()?,
85 )
86 }
87 })
88}
89
90fn check_depth(depth: usize) -> Result<(), String> {
91 if depth > MAX_PACKSTREAM_DEPTH {
92 Err(format!(
93 "value nests deeper than {MAX_PACKSTREAM_DEPTH} levels"
94 ))
95 } else {
96 Ok(())
97 }
98}
99
100fn object<'a>(json: &'a Json, what: &str) -> Result<&'a serde_json::Map<String, Json>, String> {
101 json.as_object()
102 .ok_or_else(|| format!("{what} must be an object"))
103}
104
105fn int_field(map: &serde_json::Map<String, Json>, key: &str, what: &str) -> Result<i64, String> {
106 map.get(key)
107 .and_then(Json::as_i64)
108 .ok_or_else(|| format!("{what} needs an integer '{key}'"))
109}
110
111fn element_id(map: &serde_json::Map<String, Json>, key: &str, fallback: i64) -> Value {
112 Value::String(
113 map.get(key)
114 .and_then(Json::as_str)
115 .map(str::to_string)
116 .unwrap_or_else(|| fallback.to_string()),
117 )
118}
119
120fn properties(
121 map: &serde_json::Map<String, Json>,
122 what: &str,
123 depth: usize,
124) -> Result<Value, String> {
125 match map.get("properties") {
126 None | Some(Json::Null) => Ok(Value::Map(Vec::new())),
127 Some(Json::Object(_)) => to_value(&map["properties"], depth),
128 Some(_) => Err(format!("{what} 'properties' must be an object")),
129 }
130}
131
132struct Node {
133 id: i64,
134 value: Value,
135}
136
137fn node_value(json: &Json, depth: usize) -> Result<Node, String> {
140 check_depth(depth)?;
141 let json = json.get("$node").unwrap_or(json);
142 let map = object(json, "$node")?;
143 let id = int_field(map, "id", "$node")?;
144 let labels = match map.get("labels") {
145 None | Some(Json::Null) => Vec::new(),
146 Some(Json::Array(labels)) => labels
147 .iter()
148 .map(|l| {
149 l.as_str()
150 .map(Value::string)
151 .ok_or_else(|| "$node 'labels' must be strings".to_string())
152 })
153 .collect::<Result<_, _>>()?,
154 Some(_) => return Err("$node 'labels' must be an array of strings".to_string()),
155 };
156 Ok(Node {
157 id,
158 value: Value::Struct {
159 tag: TAG_NODE,
160 fields: vec![
161 Value::Int(id),
162 Value::List(labels),
163 properties(map, "$node", depth + 1)?,
164 element_id(map, "element_id", id),
165 ],
166 },
167 })
168}
169
170struct Relationship {
171 id: i64,
172 start: i64,
173 end: i64,
174 bound: Value,
175 unbound: Value,
176}
177
178fn relationship_value(json: &Json, depth: usize) -> Result<Relationship, String> {
181 check_depth(depth)?;
182 let json = json.get("$relationship").unwrap_or(json);
183 let map = object(json, "$relationship")?;
184 let id = int_field(map, "id", "$relationship")?;
185 let start = int_field(map, "start", "$relationship")?;
186 let end = int_field(map, "end", "$relationship")?;
187 let rel_type = map
188 .get("type")
189 .and_then(Json::as_str)
190 .filter(|t| !t.is_empty())
191 .ok_or("$relationship needs a non-empty string 'type'")?
192 .to_string();
193 let props = properties(map, "$relationship", depth + 1)?;
194 let eid = element_id(map, "element_id", id);
195 Ok(Relationship {
196 id,
197 start,
198 end,
199 bound: Value::Struct {
200 tag: TAG_RELATIONSHIP,
201 fields: vec![
202 Value::Int(id),
203 Value::Int(start),
204 Value::Int(end),
205 Value::String(rel_type.clone()),
206 props.clone(),
207 eid.clone(),
208 element_id(map, "start_element_id", start),
209 element_id(map, "end_element_id", end),
210 ],
211 },
212 unbound: Value::Struct {
213 tag: TAG_UNBOUND_RELATIONSHIP,
214 fields: vec![Value::Int(id), Value::String(rel_type), props, eid],
215 },
216 })
217}
218
219fn path_value(json: &Json, depth: usize) -> Result<Value, String> {
220 check_depth(depth)?;
221 let map = object(json, "$path")?;
222 let nodes = map
223 .get("nodes")
224 .and_then(Json::as_array)
225 .ok_or("$path needs a 'nodes' array")?;
226 let rels = match map.get("relationships") {
227 None | Some(Json::Null) => Vec::new(),
228 Some(Json::Array(r)) => r.clone(),
229 Some(_) => return Err("$path 'relationships' must be an array".to_string()),
230 };
231 if nodes.is_empty() {
232 return Err("$path needs at least one node".to_string());
233 }
234 if rels.len() + 1 != nodes.len() {
235 return Err(format!(
236 "$path with {} nodes needs {} relationships, got {}",
237 nodes.len(),
238 nodes.len() - 1,
239 rels.len()
240 ));
241 }
242 let nodes: Vec<Node> = nodes
243 .iter()
244 .map(|n| node_value(n, depth + 1))
245 .collect::<Result<_, _>>()?;
246 let rels: Vec<Relationship> = rels
247 .iter()
248 .map(|r| relationship_value(r, depth + 1))
249 .collect::<Result<_, _>>()?;
250
251 let mut unique_nodes: Vec<&Node> = Vec::new();
254 for node in &nodes {
255 if !unique_nodes.iter().any(|n| n.id == node.id) {
256 unique_nodes.push(node);
257 }
258 }
259 let mut unique_rels: Vec<&Relationship> = Vec::new();
260 for rel in &rels {
261 if !unique_rels.iter().any(|r| r.id == rel.id) {
262 unique_rels.push(rel);
263 }
264 }
265
266 let mut indices = Vec::with_capacity(rels.len() * 2);
267 for (i, rel) in rels.iter().enumerate() {
268 let (from, to) = (nodes[i].id, nodes[i + 1].id);
269 let rel_index = unique_rels
270 .iter()
271 .position(|r| r.id == rel.id)
272 .map(|p| p as i64 + 1)
273 .unwrap_or(1);
274 let signed = if rel.start == from && rel.end == to {
275 rel_index
276 } else if rel.start == to && rel.end == from {
277 -rel_index
278 } else {
279 return Err(format!(
280 "$path relationship {} ({}->{}) does not connect nodes {} and {}",
281 rel.id, rel.start, rel.end, from, to
282 ));
283 };
284 let node_index = unique_nodes
285 .iter()
286 .position(|n| n.id == to)
287 .map(|p| p as i64)
288 .unwrap_or(0);
289 indices.push(Value::Int(signed));
290 indices.push(Value::Int(node_index));
291 }
292
293 Ok(Value::Struct {
294 tag: TAG_PATH,
295 fields: vec![
296 Value::List(unique_nodes.iter().map(|n| n.value.clone()).collect()),
297 Value::List(unique_rels.iter().map(|r| r.unbound.clone()).collect()),
298 Value::List(indices),
299 ],
300 })
301}
302
303pub fn structure_name(tag: u8) -> String {
305 match tag {
306 TAG_NODE => "Node".to_string(),
307 TAG_RELATIONSHIP => "Relationship".to_string(),
308 TAG_UNBOUND_RELATIONSHIP => "UnboundRelationship".to_string(),
309 TAG_PATH => "Path".to_string(),
310 0x44 => "Date".to_string(),
311 0x54 => "Time".to_string(),
312 0x74 => "LocalTime".to_string(),
313 0x49 => "DateTime".to_string(),
314 0x69 => "DateTimeZoneId".to_string(),
315 0x64 => "LocalDateTime".to_string(),
316 0x45 => "Duration".to_string(),
317 0x58 => "Point2D".to_string(),
318 0x59 => "Point3D".to_string(),
319 other => format!("0x{other:02X}"),
320 }
321}
322
323pub fn value_to_json(value: &Value) -> Json {
325 match value {
326 Value::Null => Json::Null,
327 Value::Bool(b) => Json::Bool(*b),
328 Value::Int(i) => Json::from(*i),
329 Value::Float(f) => serde_json::Number::from_f64(*f)
330 .map(Json::Number)
331 .unwrap_or_else(|| Json::String(f.to_string())),
332 Value::Bytes(b) => serde_json::json!({"$bytes_length": b.len()}),
333 Value::String(s) => Json::String(s.clone()),
334 Value::List(items) => Json::Array(items.iter().map(value_to_json).collect()),
335 Value::Map(entries) => Json::Object(
336 entries
337 .iter()
338 .map(|(k, v)| (k.clone(), value_to_json(v)))
339 .collect(),
340 ),
341 Value::Struct { tag, fields } => serde_json::json!({
342 "$structure": structure_name(*tag),
343 "fields": fields.iter().map(value_to_json).collect::<Vec<_>>(),
344 }),
345 }
346}
347
348pub const STAT_COUNTERS: &[&str] = &[
350 "nodes-created",
351 "nodes-deleted",
352 "relationships-created",
353 "relationships-deleted",
354 "properties-set",
355 "labels-added",
356 "labels-removed",
357 "indexes-added",
358 "indexes-removed",
359 "constraints-added",
360 "constraints-removed",
361 "system-updates",
362];
363
364pub fn stats_value(json: Option<&Json>) -> Result<Option<Value>, String> {
368 let map = match json {
369 None | Some(Json::Null) => return Ok(None),
370 Some(Json::Object(map)) => map,
371 Some(_) => return Err("'stats' must be an object".to_string()),
372 };
373 let mut entries = Vec::new();
374 let mut updates = false;
375 let mut system_updates = false;
376 for (key, value) in map {
377 let wire = key.replace('_', "-");
378 if !STAT_COUNTERS.contains(&wire.as_str()) {
379 return Err(format!(
380 "unknown stats key '{key}' (allowed: {})",
381 STAT_COUNTERS
382 .iter()
383 .map(|k| k.replace('-', "_"))
384 .collect::<Vec<_>>()
385 .join(", ")
386 ));
387 }
388 let count = value
389 .as_i64()
390 .filter(|c| *c >= 0)
391 .ok_or_else(|| format!("stats '{key}' must be a non-negative integer"))?;
392 if count == 0 {
393 continue;
394 }
395 if wire == "system-updates" {
396 system_updates = true;
397 } else {
398 updates = true;
399 }
400 let wire_key = STAT_COUNTERS
401 .iter()
402 .find(|k| **k == wire)
403 .copied()
404 .unwrap_or("nodes-created");
405 entries.push((wire_key.to_string(), Value::Int(count)));
406 }
407 if entries.is_empty() {
408 return Ok(None);
409 }
410 if updates {
411 entries.push(("contains-updates".to_string(), Value::Bool(true)));
412 }
413 if system_updates {
414 entries.push(("contains-system-updates".to_string(), Value::Bool(true)));
415 }
416 Ok(Some(Value::Map(entries)))
417}
418
419pub fn valid_failure_code(code: &str) -> bool {
422 let parts: Vec<&str> = code.split('.').collect();
423 let word = |s: &str| {
424 !s.is_empty()
425 && s.len() <= 64
426 && s.chars().next().is_some_and(|c| c.is_ascii_uppercase())
427 && s.chars().all(|c| c.is_ascii_alphanumeric())
428 };
429 parts.len() == 4
430 && parts[0] == "Neo"
431 && matches!(parts[1], "ClientError" | "TransientError" | "DatabaseError")
432 && word(parts[2])
433 && word(parts[3])
434}
435
436#[derive(Debug, Clone)]
438pub struct QueryAnswer {
439 pub fields: Vec<String>,
440 pub records: Vec<Vec<Value>>,
441 pub stats: Option<Value>,
442 pub query_type: &'static str,
443}
444
445pub const MAX_ACTION_DEPTH: usize = MAX_PACKSTREAM_DEPTH + 4;
448pub const MAX_ACTION_NODES: usize = 65536;
449pub const MAX_ACTION_RETAINED_BYTES: usize = 8 * 1024 * 1024;
450pub fn action_within_budget(action: &Json) -> bool {
451 crate::utils::json_budget::within_budget(
452 action,
453 MAX_ACTION_RETAINED_BYTES,
454 MAX_ACTION_NODES,
455 MAX_ACTION_DEPTH,
456 )
457}
458
459fn within_wire_depth(value: &Value, depth: usize) -> bool {
461 let mut pending = vec![(value, depth)];
462 while let Some((value, depth)) = pending.pop() {
463 let children = match value {
464 Value::List(v) | Value::Struct { fields: v, .. } => Some(v.as_slice()),
465 Value::Map(v) => {
466 if depth > MAX_PACKSTREAM_DEPTH {
467 return false;
468 }
469 pending.extend(v.iter().map(|(_, v)| (v, depth + 1)));
470 None
471 }
472 _ => None,
473 };
474 if let Some(children) = children {
475 if depth > MAX_PACKSTREAM_DEPTH {
476 return false;
477 }
478 pending.extend(children.iter().map(|v| (v, depth + 1)));
479 }
480 }
481 true
482}
483
484pub fn query_answer(action: &Json) -> Result<QueryAnswer, String> {
488 if !action_within_budget(action) {
489 return Err("Bolt answer exceeds the in-memory depth/node/retained-content budget".into());
490 }
491 let fields: Vec<String> = match action.get("fields") {
492 Some(Json::Array(fields)) => fields
493 .iter()
494 .map(|f| {
495 f.as_str()
496 .filter(|s| !s.is_empty())
497 .map(str::to_string)
498 .ok_or_else(|| "every entry in 'fields' must be a non-empty string".to_string())
499 })
500 .collect::<Result<_, _>>()?,
501 None | Some(Json::Null) => Vec::new(),
502 Some(_) => return Err("'fields' must be an array of column names".to_string()),
503 };
504 for (i, f) in fields.iter().enumerate() {
505 if fields[..i].contains(f) {
506 return Err(format!("column '{f}' appears twice in 'fields'"));
507 }
508 }
509 let rows = match action.get("records") {
510 None | Some(Json::Null) => Vec::new(),
511 Some(Json::Array(rows)) => rows.iter().collect::<Vec<_>>(),
512 Some(_) => return Err("'records' must be an array of rows".to_string()),
513 };
514 let mut records = Vec::with_capacity(rows.len());
515 for (i, row) in rows.iter().enumerate() {
516 let cells = row.as_array().ok_or_else(|| {
517 format!("record {i} must be an array with one value per field, in field order")
518 })?;
519 if cells.len() != fields.len() {
520 return Err(format!(
521 "record {i} has {} values but there are {} fields",
522 cells.len(),
523 fields.len()
524 ));
525 }
526 records.push(
527 cells
528 .iter()
529 .map(|c| {
530 let value = json_to_value(c).map_err(|e| format!("record {i}: {e}"))?;
531 if !within_wire_depth(&value, 3) {
532 return Err(format!(
533 "record {i}: encoded value exceeds wire depth {MAX_PACKSTREAM_DEPTH}"
534 ));
535 }
536 Ok(value)
537 })
538 .collect::<Result<Vec<_>, _>>()?,
539 );
540 }
541 let query_type = match action.get("query_type").and_then(Json::as_str) {
542 None => "r",
543 Some("r") => "r",
544 Some("w") => "w",
545 Some("rw") => "rw",
546 Some("s") => "s",
547 Some(other) => {
548 return Err(format!(
549 "query_type must be r (read), w (write), rw (read-write) or s (schema), got \
550 '{other}'"
551 ))
552 }
553 };
554 Ok(QueryAnswer {
555 fields,
556 records,
557 stats: stats_value(action.get("stats"))?,
558 query_type,
559 })
560}