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