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