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