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