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