Skip to main content

uni_common/
value.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2024-2026 Dragonscale Team
3
4//! Typed value representation for graph properties and query results.
5//!
6//! [`Value`] is the canonical internal representation for all property values,
7//! query parameters, and expression results. Unlike `serde_json::Value`, it
8//! distinguishes integers from floats (`Int(i64)` vs `Float(f64)`) and includes
9//! graph-specific variants (`Node`, `Edge`, `Path`, `Vector`).
10//!
11//! Conversion to/from `serde_json::Value` is provided at the serialization
12//! boundary via `From` implementations.
13
14use crate::api::error::UniError;
15use crate::core::id::{Eid, Vid};
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use std::fmt;
19use std::hash::{Hash, Hasher};
20
21// ============================================================================
22// Temporal Value Types
23// ============================================================================
24
25/// Classification of temporal types for dispatch.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum TemporalType {
28    Date,
29    LocalTime,
30    Time,
31    LocalDateTime,
32    DateTime,
33    Duration,
34    Btic,
35}
36
37/// Typed temporal value representation.
38///
39/// Stores temporal values in their native numeric form for O(1) comparisons
40/// and direct Arrow column construction, with Cypher formatting applied only
41/// at the output boundary via [`std::fmt::Display`].
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub enum TemporalValue {
44    /// Date: days since Unix epoch (1970-01-01). Arrow: Date32.
45    Date { days_since_epoch: i32 },
46    /// Local time (no timezone): nanoseconds since midnight. Arrow: Time64(ns).
47    LocalTime { nanos_since_midnight: i64 },
48    /// Time with timezone offset: nanoseconds since midnight + offset. Arrow: Time64(ns) + metadata.
49    Time {
50        nanos_since_midnight: i64,
51        offset_seconds: i32,
52    },
53    /// Local datetime (no timezone): nanoseconds since Unix epoch. Arrow: Timestamp(ns, None).
54    LocalDateTime { nanos_since_epoch: i64 },
55    /// Datetime with timezone: nanoseconds since Unix epoch (UTC) + offset + optional tz name.
56    /// Arrow: Timestamp(ns, Some("UTC")).
57    DateTime {
58        nanos_since_epoch: i64,
59        offset_seconds: i32,
60        timezone_name: Option<String>,
61    },
62    /// Duration with calendar semantics: months + days + nanoseconds.
63    /// Matches Cypher's duration model which preserves calendar components.
64    Duration { months: i64, days: i64, nanos: i64 },
65    /// Binary Temporal Interval Codec: half-open `[lo, hi)` in milliseconds since epoch,
66    /// with per-bound granularity and certainty packed in a 64-bit meta word.
67    Btic { lo: i64, hi: i64, meta: u64 },
68}
69
70impl Eq for TemporalValue {}
71
72impl Hash for TemporalValue {
73    fn hash<H: Hasher>(&self, state: &mut H) {
74        std::mem::discriminant(self).hash(state);
75        match self {
76            TemporalValue::Date { days_since_epoch } => days_since_epoch.hash(state),
77            TemporalValue::LocalTime {
78                nanos_since_midnight,
79            } => nanos_since_midnight.hash(state),
80            TemporalValue::Time {
81                nanos_since_midnight,
82                offset_seconds,
83            } => {
84                nanos_since_midnight.hash(state);
85                offset_seconds.hash(state);
86            }
87            TemporalValue::LocalDateTime { nanos_since_epoch } => nanos_since_epoch.hash(state),
88            TemporalValue::DateTime {
89                nanos_since_epoch,
90                offset_seconds,
91                timezone_name,
92            } => {
93                nanos_since_epoch.hash(state);
94                offset_seconds.hash(state);
95                timezone_name.hash(state);
96            }
97            TemporalValue::Duration {
98                months,
99                days,
100                nanos,
101            } => {
102                months.hash(state);
103                days.hash(state);
104                nanos.hash(state);
105            }
106            TemporalValue::Btic { lo, hi, meta } => {
107                lo.hash(state);
108                hi.hash(state);
109                meta.hash(state);
110            }
111        }
112    }
113}
114
115impl TemporalValue {
116    /// Returns the temporal type classification.
117    pub fn temporal_type(&self) -> TemporalType {
118        match self {
119            TemporalValue::Date { .. } => TemporalType::Date,
120            TemporalValue::LocalTime { .. } => TemporalType::LocalTime,
121            TemporalValue::Time { .. } => TemporalType::Time,
122            TemporalValue::LocalDateTime { .. } => TemporalType::LocalDateTime,
123            TemporalValue::DateTime { .. } => TemporalType::DateTime,
124            TemporalValue::Duration { .. } => TemporalType::Duration,
125            TemporalValue::Btic { .. } => TemporalType::Btic,
126        }
127    }
128
129    // -----------------------------------------------------------------------
130    // Component accessors
131    // -----------------------------------------------------------------------
132
133    /// Year component, or None for time-only/duration types.
134    pub fn year(&self) -> Option<i64> {
135        self.to_date().map(|d| d.year() as i64)
136    }
137
138    /// Month component (1-12), or None for time-only/duration types.
139    pub fn month(&self) -> Option<i64> {
140        self.to_date().map(|d| d.month() as i64)
141    }
142
143    /// Day-of-month component (1-31), or None for time-only/duration types.
144    pub fn day(&self) -> Option<i64> {
145        self.to_date().map(|d| d.day() as i64)
146    }
147
148    /// Hour component (0-23), or None for date-only types.
149    pub fn hour(&self) -> Option<i64> {
150        self.to_time().map(|t| t.hour() as i64)
151    }
152
153    /// Minute component (0-59), or None for date-only types.
154    pub fn minute(&self) -> Option<i64> {
155        self.to_time().map(|t| t.minute() as i64)
156    }
157
158    /// Second component (0-59), or None for date-only types.
159    pub fn second(&self) -> Option<i64> {
160        self.to_time().map(|t| t.second() as i64)
161    }
162
163    // -----------------------------------------------------------------------
164    // Internal chrono conversion helpers
165    // -----------------------------------------------------------------------
166
167    /// Extract a NaiveDate from types that have a date component.
168    pub fn to_date(&self) -> Option<chrono::NaiveDate> {
169        let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1)?;
170        match self {
171            TemporalValue::Date { days_since_epoch } => {
172                epoch.checked_add_signed(chrono::Duration::days(*days_since_epoch as i64))
173            }
174            TemporalValue::LocalDateTime { nanos_since_epoch } => {
175                let dt = chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch);
176                Some(dt.date_naive())
177            }
178            TemporalValue::DateTime {
179                nanos_since_epoch,
180                offset_seconds,
181                ..
182            } => {
183                // Convert UTC nanos to local time by adding offset
184                let local_nanos = nanos_since_epoch + (*offset_seconds as i64) * 1_000_000_000;
185                let dt = chrono::DateTime::from_timestamp_nanos(local_nanos);
186                Some(dt.date_naive())
187            }
188            _ => None,
189        }
190    }
191
192    /// Extract a NaiveTime from types that have a time component.
193    pub fn to_time(&self) -> Option<chrono::NaiveTime> {
194        match self {
195            TemporalValue::LocalTime {
196                nanos_since_midnight,
197            }
198            | TemporalValue::Time {
199                nanos_since_midnight,
200                ..
201            } => nanos_to_time(*nanos_since_midnight),
202            TemporalValue::LocalDateTime { nanos_since_epoch } => {
203                let dt = chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch);
204                Some(dt.naive_utc().time())
205            }
206            TemporalValue::DateTime {
207                nanos_since_epoch,
208                offset_seconds,
209                ..
210            } => {
211                let local_nanos = nanos_since_epoch + (*offset_seconds as i64) * 1_000_000_000;
212                let dt = chrono::DateTime::from_timestamp_nanos(local_nanos);
213                Some(dt.naive_utc().time())
214            }
215            _ => None,
216        }
217    }
218}
219
220/// Convert nanoseconds since midnight to NaiveTime.
221fn nanos_to_time(nanos: i64) -> Option<chrono::NaiveTime> {
222    let total_secs = nanos / 1_000_000_000;
223    let h = (total_secs / 3600) as u32;
224    let m = ((total_secs % 3600) / 60) as u32;
225    let s = (total_secs % 60) as u32;
226    let ns = (nanos % 1_000_000_000) as u32;
227    chrono::NaiveTime::from_hms_nano_opt(h, m, s, ns)
228}
229
230/// Format an offset in seconds as "+HH:MM" or "Z".
231fn format_offset(offset_seconds: i32) -> String {
232    if offset_seconds == 0 {
233        return "Z".to_string();
234    }
235    format_offset_numeric(offset_seconds)
236}
237
238/// Format offset always as `+HH:MM` or `+HH:MM:SS` (never as `Z`).
239fn format_offset_numeric(offset_seconds: i32) -> String {
240    let sign = if offset_seconds >= 0 { '+' } else { '-' };
241    let abs = offset_seconds.unsigned_abs();
242    let h = abs / 3600;
243    let m = (abs % 3600) / 60;
244    let s = abs % 60;
245    if s != 0 {
246        format!("{}{:02}:{:02}:{:02}", sign, h, m, s)
247    } else {
248        format!("{}{:02}:{:02}", sign, h, m)
249    }
250}
251
252/// Format sub-second fractional part, stripping all trailing zeros.
253fn format_fractional(nanos: u32) -> String {
254    if nanos == 0 {
255        return String::new();
256    }
257    let s = format!("{:09}", nanos);
258    let trimmed = s.trim_end_matches('0');
259    format!(".{}", trimmed)
260}
261
262/// Format time as HH:MM[:SS[.n...]] — omit :SS when seconds and sub-seconds are zero.
263fn format_time_component(hour: u32, minute: u32, second: u32, nanos: u32) -> String {
264    if second == 0 && nanos == 0 {
265        format!("{:02}:{:02}", hour, minute)
266    } else {
267        let frac = format_fractional(nanos);
268        format!("{:02}:{:02}:{:02}{}", hour, minute, second, frac)
269    }
270}
271
272/// Format a NaiveTime as a canonical time string.
273fn format_naive_time(t: &chrono::NaiveTime) -> String {
274    format_time_component(t.hour(), t.minute(), t.second(), t.nanosecond())
275}
276
277/// Convert nanos since midnight to NaiveTime, defaulting to midnight on invalid input.
278fn nanos_to_time_or_midnight(nanos: i64) -> chrono::NaiveTime {
279    nanos_to_time(nanos).unwrap_or_else(|| chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap())
280}
281
282impl fmt::Display for TemporalValue {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        match self {
285            TemporalValue::Date { days_since_epoch } => {
286                let epoch = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
287                // Use a checked add (like `to_date`) so an out-of-range
288                // `days_since_epoch` degrades gracefully instead of panicking
289                // inside `Display`. On overflow, saturate to chrono's
290                // representable range so we still render a valid date string.
291                let date = epoch
292                    .checked_add_signed(chrono::Duration::days(*days_since_epoch as i64))
293                    .unwrap_or(if *days_since_epoch >= 0 {
294                        chrono::NaiveDate::MAX
295                    } else {
296                        chrono::NaiveDate::MIN
297                    });
298                write!(f, "{}", date.format("%Y-%m-%d"))
299            }
300            TemporalValue::LocalTime {
301                nanos_since_midnight,
302            } => {
303                let time = nanos_to_time_or_midnight(*nanos_since_midnight);
304                write!(f, "{}", format_naive_time(&time))
305            }
306            TemporalValue::Time {
307                nanos_since_midnight,
308                offset_seconds,
309            } => {
310                let time = nanos_to_time_or_midnight(*nanos_since_midnight);
311                write!(
312                    f,
313                    "{}{}",
314                    format_naive_time(&time),
315                    format_offset(*offset_seconds)
316                )
317            }
318            TemporalValue::LocalDateTime { nanos_since_epoch } => {
319                let ndt = chrono::DateTime::from_timestamp_nanos(*nanos_since_epoch).naive_utc();
320                write!(
321                    f,
322                    "{}T{}",
323                    ndt.date().format("%Y-%m-%d"),
324                    format_naive_time(&ndt.time())
325                )
326            }
327            TemporalValue::DateTime {
328                nanos_since_epoch,
329                offset_seconds,
330                timezone_name,
331            } => {
332                // Display in local time (UTC nanos + offset)
333                let local_nanos = nanos_since_epoch + (*offset_seconds as i64) * 1_000_000_000;
334                let ndt = chrono::DateTime::from_timestamp_nanos(local_nanos).naive_utc();
335                let tz = format_offset(*offset_seconds);
336                write!(
337                    f,
338                    "{}T{}{}",
339                    ndt.date().format("%Y-%m-%d"),
340                    format_naive_time(&ndt.time()),
341                    tz
342                )?;
343                if let Some(name) = timezone_name {
344                    write!(f, "[{}]", name)?;
345                }
346                Ok(())
347            }
348            TemporalValue::Duration {
349                months,
350                days,
351                nanos,
352            } => {
353                write!(f, "P")?;
354                let years = months / 12;
355                let rem_months = months % 12;
356                if years != 0 {
357                    write!(f, "{}Y", years)?;
358                }
359                if rem_months != 0 {
360                    write!(f, "{}M", rem_months)?;
361                }
362                if *days != 0 {
363                    write!(f, "{}D", days)?;
364                }
365                // Time part
366                let abs_nanos = nanos.unsigned_abs() as i128;
367                let nanos_sign = if *nanos < 0 { -1i64 } else { 1 };
368                let total_secs = (abs_nanos / 1_000_000_000) as i64;
369                let frac_nanos = (abs_nanos % 1_000_000_000) as u32;
370                let hours = total_secs / 3600;
371                let mins = (total_secs % 3600) / 60;
372                let secs = total_secs % 60;
373
374                if hours != 0 || mins != 0 || secs != 0 || frac_nanos != 0 {
375                    write!(f, "T")?;
376                    if hours != 0 {
377                        write!(f, "{}H", hours * nanos_sign)?;
378                    }
379                    if mins != 0 {
380                        write!(f, "{}M", mins * nanos_sign)?;
381                    }
382                    if secs != 0 || frac_nanos != 0 {
383                        let frac = format_fractional(frac_nanos);
384                        if nanos_sign < 0 && (secs != 0 || frac_nanos != 0) {
385                            write!(f, "-{}{}", secs, frac)?;
386                        } else {
387                            write!(f, "{}{}", secs, frac)?;
388                        }
389                        write!(f, "S")?;
390                    }
391                } else if years == 0 && rem_months == 0 && *days == 0 {
392                    // Zero duration
393                    write!(f, "T0S")?;
394                }
395                Ok(())
396            }
397            TemporalValue::Btic { lo, hi, meta } => match uni_btic::Btic::new(*lo, *hi, *meta) {
398                Ok(btic) => write!(f, "{btic}"),
399                Err(_) => write!(f, "Btic[lo={lo}, hi={hi}, meta={meta:#x}]"),
400            },
401        }
402    }
403}
404
405// Use chrono traits in component accessors - needed by TemporalValue accessors
406use chrono::Datelike as _;
407use chrono::Timelike as _;
408
409/// Dynamic value type for properties, parameters, and results.
410///
411/// Preserves the distinction between integers and floats, and includes
412/// graph-specific variants for nodes, edges, paths, and vectors.
413///
414/// Note: `PartialEq`, `Eq`, and `Hash` are implemented manually to support
415/// using `Value` as a HashMap key. The [`Value::Float`] arm uses a *normalized*
416/// total ordering rather than raw IEEE-754: `0.0` equals `-0.0`, and `NaN`
417/// equals `NaN` (so `Eq`'s reflexivity holds). `Hash` is consistent with this:
418/// all zeros hash alike and all NaNs hash alike. All other floats compare and
419/// hash by their (canonical) bit representation. This affects only internal
420/// bucketing — Cypher `=`/`IN`/`DISTINCT` route through `cypher_eq`, not here.
421#[derive(Debug, Clone, Serialize, Deserialize)]
422#[serde(untagged)]
423#[non_exhaustive]
424pub enum Value {
425    /// JSON/Cypher null.
426    Null,
427    /// Boolean value.
428    Bool(bool),
429    /// 64-bit signed integer.
430    Int(i64),
431    /// 64-bit floating-point number.
432    Float(f64),
433    /// UTF-8 string.
434    String(String),
435    /// Raw byte buffer.
436    Bytes(Vec<u8>),
437    /// Ordered list of values.
438    List(Vec<Value>),
439    /// String-keyed map of values.
440    Map(HashMap<String, Value>),
441
442    // Graph-specific
443    /// Graph node with VID, label, and properties.
444    Node(Node),
445    /// Graph edge with EID, type, endpoints, and properties.
446    Edge(Edge),
447    /// Graph path (alternating nodes and edges).
448    Path(Path),
449
450    // Vector
451    /// Dense float vector for similarity search.
452    Vector(Vec<f32>),
453
454    /// Learned-sparse vector (SPLADE / BGE-M3): two parallel arrays with
455    /// strictly-ascending `indices` (term ids) and parallel `values` (weights).
456    /// Holds plain fields; reconstruct the [`uni_sparse_vector::SparseVector`]
457    /// type only at boundaries (the BTIC split). Real persistence goes through
458    /// the explicit codecs, never untagged serde (which would shadow this as a
459    /// `Map`).
460    SparseVector {
461        /// Term ids, strictly ascending (sorted + unique).
462        indices: Vec<u32>,
463        /// Weights, parallel to `indices`.
464        values: Vec<f32>,
465    },
466
467    /// Binary/bit vector for Hamming/Jaccard similarity: a packed byte buffer
468    /// where each `u8` is one lane of 8 bits. Persistence goes through the
469    /// explicit `FixedSizeList<UInt8>` column and codec paths, never untagged
470    /// serde (which would shadow it as a `List`/`Bytes`).
471    BinaryVector(Vec<u8>),
472
473    // Temporal
474    /// Typed temporal value (date, time, datetime, duration).
475    Temporal(TemporalValue),
476}
477
478// ---------------------------------------------------------------------------
479// Accessor methods (mirrors serde_json::Value API for migration ease)
480// ---------------------------------------------------------------------------
481
482impl Value {
483    /// Returns `true` if this value is `Null`.
484    pub fn is_null(&self) -> bool {
485        matches!(self, Value::Null)
486    }
487
488    /// Returns the boolean if this is `Bool`, otherwise `None`.
489    pub fn as_bool(&self) -> Option<bool> {
490        match self {
491            Value::Bool(b) => Some(*b),
492            _ => None,
493        }
494    }
495
496    /// Returns the integer if this is `Int`, otherwise `None`.
497    pub fn as_i64(&self) -> Option<i64> {
498        match self {
499            Value::Int(i) => Some(*i),
500            _ => None,
501        }
502    }
503
504    /// Returns the integer as `u64` if this is a non-negative `Int`, otherwise `None`.
505    pub fn as_u64(&self) -> Option<u64> {
506        match self {
507            Value::Int(i) if *i >= 0 => Some(*i as u64),
508            _ => None,
509        }
510    }
511
512    /// Returns a float, coercing `Int` to `f64` if needed.
513    ///
514    /// Returns `None` for non-numeric variants.
515    pub fn as_f64(&self) -> Option<f64> {
516        match self {
517            Value::Float(f) => Some(*f),
518            Value::Int(i) => Some(*i as f64),
519            _ => None,
520        }
521    }
522
523    /// Returns the string slice if this is `String`, otherwise `None`.
524    pub fn as_str(&self) -> Option<&str> {
525        match self {
526            Value::String(s) => Some(s),
527            _ => None,
528        }
529    }
530
531    /// Returns `true` if this is `Int`.
532    pub fn is_i64(&self) -> bool {
533        matches!(self, Value::Int(_))
534    }
535
536    /// Returns `true` if this is `Float` (not `Int`).
537    pub fn is_f64(&self) -> bool {
538        matches!(self, Value::Float(_))
539    }
540
541    /// Returns `true` if this is `String`.
542    pub fn is_string(&self) -> bool {
543        matches!(self, Value::String(_))
544    }
545
546    /// Returns `true` if this is `Int` or `Float`.
547    pub fn is_number(&self) -> bool {
548        matches!(self, Value::Int(_) | Value::Float(_))
549    }
550
551    /// Returns the list if this is `List`, otherwise `None`.
552    pub fn as_array(&self) -> Option<&Vec<Value>> {
553        match self {
554            Value::List(l) => Some(l),
555            _ => None,
556        }
557    }
558
559    /// Returns the map if this is `Map`, otherwise `None`.
560    pub fn as_object(&self) -> Option<&HashMap<String, Value>> {
561        match self {
562            Value::Map(m) => Some(m),
563            _ => None,
564        }
565    }
566
567    /// Returns `true` if this is `Bool`.
568    pub fn is_bool(&self) -> bool {
569        matches!(self, Value::Bool(_))
570    }
571
572    /// Returns `true` if this is `List`.
573    pub fn is_list(&self) -> bool {
574        matches!(self, Value::List(_))
575    }
576
577    /// Returns `true` if this is `Map`.
578    pub fn is_map(&self) -> bool {
579        matches!(self, Value::Map(_))
580    }
581
582    /// Gets a value by key if this is a `Map`.
583    ///
584    /// Returns `None` if not a map or key doesn't exist.
585    pub fn get(&self, key: &str) -> Option<&Value> {
586        match self {
587            Value::Map(m) => m.get(key),
588            _ => None,
589        }
590    }
591
592    /// Returns `true` if this is a `Temporal` value.
593    pub fn is_temporal(&self) -> bool {
594        matches!(self, Value::Temporal(_))
595    }
596
597    /// Returns the temporal value reference if this is `Temporal`, otherwise `None`.
598    pub fn as_temporal(&self) -> Option<&TemporalValue> {
599        match self {
600            Value::Temporal(t) => Some(t),
601            _ => None,
602        }
603    }
604}
605
606impl fmt::Display for Value {
607    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
608        match self {
609            Value::Null => write!(f, "null"),
610            Value::Bool(b) => write!(f, "{b}"),
611            Value::Int(i) => write!(f, "{i}"),
612            Value::Float(v) => {
613                if v.fract() == 0.0 && v.is_finite() {
614                    write!(f, "{v:.1}")
615                } else {
616                    write!(f, "{v}")
617                }
618            }
619            Value::String(s) => write!(f, "{s}"),
620            Value::Bytes(b) => write!(f, "<{} bytes>", b.len()),
621            Value::List(l) => {
622                write!(f, "[")?;
623                for (i, item) in l.iter().enumerate() {
624                    if i > 0 {
625                        write!(f, ", ")?;
626                    }
627                    write!(f, "{item}")?;
628                }
629                write!(f, "]")
630            }
631            Value::Map(m) => {
632                write!(f, "{{")?;
633                for (i, (k, v)) in m.iter().enumerate() {
634                    if i > 0 {
635                        write!(f, ", ")?;
636                    }
637                    write!(f, "{k}: {v}")?;
638                }
639                write!(f, "}}")
640            }
641            Value::Node(n) => write!(f, "(:{} {{vid: {}}})", n.labels.join(":"), n.vid),
642            Value::Edge(e) => write!(f, "-[:{}]-", e.edge_type),
643            Value::Path(p) => write!(
644                f,
645                "<path: {} nodes, {} edges>",
646                p.nodes.len(),
647                p.edges.len()
648            ),
649            Value::Vector(v) => write!(f, "<vector: {} dims>", v.len()),
650            Value::SparseVector { indices, .. } => {
651                write!(f, "<sparse vector: {} nnz>", indices.len())
652            }
653            Value::BinaryVector(bytes) => write!(f, "<binary vector: {} lanes>", bytes.len()),
654            Value::Temporal(t) => write!(f, "{t}"),
655        }
656    }
657}
658
659// ---------------------------------------------------------------------------
660// PartialEq, Eq, and Hash implementations
661// ---------------------------------------------------------------------------
662
663/// Exact ordering of an `i64` against an `f64`, without precision loss.
664///
665/// Casting the integer to `f64` first (the naive approach) collapses distinct
666/// `i64` values above `2^53` onto the same float, so `2^53 + 1` would compare
667/// *equal* to `2^53.0`. This compares the integer against the float's exact real
668/// value instead, so the full `i64` range orders correctly against any finite
669/// float and integer/float ties resolve by true magnitude.
670///
671/// `f` is assumed non-`NaN`; callers that admit `NaN` must handle it beforehand.
672///
673/// # Examples
674/// ```
675/// use std::cmp::Ordering;
676/// use uni_common::cmp_i64_f64;
677/// assert_eq!(cmp_i64_f64(9_007_199_254_740_993, 9_007_199_254_740_992.0), Ordering::Greater);
678/// assert_eq!(cmp_i64_f64(2, 2.0), Ordering::Equal);
679/// assert_eq!(cmp_i64_f64(1, 1.5), Ordering::Less);
680/// ```
681pub fn cmp_i64_f64(i: i64, f: f64) -> std::cmp::Ordering {
682    use std::cmp::Ordering;
683    if f.is_infinite() {
684        return if f > 0.0 {
685            Ordering::Less
686        } else {
687            Ordering::Greater
688        };
689    }
690    let ff = f.floor();
691    // 2^63: every i64 is <= i64::MAX = 2^63 - 1 < 2^63 <= f, so the int is Less.
692    if ff >= 9_223_372_036_854_775_808.0 {
693        return Ordering::Less;
694    }
695    // -2^63 = i64::MIN: when floor(f) < -2^63 the int is >= i64::MIN > f.
696    if ff < -9_223_372_036_854_775_808.0 {
697        return Ordering::Greater;
698    }
699    // `ff` is integral and within [-2^63, 2^63), so this cast is exact.
700    let fi = ff as i64;
701    match i.cmp(&fi) {
702        // Integer parts equal; a positive fractional part makes `f` the larger.
703        Ordering::Equal if f > ff => Ordering::Less,
704        Ordering::Equal => Ordering::Equal,
705        other => other,
706    }
707}
708
709/// Normalized float equality used by [`Value`]'s `PartialEq`/`Hash`.
710///
711/// Treats `0.0 == -0.0` and `NaN == NaN`, so that `Value` upholds the std
712/// `Hash`/`Eq` contract (`a == b` implies `hash(a) == hash(b)`) and `Eq`'s
713/// reflexivity (`NaN == NaN`). All other floats compare via `total_cmp`, which
714/// agrees with IEEE-754 `==` on finite, non-zero values.
715fn float_eq_normalized(a: f64, b: f64) -> bool {
716    a.total_cmp(&b) == std::cmp::Ordering::Equal
717        || (a == 0.0 && b == 0.0)
718        || (a.is_nan() && b.is_nan())
719}
720
721/// `f32` counterpart of [`float_eq_normalized`], used by the `Vector` and
722/// `SparseVector` equality arms.
723///
724/// Treats `0.0 == -0.0` and `NaN == NaN` so that vector-valued [`Value`]s
725/// uphold `Eq` reflexivity and stay consistent with [`hash_f32_normalized`]
726/// (which normalizes NaN in the `Hash` impl). Without this, a
727/// `Vector`/`SparseVector` containing NaN would not equal itself while still
728/// hashing identically — silently breaking `HashSet`/`HashMap` dedup.
729fn float_eq_normalized_f32(a: f32, b: f32) -> bool {
730    a.total_cmp(&b) == std::cmp::Ordering::Equal
731        || (a == 0.0 && b == 0.0)
732        || (a.is_nan() && b.is_nan())
733}
734
735/// Compares two `f32` slices element-wise using [`float_eq_normalized_f32`].
736///
737/// Length-checked, then per-element normalized comparison — the equality
738/// analogue of the normalized hashing done for `Vec<f32>` weights.
739fn slice_eq_normalized_f32(a: &[f32], b: &[f32]) -> bool {
740    a.len() == b.len()
741        && a.iter()
742            .zip(b)
743            .all(|(x, y)| float_eq_normalized_f32(*x, *y))
744}
745
746impl PartialEq for Value {
747    /// Structural equality, with the [`Value::Float`] arm normalized so that
748    /// `0.0 == -0.0` and `NaN == NaN` (see `float_eq_normalized`).
749    ///
750    /// All non-float arms match the behavior of the former `#[derive(PartialEq)]`
751    /// exactly. Container variants (`List`, `Map`, `Node`, `Edge`, `Path`)
752    /// recurse through this same impl, so nested floats normalize too.
753    fn eq(&self, other: &Self) -> bool {
754        match (self, other) {
755            // Normalized float arm — the whole point of this hand-written impl.
756            (Value::Float(a), Value::Float(b)) => float_eq_normalized(*a, *b),
757            // All other arms reproduce the derived structural equality.
758            (Value::Null, Value::Null) => true,
759            (Value::Bool(a), Value::Bool(b)) => a == b,
760            (Value::Int(a), Value::Int(b)) => a == b,
761            (Value::String(a), Value::String(b)) => a == b,
762            (Value::Bytes(a), Value::Bytes(b)) => a == b,
763            (Value::List(a), Value::List(b)) => a == b,
764            (Value::Map(a), Value::Map(b)) => a == b,
765            (Value::Node(a), Value::Node(b)) => a == b,
766            (Value::Edge(a), Value::Edge(b)) => a == b,
767            (Value::Path(a), Value::Path(b)) => a == b,
768            // `Vec<f32>` `==` uses IEEE-754 (`NaN != NaN`), which would break
769            // `Eq` reflexivity and disagree with the NaN-normalizing `Hash`
770            // impl; compare element-wise with the same normalization instead.
771            (Value::Vector(a), Value::Vector(b)) => slice_eq_normalized_f32(a, b),
772            (
773                Value::SparseVector {
774                    indices: i1,
775                    values: v1,
776                },
777                Value::SparseVector {
778                    indices: i2,
779                    values: v2,
780                },
781            ) => i1 == i2 && slice_eq_normalized_f32(v1, v2),
782            // Exact byte buffers — native `Vec<u8>` equality (no float
783            // normalization); parallel to the `Bytes` arm.
784            (Value::BinaryVector(a), Value::BinaryVector(b)) => a == b,
785            (Value::Temporal(a), Value::Temporal(b)) => a == b,
786            // Distinct variants are never equal.
787            _ => false,
788        }
789    }
790}
791
792impl Eq for Value {}
793
794/// Hashes an `f64` with signed-zero and NaN normalization.
795///
796/// `0.0` and `-0.0` hash identically, and every NaN bit pattern hashes
797/// identically, keeping `Hash` consistent with [`float_eq_normalized`].
798fn hash_f64_normalized<H: Hasher>(f: f64, state: &mut H) {
799    let bits = if f == 0.0 {
800        0.0f64.to_bits()
801    } else if f.is_nan() {
802        f64::NAN.to_bits()
803    } else {
804        f.to_bits()
805    };
806    bits.hash(state);
807}
808
809/// Hashes an `f32` with signed-zero and NaN normalization.
810///
811/// The `f32` counterpart of [`hash_f64_normalized`], used by the `Vector` and
812/// `SparseVector` arms: `Vec<f32>` weights compare via IEEE-754 `==` (so
813/// `0.0 == -0.0`), so they must hash with the same normalization to uphold the
814/// `Hash`/`Eq` contract.
815fn hash_f32_normalized<H: Hasher>(f: f32, state: &mut H) {
816    let bits = if f == 0.0 {
817        0.0f32.to_bits()
818    } else if f.is_nan() {
819        f32::NAN.to_bits()
820    } else {
821        f.to_bits()
822    };
823    bits.hash(state);
824}
825
826impl Hash for Value {
827    fn hash<H: Hasher>(&self, state: &mut H) {
828        // Discriminant first for type safety
829        std::mem::discriminant(self).hash(state);
830        match self {
831            Value::Null => {}
832            Value::Bool(b) => b.hash(state),
833            Value::Int(i) => i.hash(state),
834            // Normalize so that `0.0`/`-0.0` hash alike and all NaNs hash alike,
835            // matching `PartialEq` (see `float_eq_normalized`) and upholding the
836            // `Hash`/`Eq` contract.
837            Value::Float(f) => hash_f64_normalized(*f, state),
838            Value::String(s) => s.hash(state),
839            Value::Bytes(b) => b.hash(state),
840            Value::List(l) => l.hash(state),
841            Value::Map(m) => hash_map(m, state),
842            Value::Node(n) => n.hash(state),
843            Value::Edge(e) => e.hash(state),
844            Value::Path(p) => p.hash(state),
845            Value::Vector(v) => {
846                // `Vec<f32>` compares via IEEE-754 `==` (so `0.0 == -0.0`); hash
847                // with the same signed-zero/NaN normalization to stay consistent.
848                v.len().hash(state);
849                for f in v {
850                    hash_f32_normalized(*f, state);
851                }
852            }
853            Value::SparseVector { indices, values } => {
854                // Parallel to the `Vector` arm: `Vec<f32>` weights compare via
855                // IEEE-754 `==`, so hash with the same signed-zero/NaN
856                // normalization to uphold the `Hash`/`Eq` contract.
857                indices.hash(state);
858                values.len().hash(state);
859                for f in values {
860                    hash_f32_normalized(*f, state);
861                }
862            }
863            // Exact byte buffer — native `Vec<u8>` hashing, consistent with the
864            // native-equality arm above.
865            Value::BinaryVector(b) => b.hash(state),
866            Value::Temporal(t) => t.hash(state),
867        }
868    }
869}
870
871// ---------------------------------------------------------------------------
872// Graph entity types
873// ---------------------------------------------------------------------------
874
875/// Helper to hash a HashMap deterministically by sorting keys.
876fn hash_map<H: Hasher>(m: &HashMap<String, Value>, state: &mut H) {
877    let mut pairs: Vec<_> = m.iter().collect();
878    pairs.sort_by_key(|(k, _)| *k);
879    pairs.len().hash(state);
880    for (k, v) in pairs {
881        k.hash(state);
882        v.hash(state);
883    }
884}
885
886/// Graph node with identity, labels, and properties.
887#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
888pub struct Node {
889    /// Internal vertex identifier.
890    pub vid: Vid,
891    /// Node labels (multi-label support).
892    pub labels: Vec<String>,
893    /// Property key-value pairs.
894    pub properties: HashMap<String, Value>,
895}
896
897impl Hash for Node {
898    fn hash<H: Hasher>(&self, state: &mut H) {
899        self.vid.hash(state);
900        let mut sorted_labels = self.labels.clone();
901        sorted_labels.sort();
902        sorted_labels.hash(state);
903        hash_map(&self.properties, state);
904    }
905}
906
907impl Node {
908    /// Gets a typed property by name.
909    ///
910    /// # Errors
911    ///
912    /// Returns `UniError::Query` if the property is missing,
913    /// or `UniError::Type` if it cannot be converted.
914    pub fn get<T: FromValue>(&self, property: &str) -> crate::Result<T> {
915        let val = self
916            .properties
917            .get(property)
918            .ok_or_else(|| UniError::Query {
919                message: format!("Property '{}' not found on node {}", property, self.vid),
920                query: None,
921            })?;
922        T::from_value(val)
923    }
924
925    /// Tries to get a typed property, returning `None` on failure.
926    pub fn try_get<T: FromValue>(&self, property: &str) -> Option<T> {
927        self.properties
928            .get(property)
929            .and_then(|v| T::from_value(v).ok())
930    }
931}
932
933/// Graph edge with identity, type, endpoints, and properties.
934#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
935pub struct Edge {
936    /// Internal edge identifier.
937    pub eid: Eid,
938    /// Relationship type name.
939    pub edge_type: String,
940    /// Source vertex ID.
941    pub src: Vid,
942    /// Destination vertex ID.
943    pub dst: Vid,
944    /// Property key-value pairs.
945    pub properties: HashMap<String, Value>,
946}
947
948impl Hash for Edge {
949    fn hash<H: Hasher>(&self, state: &mut H) {
950        self.eid.hash(state);
951        self.edge_type.hash(state);
952        self.src.hash(state);
953        self.dst.hash(state);
954        hash_map(&self.properties, state);
955    }
956}
957
958impl Edge {
959    /// Gets a typed property by name.
960    ///
961    /// # Errors
962    ///
963    /// Returns `UniError::Query` if the property is missing,
964    /// or `UniError::Type` if it cannot be converted.
965    pub fn get<T: FromValue>(&self, property: &str) -> crate::Result<T> {
966        let val = self
967            .properties
968            .get(property)
969            .ok_or_else(|| UniError::Query {
970                message: format!("Property '{}' not found on edge {}", property, self.eid),
971                query: None,
972            })?;
973        T::from_value(val)
974    }
975}
976
977/// Graph path consisting of alternating nodes and edges.
978#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
979pub struct Path {
980    /// Ordered sequence of nodes along the path.
981    pub nodes: Vec<Node>,
982    /// Ordered sequence of edges connecting the nodes.
983    #[serde(rename = "relationships")]
984    pub edges: Vec<Edge>,
985}
986
987impl Path {
988    /// Returns the nodes in this path.
989    pub fn nodes(&self) -> &[Node] {
990        &self.nodes
991    }
992
993    /// Returns the edges in this path.
994    pub fn edges(&self) -> &[Edge] {
995        &self.edges
996    }
997
998    /// Returns the number of edges (path length).
999    pub fn len(&self) -> usize {
1000        self.edges.len()
1001    }
1002
1003    /// Returns `true` if the path has no edges.
1004    pub fn is_empty(&self) -> bool {
1005        self.edges.is_empty()
1006    }
1007
1008    /// Returns the starting node, or `None` if the path is empty.
1009    pub fn start(&self) -> Option<&Node> {
1010        self.nodes.first()
1011    }
1012
1013    /// Returns the ending node, or `None` if the path is empty.
1014    pub fn end(&self) -> Option<&Node> {
1015        self.nodes.last()
1016    }
1017}
1018
1019// ---------------------------------------------------------------------------
1020// FromValue trait
1021// ---------------------------------------------------------------------------
1022
1023/// Trait for fallible conversion from [`Value`].
1024pub trait FromValue: Sized {
1025    /// Converts a `Value` reference to `Self`.
1026    ///
1027    /// # Errors
1028    ///
1029    /// Returns `UniError::Type` if the value cannot be converted.
1030    fn from_value(value: &Value) -> crate::Result<Self>;
1031}
1032
1033/// Blanket implementation: any `T: TryFrom<&Value, Error = UniError>` is `FromValue`.
1034impl<T> FromValue for T
1035where
1036    T: for<'a> TryFrom<&'a Value, Error = UniError>,
1037{
1038    fn from_value(value: &Value) -> crate::Result<Self> {
1039        Self::try_from(value)
1040    }
1041}
1042
1043// ---------------------------------------------------------------------------
1044// TryFrom<Value> macro for owned values (delegates to &Value)
1045// ---------------------------------------------------------------------------
1046
1047macro_rules! impl_try_from_value_owned {
1048    ($($t:ty),+ $(,)?) => {
1049        $(
1050            impl TryFrom<Value> for $t {
1051                type Error = UniError;
1052                fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
1053                    Self::try_from(&value)
1054                }
1055            }
1056        )+
1057    };
1058}
1059
1060impl_try_from_value_owned!(
1061    String,
1062    i64,
1063    i32,
1064    f64,
1065    bool,
1066    Vid,
1067    Eid,
1068    Vec<f32>,
1069    Path,
1070    Node,
1071    Edge
1072);
1073
1074// ---------------------------------------------------------------------------
1075// TryFrom<&Value> implementations for standard types
1076// ---------------------------------------------------------------------------
1077
1078/// Create a type mismatch error.
1079fn type_error(expected: &str, value: &Value) -> UniError {
1080    UniError::Type {
1081        expected: expected.to_string(),
1082        actual: format!("{:?}", value),
1083    }
1084}
1085
1086impl TryFrom<&Value> for String {
1087    type Error = UniError;
1088
1089    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1090        match value {
1091            Value::String(s) => Ok(s.clone()),
1092            Value::Int(i) => Ok(i.to_string()),
1093            Value::Float(f) => Ok(f.to_string()),
1094            Value::Bool(b) => Ok(b.to_string()),
1095            Value::Temporal(t) => Ok(t.to_string()),
1096            _ => Err(type_error("String", value)),
1097        }
1098    }
1099}
1100
1101impl TryFrom<&Value> for i64 {
1102    type Error = UniError;
1103
1104    // Float→i64 **truncates toward zero** (`1.9` → `1`). This is deliberate and
1105    // must not be "fixed" to match the strict `i32` impl below: this conversion
1106    // backs Cypher's `toInteger()`, whose spec truncates a float. The `i32`
1107    // impl, by contrast, is the *strict typed* coercion used for schema/storage
1108    // and rejects out-of-range or fractional floats. The two policies differ on
1109    // purpose.
1110    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1111        match value {
1112            Value::Int(i) => Ok(*i),
1113            Value::Float(f) => Ok(*f as i64),
1114            _ => Err(type_error("Int", value)),
1115        }
1116    }
1117}
1118
1119impl TryFrom<&Value> for i32 {
1120    type Error = UniError;
1121
1122    // Strict typed coercion (schema/storage): unlike the `i64`/`toInteger`
1123    // impl above, an out-of-range or fractional float is an error, not a
1124    // truncation — losing precision when narrowing into a typed column is a
1125    // bug, not a convenience.
1126    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1127        match value {
1128            Value::Int(i) => i32::try_from(*i).map_err(|_| UniError::Type {
1129                expected: "i32".to_string(),
1130                actual: format!("Integer {} out of range", i),
1131            }),
1132            Value::Float(f) => {
1133                if *f < i32::MIN as f64 || *f > i32::MAX as f64 {
1134                    return Err(UniError::Type {
1135                        expected: "i32".to_string(),
1136                        actual: format!("Float {} out of range", f),
1137                    });
1138                }
1139                if f.fract() != 0.0 {
1140                    return Err(UniError::Type {
1141                        expected: "i32".to_string(),
1142                        actual: format!("Float {} has fractional part", f),
1143                    });
1144                }
1145                Ok(*f as i32)
1146            }
1147            _ => Err(type_error("Int", value)),
1148        }
1149    }
1150}
1151
1152impl TryFrom<&Value> for f64 {
1153    type Error = UniError;
1154
1155    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1156        match value {
1157            Value::Float(f) => Ok(*f),
1158            Value::Int(i) => Ok(*i as f64),
1159            _ => Err(type_error("Float", value)),
1160        }
1161    }
1162}
1163
1164impl TryFrom<&Value> for bool {
1165    type Error = UniError;
1166
1167    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1168        match value {
1169            Value::Bool(b) => Ok(*b),
1170            _ => Err(type_error("Bool", value)),
1171        }
1172    }
1173}
1174
1175impl TryFrom<&Value> for Vid {
1176    type Error = UniError;
1177
1178    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1179        match value {
1180            Value::Node(n) => Ok(n.vid),
1181            Value::String(s) => {
1182                if let Ok(id) = s.parse::<u64>() {
1183                    return Ok(Vid::new(id));
1184                }
1185                Err(UniError::Type {
1186                    expected: "Vid".into(),
1187                    actual: s.clone(),
1188                })
1189            }
1190            Value::Int(i) => Ok(Vid::new(*i as u64)),
1191            _ => Err(type_error("Vid", value)),
1192        }
1193    }
1194}
1195
1196impl TryFrom<&Value> for Eid {
1197    type Error = UniError;
1198
1199    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1200        match value {
1201            Value::Edge(e) => Ok(e.eid),
1202            Value::String(s) => {
1203                if let Ok(id) = s.parse::<u64>() {
1204                    return Ok(Eid::new(id));
1205                }
1206                Err(UniError::Type {
1207                    expected: "Eid".into(),
1208                    actual: s.clone(),
1209                })
1210            }
1211            Value::Int(i) => Ok(Eid::new(*i as u64)),
1212            _ => Err(type_error("Eid", value)),
1213        }
1214    }
1215}
1216
1217impl TryFrom<&Value> for Vec<f32> {
1218    type Error = UniError;
1219
1220    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1221        match value {
1222            Value::Vector(v) => Ok(v.clone()),
1223            Value::List(l) => {
1224                let mut vec = Vec::with_capacity(l.len());
1225                for item in l {
1226                    match item {
1227                        Value::Float(f) => vec.push(*f as f32),
1228                        Value::Int(i) => vec.push(*i as f32),
1229                        _ => return Err(type_error("Float", item)),
1230                    }
1231                }
1232                Ok(vec)
1233            }
1234            _ => Err(type_error("Vector", value)),
1235        }
1236    }
1237}
1238
1239impl<T> TryFrom<&Value> for Option<T>
1240where
1241    T: for<'a> TryFrom<&'a Value, Error = UniError>,
1242{
1243    type Error = UniError;
1244
1245    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1246        match value {
1247            Value::Null => Ok(None),
1248            _ => T::try_from(value).map(Some),
1249        }
1250    }
1251}
1252
1253impl<T> TryFrom<Value> for Option<T>
1254where
1255    T: TryFrom<Value, Error = UniError>,
1256{
1257    type Error = UniError;
1258    fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
1259        match value {
1260            Value::Null => Ok(None),
1261            _ => T::try_from(value).map(Some),
1262        }
1263    }
1264}
1265
1266impl<T> TryFrom<&Value> for Vec<T>
1267where
1268    T: for<'a> TryFrom<&'a Value, Error = UniError>,
1269{
1270    type Error = UniError;
1271
1272    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1273        match value {
1274            Value::List(l) => {
1275                let mut vec = Vec::with_capacity(l.len());
1276                for item in l {
1277                    vec.push(T::try_from(item)?);
1278                }
1279                Ok(vec)
1280            }
1281            _ => Err(type_error("List", value)),
1282        }
1283    }
1284}
1285
1286impl<T> TryFrom<Value> for Vec<T>
1287where
1288    T: TryFrom<Value, Error = UniError>,
1289{
1290    type Error = UniError;
1291    fn try_from(value: Value) -> std::result::Result<Self, Self::Error> {
1292        match value {
1293            Value::List(l) => {
1294                let mut vec = Vec::with_capacity(l.len());
1295                for item in l {
1296                    vec.push(T::try_from(item)?);
1297                }
1298                Ok(vec)
1299            }
1300            other => Err(type_error("List", &other)),
1301        }
1302    }
1303}
1304
1305// ---------------------------------------------------------------------------
1306// TryFrom<&Value> for graph entities (deserialization from Map)
1307// ---------------------------------------------------------------------------
1308
1309/// Gets a value from a map trying alternative keys in order.
1310fn get_with_fallback<'a>(map: &'a HashMap<String, Value>, keys: &[&str]) -> Option<&'a Value> {
1311    keys.iter().find_map(|k| map.get(*k))
1312}
1313
1314/// Extracts a properties map from a value, defaulting to empty.
1315fn extract_properties(value: &Value) -> HashMap<String, Value> {
1316    match value {
1317        Value::Map(m) => m.clone(),
1318        _ => HashMap::new(),
1319    }
1320}
1321
1322impl TryFrom<&Value> for Node {
1323    type Error = UniError;
1324
1325    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1326        match value {
1327            Value::Node(n) => Ok(n.clone()),
1328            Value::Map(m) => {
1329                let vid_val = get_with_fallback(m, &["_vid", "_id", "vid"]);
1330                let props_val = m.get("properties");
1331
1332                let (Some(v), Some(p)) = (vid_val, props_val) else {
1333                    return Err(type_error("Node Map", value));
1334                };
1335
1336                // Extract labels from _labels key (List<String>)
1337                let labels = if let Some(Value::List(label_list)) = m.get("_labels") {
1338                    label_list
1339                        .iter()
1340                        .filter_map(|v| {
1341                            if let Value::String(s) = v {
1342                                Some(s.clone())
1343                            } else {
1344                                None
1345                            }
1346                        })
1347                        .collect()
1348                } else {
1349                    Vec::new()
1350                };
1351
1352                Ok(Node {
1353                    vid: Vid::try_from(v)?,
1354                    labels,
1355                    properties: extract_properties(p),
1356                })
1357            }
1358            _ => Err(type_error("Node", value)),
1359        }
1360    }
1361}
1362
1363impl TryFrom<&Value> for Edge {
1364    type Error = UniError;
1365
1366    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1367        match value {
1368            Value::Edge(e) => Ok(e.clone()),
1369            Value::Map(m) => {
1370                let eid_val = get_with_fallback(m, &["_eid", "_id", "eid"]);
1371                let type_val = get_with_fallback(m, &["_type_name", "_type", "edge_type"]);
1372                let src_val = get_with_fallback(m, &["_src", "src"]);
1373                let dst_val = get_with_fallback(m, &["_dst", "dst"]);
1374                let props_val = m.get("properties");
1375
1376                let (Some(id), Some(t), Some(s), Some(d), Some(p)) =
1377                    (eid_val, type_val, src_val, dst_val, props_val)
1378                else {
1379                    return Err(type_error("Edge Map", value));
1380                };
1381
1382                Ok(Edge {
1383                    eid: Eid::try_from(id)?,
1384                    edge_type: String::try_from(t)?,
1385                    src: Vid::try_from(s)?,
1386                    dst: Vid::try_from(d)?,
1387                    properties: extract_properties(p),
1388                })
1389            }
1390            _ => Err(type_error("Edge", value)),
1391        }
1392    }
1393}
1394
1395impl TryFrom<&Value> for Path {
1396    type Error = UniError;
1397
1398    fn try_from(value: &Value) -> std::result::Result<Self, Self::Error> {
1399        match value {
1400            Value::Path(p) => Ok(p.clone()),
1401            Value::Map(m) => {
1402                let (Some(Value::List(nodes_list)), Some(Value::List(rels_list))) =
1403                    (m.get("nodes"), m.get("relationships"))
1404                else {
1405                    return Err(type_error("Path (Map with nodes/relationships)", value));
1406                };
1407
1408                let nodes = nodes_list
1409                    .iter()
1410                    .map(Node::try_from)
1411                    .collect::<std::result::Result<Vec<_>, _>>()?;
1412
1413                let edges = rels_list
1414                    .iter()
1415                    .map(Edge::try_from)
1416                    .collect::<std::result::Result<Vec<_>, _>>()?;
1417
1418                Ok(Path { nodes, edges })
1419            }
1420            _ => Err(type_error("Path", value)),
1421        }
1422    }
1423}
1424
1425// ---------------------------------------------------------------------------
1426// From<T> for Value (primitive constructors)
1427// ---------------------------------------------------------------------------
1428
1429impl From<String> for Value {
1430    fn from(v: String) -> Self {
1431        Value::String(v)
1432    }
1433}
1434
1435impl From<&str> for Value {
1436    fn from(v: &str) -> Self {
1437        Value::String(v.to_string())
1438    }
1439}
1440
1441impl From<i64> for Value {
1442    fn from(v: i64) -> Self {
1443        Value::Int(v)
1444    }
1445}
1446
1447impl From<i32> for Value {
1448    fn from(v: i32) -> Self {
1449        Value::Int(v as i64)
1450    }
1451}
1452
1453impl From<f64> for Value {
1454    fn from(v: f64) -> Self {
1455        Value::Float(v)
1456    }
1457}
1458
1459impl From<bool> for Value {
1460    fn from(v: bool) -> Self {
1461        Value::Bool(v)
1462    }
1463}
1464
1465impl From<Vec<f32>> for Value {
1466    fn from(v: Vec<f32>) -> Self {
1467        Value::Vector(v)
1468    }
1469}
1470
1471// ---------------------------------------------------------------------------
1472// serde_json::Value ↔ Value conversions (JSONB boundary)
1473// ---------------------------------------------------------------------------
1474
1475impl From<serde_json::Value> for Value {
1476    fn from(v: serde_json::Value) -> Self {
1477        match v {
1478            serde_json::Value::Null => Value::Null,
1479            serde_json::Value::Bool(b) => Value::Bool(b),
1480            serde_json::Value::Number(n) => {
1481                if let Some(i) = n.as_i64() {
1482                    Value::Int(i)
1483                } else if let Some(f) = n.as_f64() {
1484                    Value::Float(f)
1485                } else {
1486                    Value::Null
1487                }
1488            }
1489            serde_json::Value::String(s) => Value::String(s),
1490            serde_json::Value::Array(arr) => {
1491                Value::List(arr.into_iter().map(Value::from).collect())
1492            }
1493            serde_json::Value::Object(obj) => {
1494                Value::Map(obj.into_iter().map(|(k, v)| (k, Value::from(v))).collect())
1495            }
1496        }
1497    }
1498}
1499
1500impl From<Value> for serde_json::Value {
1501    fn from(v: Value) -> Self {
1502        match v {
1503            Value::Null => serde_json::Value::Null,
1504            Value::Bool(b) => serde_json::Value::Bool(b),
1505            Value::Int(i) => serde_json::Value::Number(serde_json::Number::from(i)),
1506            Value::Float(f) => serde_json::Number::from_f64(f)
1507                .map(serde_json::Value::Number)
1508                .unwrap_or(serde_json::Value::Null), // NaN/Inf → null
1509            Value::String(s) => serde_json::Value::String(s),
1510            Value::Bytes(b) => {
1511                use base64::Engine;
1512                serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(b))
1513            }
1514            Value::List(l) => {
1515                serde_json::Value::Array(l.into_iter().map(serde_json::Value::from).collect())
1516            }
1517            Value::Map(m) => {
1518                let mut map = serde_json::Map::new();
1519                for (k, v) in m {
1520                    map.insert(k, v.into());
1521                }
1522                serde_json::Value::Object(map)
1523            }
1524            Value::Node(n) => {
1525                let mut map = serde_json::Map::new();
1526                map.insert(
1527                    "_id".to_string(),
1528                    serde_json::Value::String(n.vid.to_string()),
1529                );
1530                map.insert(
1531                    "_labels".to_string(),
1532                    serde_json::Value::Array(
1533                        n.labels
1534                            .into_iter()
1535                            .map(serde_json::Value::String)
1536                            .collect(),
1537                    ),
1538                );
1539                let props: serde_json::Value = Value::Map(n.properties).into();
1540                map.insert("properties".to_string(), props);
1541                serde_json::Value::Object(map)
1542            }
1543            Value::Edge(e) => {
1544                let mut map = serde_json::Map::new();
1545                map.insert(
1546                    "_id".to_string(),
1547                    serde_json::Value::String(e.eid.to_string()),
1548                );
1549                map.insert("_type".to_string(), serde_json::Value::String(e.edge_type));
1550                map.insert(
1551                    "_src".to_string(),
1552                    serde_json::Value::String(e.src.to_string()),
1553                );
1554                map.insert(
1555                    "_dst".to_string(),
1556                    serde_json::Value::String(e.dst.to_string()),
1557                );
1558                let props: serde_json::Value = Value::Map(e.properties).into();
1559                map.insert("properties".to_string(), props);
1560                serde_json::Value::Object(map)
1561            }
1562            Value::Path(p) => {
1563                let mut map = serde_json::Map::new();
1564                map.insert(
1565                    "nodes".to_string(),
1566                    Value::List(p.nodes.into_iter().map(Value::Node).collect()).into(),
1567                );
1568                map.insert(
1569                    "relationships".to_string(),
1570                    Value::List(p.edges.into_iter().map(Value::Edge).collect()).into(),
1571                );
1572                serde_json::Value::Object(map)
1573            }
1574            Value::Vector(v) => serde_json::Value::Array(
1575                v.into_iter()
1576                    .map(|f| {
1577                        serde_json::Number::from_f64(f as f64)
1578                            .map(serde_json::Value::Number)
1579                            .unwrap_or(serde_json::Value::Null)
1580                    })
1581                    .collect(),
1582            ),
1583            Value::SparseVector { indices, values } => {
1584                let idx = serde_json::Value::Array(
1585                    indices
1586                        .into_iter()
1587                        .map(|i| serde_json::Value::Number(serde_json::Number::from(i)))
1588                        .collect(),
1589                );
1590                let vals = serde_json::Value::Array(
1591                    values
1592                        .into_iter()
1593                        .map(|f| {
1594                            serde_json::Number::from_f64(f as f64)
1595                                .map(serde_json::Value::Number)
1596                                .unwrap_or(serde_json::Value::Null)
1597                        })
1598                        .collect(),
1599                );
1600                let mut map = serde_json::Map::new();
1601                map.insert("indices".to_string(), idx);
1602                map.insert("values".to_string(), vals);
1603                serde_json::Value::Object(map)
1604            }
1605            // Byte lanes as a JSON array of `0..=255` integers (parallel to the
1606            // dense `Vector` arm).
1607            Value::BinaryVector(bytes) => serde_json::Value::Array(
1608                bytes
1609                    .into_iter()
1610                    .map(|b| serde_json::Value::Number(serde_json::Number::from(b)))
1611                    .collect(),
1612            ),
1613            Value::Temporal(t) => serde_json::Value::String(t.to_string()),
1614        }
1615    }
1616}
1617
1618// ---------------------------------------------------------------------------
1619// unival! macro
1620// ---------------------------------------------------------------------------
1621
1622/// Constructs a [`Value`] from a literal or expression, similar to `serde_json::json!`.
1623///
1624/// # Examples
1625///
1626/// ```
1627/// use uni_common::unival;
1628/// use uni_common::Value;
1629///
1630/// let null = unival!(null);
1631/// let b = unival!(true);
1632/// let i = unival!(42);
1633/// let f = unival!(3.14);
1634/// let s = unival!("hello");
1635/// let list = unival!([1, 2, "three"]);
1636/// let map = unival!({"key": "val", "num": 42});
1637/// let expr_val = { let x: i64 = 10; unival!(x) };
1638/// ```
1639#[macro_export]
1640macro_rules! unival {
1641    // Null
1642    (null) => {
1643        $crate::Value::Null
1644    };
1645
1646    // Booleans
1647    (true) => {
1648        $crate::Value::Bool(true)
1649    };
1650    (false) => {
1651        $crate::Value::Bool(false)
1652    };
1653
1654    // Array
1655    ([ $($elem:tt),* $(,)? ]) => {
1656        $crate::Value::List(vec![ $( $crate::unival!($elem) ),* ])
1657    };
1658
1659    // Map
1660    ({ $($key:tt : $val:tt),* $(,)? }) => {
1661        $crate::Value::Map({
1662            #[allow(unused_mut)]
1663            let mut map = ::std::collections::HashMap::new();
1664            $( map.insert(($key).to_string(), $crate::unival!($val)); )*
1665            map
1666        })
1667    };
1668
1669    // Fallback: any expression — uses From<T> for Value
1670    ($e:expr) => {
1671        $crate::Value::from($e)
1672    };
1673}
1674
1675// ---------------------------------------------------------------------------
1676// Additional From impls for unival! convenience
1677// ---------------------------------------------------------------------------
1678
1679impl From<usize> for Value {
1680    fn from(v: usize) -> Self {
1681        Value::Int(v as i64)
1682    }
1683}
1684
1685impl From<u64> for Value {
1686    fn from(v: u64) -> Self {
1687        Value::Int(v as i64)
1688    }
1689}
1690
1691impl From<f32> for Value {
1692    fn from(v: f32) -> Self {
1693        Value::Float(v as f64)
1694    }
1695}
1696
1697// ---------------------------------------------------------------------------
1698// Tests
1699// ---------------------------------------------------------------------------
1700
1701#[cfg(test)]
1702mod tests {
1703    use super::*;
1704    use std::cmp::Ordering;
1705
1706    #[test]
1707    fn cmp_i64_f64_exact_above_2p53() {
1708        // 2^53 vs 2^53+1: the naive `as f64` cast collapses these to Equal.
1709        let two_p53 = 9_007_199_254_740_992.0_f64;
1710        assert_eq!(cmp_i64_f64(9_007_199_254_740_992, two_p53), Ordering::Equal);
1711        assert_eq!(
1712            cmp_i64_f64(9_007_199_254_740_993, two_p53),
1713            Ordering::Greater
1714        );
1715        assert_eq!(cmp_i64_f64(9_007_199_254_740_991, two_p53), Ordering::Less);
1716    }
1717
1718    #[test]
1719    fn cmp_i64_f64_small_and_fractional() {
1720        assert_eq!(cmp_i64_f64(2, 2.0), Ordering::Equal);
1721        assert_eq!(cmp_i64_f64(1, 1.5), Ordering::Less);
1722        assert_eq!(cmp_i64_f64(2, 1.5), Ordering::Greater);
1723        assert_eq!(cmp_i64_f64(-3, -2.5), Ordering::Less);
1724        assert_eq!(cmp_i64_f64(-2, -2.5), Ordering::Greater);
1725        assert_eq!(cmp_i64_f64(0, -0.0), Ordering::Equal);
1726    }
1727
1728    #[test]
1729    fn cmp_i64_f64_extremes_and_infinities() {
1730        assert_eq!(cmp_i64_f64(i64::MAX, f64::INFINITY), Ordering::Less);
1731        assert_eq!(cmp_i64_f64(i64::MIN, f64::NEG_INFINITY), Ordering::Greater);
1732        // i64::MAX = 2^63 - 1; the f64 2^63 is strictly larger.
1733        assert_eq!(
1734            cmp_i64_f64(i64::MAX, 9_223_372_036_854_775_808.0),
1735            Ordering::Less
1736        );
1737        // i64::MIN = -2^63, exactly representable as f64 -> Equal.
1738        assert_eq!(
1739            cmp_i64_f64(i64::MIN, -9_223_372_036_854_775_808.0),
1740            Ordering::Equal
1741        );
1742        // A float below -2^63 is smaller than any i64.
1743        assert_eq!(cmp_i64_f64(i64::MIN, -1e300), Ordering::Greater);
1744        // A huge positive float dwarfs any i64.
1745        assert_eq!(cmp_i64_f64(i64::MAX, 1e300), Ordering::Less);
1746    }
1747
1748    #[test]
1749    fn test_accessor_methods() {
1750        assert!(Value::Null.is_null());
1751        assert!(!Value::Int(1).is_null());
1752
1753        assert_eq!(Value::Bool(true).as_bool(), Some(true));
1754        assert_eq!(Value::Int(42).as_bool(), None);
1755
1756        assert_eq!(Value::Int(42).as_i64(), Some(42));
1757        assert_eq!(Value::Float(2.5).as_i64(), None);
1758
1759        // as_f64 coerces Int to Float
1760        assert_eq!(Value::Float(2.5).as_f64(), Some(2.5));
1761        assert_eq!(Value::Int(42).as_f64(), Some(42.0));
1762        assert_eq!(Value::String("x".into()).as_f64(), None);
1763
1764        assert_eq!(Value::String("hello".into()).as_str(), Some("hello"));
1765        assert_eq!(Value::Int(1).as_str(), None);
1766
1767        assert!(Value::Int(1).is_i64());
1768        assert!(!Value::Float(1.0).is_i64());
1769
1770        assert!(Value::Float(1.0).is_f64());
1771        assert!(!Value::Int(1).is_f64());
1772
1773        assert!(Value::Int(1).is_number());
1774        assert!(Value::Float(1.0).is_number());
1775        assert!(!Value::String("x".into()).is_number());
1776    }
1777
1778    #[test]
1779    fn test_serde_json_roundtrip() {
1780        let val = Value::Int(42);
1781        let json: serde_json::Value = val.clone().into();
1782        let back: Value = json.into();
1783        assert_eq!(val, back);
1784
1785        let val = Value::Float(2.5);
1786        let json: serde_json::Value = val.clone().into();
1787        let back: Value = json.into();
1788        assert_eq!(val, back);
1789
1790        let val = Value::String("hello".into());
1791        let json: serde_json::Value = val.clone().into();
1792        let back: Value = json.into();
1793        assert_eq!(val, back);
1794
1795        let val = Value::List(vec![Value::Int(1), Value::Int(2)]);
1796        let json: serde_json::Value = val.clone().into();
1797        let back: Value = json.into();
1798        assert_eq!(val, back);
1799    }
1800
1801    #[test]
1802    fn test_unival_macro() {
1803        assert_eq!(unival!(null), Value::Null);
1804        assert_eq!(unival!(true), Value::Bool(true));
1805        assert_eq!(unival!(false), Value::Bool(false));
1806        assert_eq!(unival!(42_i64), Value::Int(42));
1807        assert_eq!(unival!(2.5_f64), Value::Float(2.5));
1808        assert_eq!(unival!("hello"), Value::String("hello".into()));
1809
1810        // Array
1811        let list = unival!([1_i64, 2_i64]);
1812        assert_eq!(list, Value::List(vec![Value::Int(1), Value::Int(2)]));
1813
1814        // Map
1815        let map = unival!({"key": "val", "num": 42_i64});
1816        if let Value::Map(m) = &map {
1817            assert_eq!(m.get("key"), Some(&Value::String("val".into())));
1818            assert_eq!(m.get("num"), Some(&Value::Int(42)));
1819        } else {
1820            panic!("Expected Map");
1821        }
1822
1823        // Expression fallback
1824        let x: i64 = 99;
1825        assert_eq!(unival!(x), Value::Int(99));
1826    }
1827
1828    #[test]
1829    fn test_int_float_distinction_preserved() {
1830        // This is the key property: Int stays Int, Float stays Float
1831        let int_val = Value::Int(42);
1832        let float_val = Value::Float(42.0);
1833
1834        assert!(int_val.is_i64());
1835        assert!(!int_val.is_f64());
1836
1837        assert!(float_val.is_f64());
1838        assert!(!float_val.is_i64());
1839
1840        // They are NOT equal (different variants)
1841        assert_ne!(int_val, float_val);
1842    }
1843
1844    #[test]
1845    fn test_temporal_display_zero_seconds_omitted() {
1846        // LocalTime: 12:00 (zero seconds omitted)
1847        let lt = TemporalValue::LocalTime {
1848            nanos_since_midnight: 12 * 3600 * 1_000_000_000,
1849        };
1850        assert_eq!(lt.to_string(), "12:00");
1851
1852        // LocalTime: 12:31:14 (non-zero seconds kept)
1853        let lt2 = TemporalValue::LocalTime {
1854            nanos_since_midnight: (12 * 3600 + 31 * 60 + 14) * 1_000_000_000,
1855        };
1856        assert_eq!(lt2.to_string(), "12:31:14");
1857
1858        // LocalTime: 00:00:00.5 (zero seconds but non-zero nanos — keep seconds)
1859        let lt3 = TemporalValue::LocalTime {
1860            nanos_since_midnight: 500_000_000,
1861        };
1862        assert_eq!(lt3.to_string(), "00:00:00.5");
1863
1864        // Time: 12:00Z (zero offset uses Z)
1865        let t = TemporalValue::Time {
1866            nanos_since_midnight: 12 * 3600 * 1_000_000_000,
1867            offset_seconds: 0,
1868        };
1869        assert_eq!(t.to_string(), "12:00Z");
1870
1871        // Time: 12:31:14+01:00 (non-zero offset)
1872        let t2 = TemporalValue::Time {
1873            nanos_since_midnight: (12 * 3600 + 31 * 60 + 14) * 1_000_000_000,
1874            offset_seconds: 3600,
1875        };
1876        assert_eq!(t2.to_string(), "12:31:14+01:00");
1877
1878        // LocalDateTime: 1984-10-11T12:31 (zero seconds omitted)
1879        let epoch_nanos = chrono::NaiveDate::from_ymd_opt(1984, 10, 11)
1880            .unwrap()
1881            .and_hms_opt(12, 31, 0)
1882            .unwrap()
1883            .and_utc()
1884            .timestamp_nanos_opt()
1885            .unwrap();
1886        let ldt = TemporalValue::LocalDateTime {
1887            nanos_since_epoch: epoch_nanos,
1888        };
1889        assert_eq!(ldt.to_string(), "1984-10-11T12:31");
1890
1891        // DateTime: 1984-10-11T12:31+01:00 (zero seconds, with offset)
1892        let utc_nanos = chrono::NaiveDate::from_ymd_opt(1984, 10, 11)
1893            .unwrap()
1894            .and_hms_opt(11, 31, 0)
1895            .unwrap()
1896            .and_utc()
1897            .timestamp_nanos_opt()
1898            .unwrap();
1899        let dt = TemporalValue::DateTime {
1900            nanos_since_epoch: utc_nanos,
1901            offset_seconds: 3600,
1902            timezone_name: None,
1903        };
1904        assert_eq!(dt.to_string(), "1984-10-11T12:31+01:00");
1905
1906        // DateTime: 2015-07-21T21:40:32.142+01:00 (non-zero seconds with fractional)
1907        let utc_nanos2 = chrono::NaiveDate::from_ymd_opt(2015, 7, 21)
1908            .unwrap()
1909            .and_hms_nano_opt(20, 40, 32, 142_000_000)
1910            .unwrap()
1911            .and_utc()
1912            .timestamp_nanos_opt()
1913            .unwrap();
1914        let dt2 = TemporalValue::DateTime {
1915            nanos_since_epoch: utc_nanos2,
1916            offset_seconds: 3600,
1917            timezone_name: None,
1918        };
1919        assert_eq!(dt2.to_string(), "2015-07-21T21:40:32.142+01:00");
1920
1921        // DateTime: 1984-10-11T12:31Z (zero offset uses Z)
1922        let utc_nanos3 = chrono::NaiveDate::from_ymd_opt(1984, 10, 11)
1923            .unwrap()
1924            .and_hms_opt(12, 31, 0)
1925            .unwrap()
1926            .and_utc()
1927            .timestamp_nanos_opt()
1928            .unwrap();
1929        let dt3 = TemporalValue::DateTime {
1930            nanos_since_epoch: utc_nanos3,
1931            offset_seconds: 0,
1932            timezone_name: None,
1933        };
1934        assert_eq!(dt3.to_string(), "1984-10-11T12:31Z");
1935    }
1936
1937    #[test]
1938    fn test_temporal_display_fractional_trailing_zeros_stripped() {
1939        // Full stripping: .9 not .900
1940        let d = TemporalValue::Duration {
1941            months: 0,
1942            days: 0,
1943            nanos: 900_000_000,
1944        };
1945        assert_eq!(d.to_string(), "PT0.9S");
1946
1947        // Full stripping: .4 not .400
1948        let d2 = TemporalValue::Duration {
1949            months: 0,
1950            days: 0,
1951            nanos: 400_000_000,
1952        };
1953        assert_eq!(d2.to_string(), "PT0.4S");
1954
1955        // Millisecond precision preserved: .142
1956        let d3 = TemporalValue::Duration {
1957            months: 0,
1958            days: 0,
1959            nanos: 142_000_000,
1960        };
1961        assert_eq!(d3.to_string(), "PT0.142S");
1962
1963        // Nanosecond precision: .000000001
1964        let d4 = TemporalValue::Duration {
1965            months: 0,
1966            days: 0,
1967            nanos: 1,
1968        };
1969        assert_eq!(d4.to_string(), "PT0.000000001S");
1970    }
1971
1972    #[test]
1973    fn test_temporal_display_offset_second_precision() {
1974        // Offset with seconds: +02:05:59
1975        let t = TemporalValue::Time {
1976            nanos_since_midnight: 12 * 3600 * 1_000_000_000,
1977            offset_seconds: 2 * 3600 + 5 * 60 + 59,
1978        };
1979        assert_eq!(t.to_string(), "12:00+02:05:59");
1980
1981        // Negative offset with seconds: -02:05:07
1982        let t2 = TemporalValue::Time {
1983            nanos_since_midnight: 12 * 3600 * 1_000_000_000,
1984            offset_seconds: -(2 * 3600 + 5 * 60 + 7),
1985        };
1986        assert_eq!(t2.to_string(), "12:00-02:05:07");
1987    }
1988
1989    #[test]
1990    fn test_temporal_display_datetime_with_timezone_name() {
1991        let utc_nanos = chrono::NaiveDate::from_ymd_opt(1984, 10, 11)
1992            .unwrap()
1993            .and_hms_opt(11, 31, 0)
1994            .unwrap()
1995            .and_utc()
1996            .timestamp_nanos_opt()
1997            .unwrap();
1998        let dt = TemporalValue::DateTime {
1999            nanos_since_epoch: utc_nanos,
2000            offset_seconds: 3600,
2001            timezone_name: Some("Europe/Stockholm".to_string()),
2002        };
2003        assert_eq!(dt.to_string(), "1984-10-11T12:31+01:00[Europe/Stockholm]");
2004    }
2005
2006    /// Regression: `Value` `Hash`/`Eq` contract violation on signed-zero floats.
2007    ///
2008    /// `Value::Float` compares via IEEE-754 (`0.0 == -0.0`) but hashes via
2009    /// `f64::to_bits`, where `0.0` and `-0.0` differ. The std contract requires
2010    /// `k1 == k2` to imply `hash(k1) == hash(k2)`; violating it corrupts
2011    /// `HashMap<Vec<Value>, _>` keys used for `PARTITION BY`.
2012    #[test]
2013    fn value_hash_eq_contract_float_signed_zero() {
2014        use std::collections::hash_map::DefaultHasher;
2015        use std::hash::{Hash, Hasher};
2016
2017        fn h(v: &Value) -> u64 {
2018            let mut s = DefaultHasher::new();
2019            v.hash(&mut s);
2020            s.finish()
2021        }
2022
2023        let pos = Value::Float(0.0);
2024        let neg = Value::Float(-0.0);
2025        assert_eq!(pos, neg, "0.0 and -0.0 compare equal");
2026        assert_eq!(
2027            h(&pos),
2028            h(&neg),
2029            "equal Values must hash equally (Hash/Eq contract)"
2030        );
2031    }
2032}