Skip to main content

uni_common/
cypher_value_codec.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! MessagePack-based binary encoding for CypherValue (uni_common::Value).
5//!
6//! # Design
7//!
8//! All property values are stored as self-describing binary blobs in Arrow
9//! `LargeBinary` columns. Each blob has the format:
10//!
11//! ```text
12//! [tag_byte: u8][msgpack_payload: bytes]
13//! ```
14//!
15//! The tag byte provides O(1) type identification without deserialization.
16//! MessagePack preserves int/float distinction natively (unlike JSON).
17//!
18//! # Tag Constants
19//!
20//! | Tag | Type | Payload |
21//! |-----|------|---------|
22//! | 0 | Null | empty |
23//! | 1 | Bool | msgpack bool |
24//! | 2 | Int | msgpack i64 |
25//! | 3 | Float | msgpack f64 |
26//! | 4 | String | msgpack string |
27//! | 5 | List | msgpack array of recursively-encoded blobs |
28//! | 6 | Map | msgpack map of string → recursively-encoded blobs |
29//! | 7 | Bytes | msgpack binary |
30//! | 8 | Node | msgpack {vid, label, props} |
31//! | 9 | Edge | msgpack {eid, type, src, dst, props} |
32//! | 10 | Path | msgpack {nodes, rels} |
33//! | 11 | Date | msgpack i32 (days since epoch) |
34//! | 12 | Time | msgpack i64 (nanoseconds since midnight) |
35//! | 13 | DateTime | msgpack i64 (nanoseconds since epoch) |
36//! | 14 | Duration | msgpack {months, days, nanos} |
37//! | 15 | Point | msgpack {srid, coords} |
38//! | 16 | Vector | msgpack array of f32 |
39//!
40//! Nested values (List elements, Map values, Node/Edge properties) are
41//! recursively encoded as `[tag][payload]` blobs.
42
43use crate::api::error::UniError;
44use crate::core::id::{Eid, Vid};
45use crate::value::{Edge, Node, Path, Value};
46use serde::{Deserialize, Serialize};
47use std::collections::{BTreeMap, HashMap};
48
49// Tag constants
50pub const TAG_NULL: u8 = 0;
51pub const TAG_BOOL: u8 = 1;
52pub const TAG_INT: u8 = 2;
53pub const TAG_FLOAT: u8 = 3;
54pub const TAG_STRING: u8 = 4;
55pub const TAG_LIST: u8 = 5;
56pub const TAG_MAP: u8 = 6;
57pub const TAG_BYTES: u8 = 7;
58pub const TAG_NODE: u8 = 8;
59pub const TAG_EDGE: u8 = 9;
60pub const TAG_PATH: u8 = 10;
61pub const TAG_DATE: u8 = 11;
62pub const TAG_TIME: u8 = 12;
63pub const TAG_DATETIME: u8 = 13;
64pub const TAG_DURATION: u8 = 14;
65// pub const TAG_POINT: u8 = 15;
66pub const TAG_VECTOR: u8 = 16;
67pub const TAG_LOCALTIME: u8 = 17;
68pub const TAG_LOCALDATETIME: u8 = 18;
69
70// ---------------------------------------------------------------------------
71// Public encode/decode API
72// ---------------------------------------------------------------------------
73
74/// Encode a Value to tagged MessagePack bytes.
75pub fn encode(value: &Value) -> Vec<u8> {
76    let mut buf = Vec::new();
77    encode_to_buf(value, &mut buf);
78    buf
79}
80
81/// Decode tagged MessagePack bytes to a Value.
82pub fn decode(bytes: &[u8]) -> Result<Value, UniError> {
83    if bytes.is_empty() {
84        return Err(UniError::Storage {
85            message: "empty CypherValue bytes".to_string(),
86            source: None,
87        });
88    }
89    let tag = bytes[0];
90    let payload = &bytes[1..];
91
92    match tag {
93        TAG_NULL => Ok(Value::Null),
94        TAG_BOOL => {
95            let b: bool = rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
96                message: format!("failed to decode bool: {}", e),
97                source: None,
98            })?;
99            Ok(Value::Bool(b))
100        }
101        TAG_INT => {
102            let i: i64 = rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
103                message: format!("failed to decode int: {}", e),
104                source: None,
105            })?;
106            Ok(Value::Int(i))
107        }
108        TAG_FLOAT => {
109            let f: f64 = rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
110                message: format!("failed to decode float: {}", e),
111                source: None,
112            })?;
113            Ok(Value::Float(f))
114        }
115        TAG_STRING => {
116            let s: String = rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
117                message: format!("failed to decode string: {}", e),
118                source: None,
119            })?;
120            Ok(Value::String(s))
121        }
122        TAG_BYTES => {
123            let b: Vec<u8> = rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
124                message: format!("failed to decode bytes: {}", e),
125                source: None,
126            })?;
127            Ok(Value::Bytes(b))
128        }
129        TAG_LIST => {
130            let blobs: Vec<Vec<u8>> =
131                rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
132                    message: format!("failed to decode list: {}", e),
133                    source: None,
134                })?;
135            let items: Result<Vec<Value>, UniError> = blobs.iter().map(|b| decode(b)).collect();
136            Ok(Value::List(items?))
137        }
138        TAG_MAP => {
139            let blob_map: HashMap<String, Vec<u8>> =
140                rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
141                    message: format!("failed to decode map: {}", e),
142                    source: None,
143                })?;
144            let mut map = HashMap::new();
145            for (k, v_blob) in blob_map {
146                map.insert(k, decode(&v_blob)?);
147            }
148            Ok(Value::Map(map))
149        }
150        TAG_NODE => {
151            let np: NodePayload =
152                rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
153                    message: format!("failed to decode node: {}", e),
154                    source: None,
155                })?;
156            let mut props = HashMap::new();
157            for (k, v_blob) in np.properties {
158                props.insert(k, decode(&v_blob)?);
159            }
160            Ok(Value::Node(Node {
161                vid: np.vid,
162                labels: np.labels,
163                properties: props,
164            }))
165        }
166        TAG_EDGE => {
167            let ep: EdgePayload =
168                rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
169                    message: format!("failed to decode edge: {}", e),
170                    source: None,
171                })?;
172            let mut props = HashMap::new();
173            for (k, v_blob) in ep.properties {
174                props.insert(k, decode(&v_blob)?);
175            }
176            Ok(Value::Edge(Edge {
177                eid: ep.eid,
178                edge_type: ep.edge_type,
179                src: ep.src,
180                dst: ep.dst,
181                properties: props,
182            }))
183        }
184        TAG_PATH => {
185            let pp: PathPayload =
186                rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
187                    message: format!("failed to decode path: {}", e),
188                    source: None,
189                })?;
190            let nodes: Result<Vec<Node>, UniError> = pp
191                .nodes
192                .iter()
193                .map(|b| match decode(b)? {
194                    Value::Node(n) => Ok(n),
195                    _ => Err(UniError::Storage {
196                        message: "path node blob is not a Node".to_string(),
197                        source: None,
198                    }),
199                })
200                .collect();
201            let edges: Result<Vec<Edge>, UniError> = pp
202                .edges
203                .iter()
204                .map(|b| match decode(b)? {
205                    Value::Edge(e) => Ok(e),
206                    _ => Err(UniError::Storage {
207                        message: "path edge blob is not an Edge".to_string(),
208                        source: None,
209                    }),
210                })
211                .collect();
212            Ok(Value::Path(Path {
213                nodes: nodes?,
214                edges: edges?,
215            }))
216        }
217        TAG_VECTOR => {
218            let v: Vec<f32> = rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
219                message: format!("failed to decode vector: {}", e),
220                source: None,
221            })?;
222            Ok(Value::Vector(v))
223        }
224        TAG_DATE => {
225            let days: i32 = rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
226                message: format!("failed to decode date: {}", e),
227                source: None,
228            })?;
229            Ok(Value::Temporal(crate::value::TemporalValue::Date {
230                days_since_epoch: days,
231            }))
232        }
233        TAG_LOCALTIME => {
234            let nanos: i64 = rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
235                message: format!("failed to decode localtime: {}", e),
236                source: None,
237            })?;
238            Ok(Value::Temporal(crate::value::TemporalValue::LocalTime {
239                nanos_since_midnight: nanos,
240            }))
241        }
242        TAG_TIME => {
243            let tp: TimePayload =
244                rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
245                    message: format!("failed to decode time: {}", e),
246                    source: None,
247                })?;
248            Ok(Value::Temporal(crate::value::TemporalValue::Time {
249                nanos_since_midnight: tp.nanos,
250                offset_seconds: tp.offset,
251            }))
252        }
253        TAG_LOCALDATETIME => {
254            let nanos: i64 = rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
255                message: format!("failed to decode localdatetime: {}", e),
256                source: None,
257            })?;
258            Ok(Value::Temporal(
259                crate::value::TemporalValue::LocalDateTime {
260                    nanos_since_epoch: nanos,
261                },
262            ))
263        }
264        TAG_DATETIME => {
265            let dp: DateTimePayload =
266                rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
267                    message: format!("failed to decode datetime: {}", e),
268                    source: None,
269                })?;
270            Ok(Value::Temporal(crate::value::TemporalValue::DateTime {
271                nanos_since_epoch: dp.nanos,
272                offset_seconds: dp.offset,
273                timezone_name: dp.tz_name,
274            }))
275        }
276        TAG_DURATION => {
277            let dp: DurationPayload =
278                rmp_serde::from_slice(payload).map_err(|e| UniError::Storage {
279                    message: format!("failed to decode duration: {}", e),
280                    source: None,
281                })?;
282            Ok(Value::Temporal(crate::value::TemporalValue::Duration {
283                months: dp.months,
284                days: dp.days,
285                nanos: dp.nanos,
286            }))
287        }
288        _ => Err(UniError::Storage {
289            message: format!("unknown CypherValue tag: {}", tag),
290            source: None,
291        }),
292    }
293}
294
295// ---------------------------------------------------------------------------
296// O(1) introspection API (no deserialization)
297// ---------------------------------------------------------------------------
298
299/// Peek at the tag byte without deserializing.
300pub fn peek_tag(bytes: &[u8]) -> Option<u8> {
301    bytes.first().copied()
302}
303
304/// Fast null check.
305pub fn is_null(bytes: &[u8]) -> bool {
306    peek_tag(bytes) == Some(TAG_NULL)
307}
308
309// ---------------------------------------------------------------------------
310// Fast typed decode (skip Value construction)
311// ---------------------------------------------------------------------------
312
313/// Decode an int directly without constructing a Value.
314pub fn decode_int(bytes: &[u8]) -> Option<i64> {
315    if bytes.first().copied() != Some(TAG_INT) {
316        return None;
317    }
318    rmp_serde::from_slice(&bytes[1..]).ok()
319}
320
321/// Decode a float directly without constructing a Value.
322pub fn decode_float(bytes: &[u8]) -> Option<f64> {
323    if bytes.first().copied() != Some(TAG_FLOAT) {
324        return None;
325    }
326    rmp_serde::from_slice(&bytes[1..]).ok()
327}
328
329/// Decode a bool directly without constructing a Value.
330pub fn decode_bool(bytes: &[u8]) -> Option<bool> {
331    if bytes.first().copied() != Some(TAG_BOOL) {
332        return None;
333    }
334    rmp_serde::from_slice(&bytes[1..]).ok()
335}
336
337/// Decode a string directly without constructing a Value.
338pub fn decode_string(bytes: &[u8]) -> Option<String> {
339    if bytes.first().copied() != Some(TAG_STRING) {
340        return None;
341    }
342    rmp_serde::from_slice(&bytes[1..]).ok()
343}
344
345// ---------------------------------------------------------------------------
346// Fast typed encode (skip Value construction)
347// ---------------------------------------------------------------------------
348
349/// Encode an int directly without constructing a Value.
350pub fn encode_int(value: i64) -> Vec<u8> {
351    let mut buf = Vec::new();
352    buf.push(TAG_INT);
353    rmp_serde::encode::write(&mut buf, &value).expect("int encode failed");
354    buf
355}
356
357/// Encode a float directly without constructing a Value.
358pub fn encode_float(value: f64) -> Vec<u8> {
359    let mut buf = Vec::new();
360    buf.push(TAG_FLOAT);
361    rmp_serde::encode::write(&mut buf, &value).expect("float encode failed");
362    buf
363}
364
365/// Encode a bool directly without constructing a Value.
366pub fn encode_bool(value: bool) -> Vec<u8> {
367    let mut buf = Vec::new();
368    buf.push(TAG_BOOL);
369    rmp_serde::encode::write(&mut buf, &value).expect("bool encode failed");
370    buf
371}
372
373/// Encode a string directly without constructing a Value.
374pub fn encode_string(value: &str) -> Vec<u8> {
375    let mut buf = Vec::new();
376    buf.push(TAG_STRING);
377    rmp_serde::encode::write(&mut buf, value).expect("string encode failed");
378    buf
379}
380
381/// Encode null directly.
382pub fn encode_null() -> Vec<u8> {
383    vec![TAG_NULL]
384}
385
386/// Extract a map entry as raw bytes without decoding the entire map.
387///
388/// This is useful for extracting a single property from overflow JSON
389/// without paying the cost of decoding all other properties.
390///
391/// Returns `None` if:
392/// - The blob is not a TAG_MAP
393/// - The key doesn't exist in the map
394/// - Deserialization fails
395pub fn extract_map_entry_raw(blob: &[u8], key: &str) -> Option<Vec<u8>> {
396    if blob.first().copied() != Some(TAG_MAP) {
397        return None;
398    }
399    let payload = &blob[1..];
400    let blob_map: HashMap<String, Vec<u8>> = rmp_serde::from_slice(payload).ok()?;
401    blob_map.get(key).cloned()
402}
403
404// ---------------------------------------------------------------------------
405// Internal helpers
406// ---------------------------------------------------------------------------
407
408fn encode_to_buf(value: &Value, buf: &mut Vec<u8>) {
409    match value {
410        Value::Null => {
411            buf.push(TAG_NULL);
412        }
413        Value::Bool(b) => {
414            buf.push(TAG_BOOL);
415            rmp_serde::encode::write(buf, b).expect("bool encode failed");
416        }
417        Value::Int(i) => {
418            buf.push(TAG_INT);
419            rmp_serde::encode::write(buf, i).expect("int encode failed");
420        }
421        Value::Float(f) => {
422            buf.push(TAG_FLOAT);
423            rmp_serde::encode::write(buf, f).expect("float encode failed");
424        }
425        Value::String(s) => {
426            buf.push(TAG_STRING);
427            rmp_serde::encode::write(buf, s).expect("string encode failed");
428        }
429        Value::Bytes(b) => {
430            buf.push(TAG_BYTES);
431            rmp_serde::encode::write(buf, b).expect("bytes encode failed");
432        }
433        Value::List(items) => {
434            buf.push(TAG_LIST);
435            let blobs: Vec<Vec<u8>> = items.iter().map(encode).collect();
436            rmp_serde::encode::write(buf, &blobs).expect("list encode failed");
437        }
438        Value::Map(map) => {
439            buf.push(TAG_MAP);
440            let blob_map: BTreeMap<String, Vec<u8>> =
441                map.iter().map(|(k, v)| (k.clone(), encode(v))).collect();
442            rmp_serde::encode::write(buf, &blob_map).expect("map encode failed");
443        }
444        Value::Node(node) => {
445            buf.push(TAG_NODE);
446            let mut props_blobs: Vec<(String, Vec<u8>)> = node
447                .properties
448                .iter()
449                .map(|(k, v)| (k.clone(), encode(v)))
450                .collect();
451            props_blobs.sort_by(|a, b| a.0.cmp(&b.0));
452            let payload = NodePayload {
453                vid: node.vid,
454                labels: node.labels.clone(),
455                properties: props_blobs,
456            };
457            rmp_serde::encode::write(buf, &payload).expect("node encode failed");
458        }
459        Value::Edge(edge) => {
460            buf.push(TAG_EDGE);
461            let mut props_blobs: Vec<(String, Vec<u8>)> = edge
462                .properties
463                .iter()
464                .map(|(k, v)| (k.clone(), encode(v)))
465                .collect();
466            props_blobs.sort_by(|a, b| a.0.cmp(&b.0));
467            let payload = EdgePayload {
468                eid: edge.eid,
469                edge_type: edge.edge_type.clone(),
470                src: edge.src,
471                dst: edge.dst,
472                properties: props_blobs,
473            };
474            rmp_serde::encode::write(buf, &payload).expect("edge encode failed");
475        }
476        Value::Path(path) => {
477            buf.push(TAG_PATH);
478            let nodes_blobs: Vec<Vec<u8>> = path
479                .nodes
480                .iter()
481                .map(|n| encode(&Value::Node(n.clone())))
482                .collect();
483            let edges_blobs: Vec<Vec<u8>> = path
484                .edges
485                .iter()
486                .map(|e| encode(&Value::Edge(e.clone())))
487                .collect();
488            let payload = PathPayload {
489                nodes: nodes_blobs,
490                edges: edges_blobs,
491            };
492            rmp_serde::encode::write(buf, &payload).expect("path encode failed");
493        }
494        Value::Vector(v) => {
495            buf.push(TAG_VECTOR);
496            rmp_serde::encode::write(buf, v).expect("vector encode failed");
497        }
498        Value::Temporal(t) => match t {
499            crate::value::TemporalValue::Date { days_since_epoch } => {
500                buf.push(TAG_DATE);
501                rmp_serde::encode::write(buf, days_since_epoch).expect("date encode failed");
502            }
503            crate::value::TemporalValue::LocalTime {
504                nanos_since_midnight,
505            } => {
506                buf.push(TAG_LOCALTIME);
507                rmp_serde::encode::write(buf, nanos_since_midnight)
508                    .expect("localtime encode failed");
509            }
510            crate::value::TemporalValue::Time {
511                nanos_since_midnight,
512                offset_seconds,
513            } => {
514                buf.push(TAG_TIME);
515                let payload = TimePayload {
516                    nanos: *nanos_since_midnight,
517                    offset: *offset_seconds,
518                };
519                rmp_serde::encode::write(buf, &payload).expect("time encode failed");
520            }
521            crate::value::TemporalValue::LocalDateTime { nanos_since_epoch } => {
522                buf.push(TAG_LOCALDATETIME);
523                rmp_serde::encode::write(buf, nanos_since_epoch)
524                    .expect("localdatetime encode failed");
525            }
526            crate::value::TemporalValue::DateTime {
527                nanos_since_epoch,
528                offset_seconds,
529                timezone_name,
530            } => {
531                buf.push(TAG_DATETIME);
532                let payload = DateTimePayload {
533                    nanos: *nanos_since_epoch,
534                    offset: *offset_seconds,
535                    tz_name: timezone_name.clone(),
536                };
537                rmp_serde::encode::write(buf, &payload).expect("datetime encode failed");
538            }
539            crate::value::TemporalValue::Duration {
540                months,
541                days,
542                nanos,
543            } => {
544                buf.push(TAG_DURATION);
545                let payload = DurationPayload {
546                    months: *months,
547                    days: *days,
548                    nanos: *nanos,
549                };
550                rmp_serde::encode::write(buf, &payload).expect("duration encode failed");
551            }
552        },
553    }
554}
555
556// ---------------------------------------------------------------------------
557// Serde-compatible payload structs for complex types
558// ---------------------------------------------------------------------------
559
560#[derive(Serialize, Deserialize)]
561struct NodePayload {
562    vid: Vid,
563    labels: Vec<String>,
564    properties: Vec<(String, Vec<u8>)>,
565}
566
567#[derive(Serialize, Deserialize)]
568struct EdgePayload {
569    eid: Eid,
570    edge_type: String,
571    src: Vid,
572    dst: Vid,
573    properties: Vec<(String, Vec<u8>)>,
574}
575
576#[derive(Serialize, Deserialize)]
577struct PathPayload {
578    nodes: Vec<Vec<u8>>,
579    edges: Vec<Vec<u8>>,
580}
581
582#[derive(Serialize, Deserialize)]
583struct TimePayload {
584    nanos: i64,
585    offset: i32,
586}
587
588#[derive(Serialize, Deserialize)]
589struct DateTimePayload {
590    nanos: i64,
591    offset: i32,
592    tz_name: Option<String>,
593}
594
595#[derive(Serialize, Deserialize)]
596struct DurationPayload {
597    months: i64,
598    days: i64,
599    nanos: i64,
600}
601
602// ---------------------------------------------------------------------------
603// Unit tests
604// ---------------------------------------------------------------------------
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609
610    #[test]
611    fn test_round_trip_null() {
612        let v = Value::Null;
613        let bytes = encode(&v);
614        assert_eq!(bytes[0], TAG_NULL);
615        assert_eq!(bytes.len(), 1);
616        let decoded = decode(&bytes).unwrap();
617        assert_eq!(decoded, v);
618    }
619
620    #[test]
621    fn test_round_trip_bool() {
622        for b in [true, false] {
623            let v = Value::Bool(b);
624            let bytes = encode(&v);
625            assert_eq!(bytes[0], TAG_BOOL);
626            let decoded = decode(&bytes).unwrap();
627            assert_eq!(decoded, v);
628        }
629    }
630
631    #[test]
632    fn test_round_trip_int() {
633        for i in [-100, 0, 42, i64::MAX, i64::MIN] {
634            let v = Value::Int(i);
635            let bytes = encode(&v);
636            assert_eq!(bytes[0], TAG_INT);
637            let decoded = decode(&bytes).unwrap();
638            assert_eq!(decoded, v);
639        }
640    }
641
642    #[test]
643    fn test_round_trip_float() {
644        for f in [-3.15, 0.0, 42.5, f64::MAX, f64::MIN] {
645            let v = Value::Float(f);
646            let bytes = encode(&v);
647            assert_eq!(bytes[0], TAG_FLOAT);
648            let decoded = decode(&bytes).unwrap();
649            assert_eq!(decoded, v);
650        }
651    }
652
653    #[test]
654    fn test_round_trip_string() {
655        for s in ["", "hello", "unicode: 🦀"] {
656            let v = Value::String(s.to_string());
657            let bytes = encode(&v);
658            assert_eq!(bytes[0], TAG_STRING);
659            let decoded = decode(&bytes).unwrap();
660            assert_eq!(decoded, v);
661        }
662    }
663
664    #[test]
665    fn test_round_trip_bytes() {
666        let v = Value::Bytes(vec![1, 2, 3, 255]);
667        let bytes = encode(&v);
668        assert_eq!(bytes[0], TAG_BYTES);
669        let decoded = decode(&bytes).unwrap();
670        assert_eq!(decoded, v);
671    }
672
673    #[test]
674    fn test_round_trip_list() {
675        let v = Value::List(vec![
676            Value::Int(1),
677            Value::String("two".to_string()),
678            Value::Float(3.0),
679            Value::Null,
680        ]);
681        let bytes = encode(&v);
682        assert_eq!(bytes[0], TAG_LIST);
683        let decoded = decode(&bytes).unwrap();
684        assert_eq!(decoded, v);
685    }
686
687    #[test]
688    fn test_round_trip_nested_list() {
689        let v = Value::List(vec![
690            Value::Int(1),
691            Value::List(vec![
692                Value::String("nested".to_string()),
693                Value::List(vec![Value::Bool(true)]),
694            ]),
695        ]);
696        let bytes = encode(&v);
697        let decoded = decode(&bytes).unwrap();
698        assert_eq!(decoded, v);
699    }
700
701    #[test]
702    fn test_round_trip_map() {
703        let mut map = HashMap::new();
704        map.insert("a".to_string(), Value::Int(1));
705        map.insert("b".to_string(), Value::String("two".to_string()));
706        map.insert("c".to_string(), Value::Null);
707        let v = Value::Map(map);
708        let bytes = encode(&v);
709        assert_eq!(bytes[0], TAG_MAP);
710        let decoded = decode(&bytes).unwrap();
711        assert_eq!(decoded, v);
712    }
713
714    #[test]
715    fn test_round_trip_node() {
716        let mut props = HashMap::new();
717        props.insert("name".to_string(), Value::String("Alice".to_string()));
718        props.insert("age".to_string(), Value::Int(30));
719        let v = Value::Node(Node {
720            vid: Vid::from(123),
721            labels: vec!["Person".to_string()],
722            properties: props,
723        });
724        let bytes = encode(&v);
725        assert_eq!(bytes[0], TAG_NODE);
726        let decoded = decode(&bytes).unwrap();
727        assert_eq!(decoded, v);
728    }
729
730    #[test]
731    fn test_round_trip_edge() {
732        let mut props = HashMap::new();
733        props.insert("since".to_string(), Value::Int(2020));
734        let v = Value::Edge(Edge {
735            eid: Eid::from(456),
736            edge_type: "KNOWS".to_string(),
737            src: Vid::from(1),
738            dst: Vid::from(2),
739            properties: props,
740        });
741        let bytes = encode(&v);
742        assert_eq!(bytes[0], TAG_EDGE);
743        let decoded = decode(&bytes).unwrap();
744        assert_eq!(decoded, v);
745    }
746
747    #[test]
748    fn test_round_trip_path() {
749        let v = Value::Path(Path {
750            nodes: vec![Node {
751                vid: Vid::from(1),
752                labels: vec!["A".to_string()],
753                properties: HashMap::new(),
754            }],
755            edges: vec![Edge {
756                eid: Eid::from(1),
757                edge_type: "REL".to_string(),
758                src: Vid::from(1),
759                dst: Vid::from(2),
760                properties: HashMap::new(),
761            }],
762        });
763        let bytes = encode(&v);
764        assert_eq!(bytes[0], TAG_PATH);
765        let decoded = decode(&bytes).unwrap();
766        assert_eq!(decoded, v);
767    }
768
769    #[test]
770    fn test_round_trip_vector() {
771        let v = Value::Vector(vec![0.1, 0.2, 0.3]);
772        let bytes = encode(&v);
773        assert_eq!(bytes[0], TAG_VECTOR);
774        let decoded = decode(&bytes).unwrap();
775        assert_eq!(decoded, v);
776    }
777
778    #[test]
779    fn test_peek_tag() {
780        assert_eq!(peek_tag(&encode(&Value::Null)), Some(TAG_NULL));
781        assert_eq!(peek_tag(&encode(&Value::Bool(true))), Some(TAG_BOOL));
782        assert_eq!(peek_tag(&encode(&Value::Int(42))), Some(TAG_INT));
783        assert_eq!(peek_tag(&encode(&Value::Float(3.15))), Some(TAG_FLOAT));
784        assert_eq!(
785            peek_tag(&encode(&Value::String("x".to_string()))),
786            Some(TAG_STRING)
787        );
788        assert_eq!(peek_tag(&[]), None);
789    }
790
791    #[test]
792    fn test_is_null() {
793        assert!(is_null(&encode(&Value::Null)));
794        assert!(!is_null(&encode(&Value::Int(0))));
795        assert!(!is_null(&[]));
796    }
797
798    #[test]
799    fn test_fast_decode_int() {
800        let bytes = encode(&Value::Int(42));
801        assert_eq!(decode_int(&bytes), Some(42));
802        assert_eq!(decode_int(&encode(&Value::Float(42.0))), None);
803        assert_eq!(decode_int(&encode(&Value::String("42".to_string()))), None);
804    }
805
806    #[test]
807    fn test_fast_decode_float() {
808        let bytes = encode(&Value::Float(3.15));
809        assert_eq!(decode_float(&bytes), Some(3.15));
810        assert_eq!(decode_float(&encode(&Value::Int(3))), None);
811    }
812
813    #[test]
814    fn test_fast_decode_bool() {
815        let bytes = encode(&Value::Bool(true));
816        assert_eq!(decode_bool(&bytes), Some(true));
817        assert_eq!(decode_bool(&encode(&Value::Int(1))), None);
818    }
819
820    #[test]
821    fn test_fast_decode_string() {
822        let bytes = encode(&Value::String("hello".to_string()));
823        assert_eq!(decode_string(&bytes), Some("hello".to_string()));
824        assert_eq!(decode_string(&encode(&Value::Int(42))), None);
825    }
826
827    #[test]
828    fn test_int_float_distinction() {
829        // This is the key win: JSON loses the int/float distinction
830        let int_val = Value::Int(42);
831        let float_val = Value::Float(42.0);
832
833        let int_bytes = encode(&int_val);
834        let float_bytes = encode(&float_val);
835
836        // Different tags
837        assert_eq!(int_bytes[0], TAG_INT);
838        assert_eq!(float_bytes[0], TAG_FLOAT);
839
840        // Different payloads
841        assert_ne!(int_bytes, float_bytes);
842
843        // Decode preserves distinction
844        assert_eq!(decode(&int_bytes).unwrap(), Value::Int(42));
845        assert_eq!(decode(&float_bytes).unwrap(), Value::Float(42.0));
846    }
847}