Skip to main content

graphmind/graph/
property.rs

1//! # Property Value Types -- Schema-Free, Dynamically Typed Properties
2//!
3//! This module defines [`PropertyValue`], the dynamic type system that allows
4//! nodes and edges to carry arbitrary key-value properties without a fixed schema.
5//!
6//! ## Type systems in databases
7//!
8//! Relational databases enforce a rigid schema: every row in a table must have
9//! the same columns with the same types. Graph databases take a **schema-free**
10//! (or schema-optional) approach -- any node can have any set of properties,
11//! and two nodes with the same label may have entirely different property keys.
12//! This flexibility is modeled in Rust via an `enum` (algebraic data type) where
13//! each variant represents a different value type.
14//!
15//! ## Algebraic data types (tagged unions)
16//!
17//! Rust's `enum` is a **tagged union** (also called a *sum type* or *discriminated
18//! union*). Unlike C's `union`, which is unsafe because the programmer must track
19//! which variant is active, Rust's enum carries a hidden discriminant tag and the
20//! compiler enforces exhaustive `match` -- you cannot forget to handle a variant.
21//! This makes `PropertyValue` both type-safe and extensible: adding a new variant
22//! (e.g., `Duration`) causes a compiler error at every unhandled `match` site.
23//!
24//! ## Three-valued logic (3VL) and NULL semantics
25//!
26//! The `Null` variant follows SQL/Cypher NULL semantics, which implement
27//! **three-valued logic**: NULL represents an *unknown* value, not an *empty* one.
28//! Under 3VL, `NULL = NULL` evaluates to NULL (not `true`), `NULL <> 5` evaluates
29//! to NULL (not `true`), and `NULL AND true` evaluates to NULL. This propagation
30//! of unknowns through expressions is critical for correct query evaluation and
31//! is one of the most common sources of bugs in database applications.
32//!
33//! ## Type coercion
34//!
35//! The query engine performs **implicit type coercion** (widening) when comparing
36//! values of different numeric types: an `Integer(i64)` is promoted to `Float(f64)`
37//! for comparison with a `Float`. String-to-number and boolean conversions are
38//! also supported via explicit functions (`toInteger()`, `toFloat()`, `toString()`).
39//! This mirrors the behavior of OpenCypher and SQL CAST operations.
40//!
41//! ## `PartialOrd` vs `Ord`
42//!
43//! IEEE 754 floating-point numbers are only `PartialOrd` because `NaN != NaN`
44//! and `NaN` is incomparable with every value. However, databases need a **total
45//! ordering** for `ORDER BY` and indexing. This module implements `Ord` by using
46//! `f64::total_cmp()`, which defines a total order where `-0.0 < +0.0` and `NaN`
47//! sorts after all other values. `PartialOrd` delegates to `Ord` so the two are
48//! always consistent.
49//!
50//! ## Hashing floats (`Hash` for `f64`)
51//!
52//! Hashing floating-point values is notoriously tricky because IEEE 754 has
53//! multiple bit representations for the same logical value (`+0.0` and `-0.0`
54//! compare equal but have different bits, while `NaN != NaN` but may have
55//! identical bits). The `to_bits()` approach used here converts the `f64` to
56//! its raw `u64` bit pattern and hashes that. This is correct for use with our
57//! `Eq` implementation (derived `PartialEq` compares variant-by-variant) because
58//! we treat each bit pattern as distinct. The discriminant tag (0, 1, 2, ...)
59//! hashed before each value prevents cross-type collisions (e.g., `Integer(42)`
60//! vs `String("42")`).
61//!
62//! Implements REQ-GRAPH-005: Support for multiple property data types
63
64use serde::{Deserialize, Serialize};
65use std::collections::HashMap;
66use std::fmt;
67
68use std::cmp::Ordering;
69
70/// Property value type supporting multiple data types
71///
72/// Supports:
73/// - String
74/// - Integer (i64)
75/// - Float (f64)
76/// - Boolean
77/// - DateTime (as i64 timestamp)
78/// - Array (`Vec<PropertyValue>`)
79/// - Map (HashMap<String, PropertyValue>)
80/// - Vector (`Vec<f32>`) for AI/Vector search
81#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub enum PropertyValue {
83    String(String),
84    Integer(i64),
85    Float(f64),
86    Boolean(bool),
87    DateTime(i64), // Unix timestamp in milliseconds
88    Array(Vec<PropertyValue>),
89    Map(HashMap<String, PropertyValue>),
90    Vector(Vec<f32>),
91    /// Duration with months, days, seconds, nanos components (ISO 8601)
92    Duration {
93        months: i64,
94        days: i64,
95        seconds: i64,
96        nanos: i32,
97    },
98    Null,
99}
100
101impl Eq for PropertyValue {}
102
103impl PartialOrd for PropertyValue {
104    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
105        Some(self.cmp(other))
106    }
107}
108
109impl Ord for PropertyValue {
110    fn cmp(&self, other: &Self) -> Ordering {
111        use PropertyValue::*;
112        match (self, other) {
113            (Null, Null) => Ordering::Equal,
114            (Null, _) => Ordering::Less,
115            (_, Null) => Ordering::Greater,
116
117            (Boolean(a), Boolean(b)) => a.cmp(b),
118            (Boolean(_), _) => Ordering::Less,
119            (_, Boolean(_)) => Ordering::Greater,
120
121            (Integer(a), Integer(b)) => a.cmp(b),
122            (Integer(_), _) => Ordering::Less,
123            (_, Integer(_)) => Ordering::Greater,
124
125            (Float(a), Float(b)) => a.total_cmp(b),
126            (Float(_), _) => Ordering::Less,
127            (_, Float(_)) => Ordering::Greater,
128
129            (String(a), String(b)) => a.cmp(b),
130            (String(_), _) => Ordering::Less,
131            (_, String(_)) => Ordering::Greater,
132
133            (DateTime(a), DateTime(b)) => a.cmp(b),
134            (DateTime(_), _) => Ordering::Less,
135            (_, DateTime(_)) => Ordering::Greater,
136
137            (Array(a), Array(b)) => a.cmp(b),
138            (Array(_), _) => Ordering::Less,
139            (_, Array(_)) => Ordering::Greater,
140
141            // Maps are not trivially comparable because HashMap doesn't implement Ord.
142            // We'll skip Maps for indexing or define a canonical order (e.g. sorted keys).
143            // For now, let's just use memory address or length as a fallback to satisfy trait?
144            // No, that breaks determinism.
145            // Let's implement a slow but deterministic comparison for Maps.
146            (Map(a), Map(b)) => {
147                let mut keys_a: Vec<_> = a.keys().collect();
148                let mut keys_b: Vec<_> = b.keys().collect();
149                keys_a.sort();
150                keys_b.sort();
151
152                // Compare keys first
153                for (ka, kb) in keys_a.iter().zip(keys_b.iter()) {
154                    match ka.cmp(kb) {
155                        Ordering::Equal => {}
156                        ord => return ord,
157                    }
158                }
159
160                if keys_a.len() != keys_b.len() {
161                    return keys_a.len().cmp(&keys_b.len());
162                }
163
164                // Compare values
165                for k in keys_a {
166                    let va = a.get(k).unwrap();
167                    let vb = b.get(k).unwrap();
168                    match va.cmp(vb) {
169                        Ordering::Equal => {}
170                        ord => return ord,
171                    }
172                }
173                Ordering::Equal
174            }
175            (Map(_), _) => Ordering::Less,
176            (_, Map(_)) => Ordering::Greater,
177
178            (Vector(a), Vector(b)) => {
179                // Lexicographical comparison using total_cmp for floats
180                for (va, vb) in a.iter().zip(b.iter()) {
181                    match va.total_cmp(vb) {
182                        Ordering::Equal => {}
183                        ord => return ord,
184                    }
185                }
186                a.len().cmp(&b.len())
187            }
188            (Vector(_), _) => Ordering::Less,
189            (_, Vector(_)) => Ordering::Greater,
190
191            (
192                Duration {
193                    months: m1,
194                    days: d1,
195                    seconds: s1,
196                    nanos: n1,
197                },
198                Duration {
199                    months: m2,
200                    days: d2,
201                    seconds: s2,
202                    nanos: n2,
203                },
204            ) => m1
205                .cmp(m2)
206                .then(d1.cmp(d2))
207                .then(s1.cmp(s2))
208                .then(n1.cmp(n2)),
209        }
210    }
211}
212
213impl std::hash::Hash for PropertyValue {
214    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
215        match self {
216            PropertyValue::String(s) => {
217                0.hash(state);
218                s.hash(state);
219            }
220            PropertyValue::Integer(i) => {
221                1.hash(state);
222                i.hash(state);
223            }
224            PropertyValue::Float(f) => {
225                2.hash(state);
226                // Hash the bits of the float
227                f.to_bits().hash(state);
228            }
229            PropertyValue::Boolean(b) => {
230                3.hash(state);
231                b.hash(state);
232            }
233            PropertyValue::DateTime(dt) => {
234                4.hash(state);
235                dt.hash(state);
236            }
237            PropertyValue::Array(arr) => {
238                5.hash(state);
239                arr.hash(state);
240            }
241            PropertyValue::Map(map) => {
242                6.hash(state);
243                // Sort keys for deterministic hashing
244                let mut keys: Vec<_> = map.keys().collect();
245                keys.sort();
246                for k in keys {
247                    k.hash(state);
248                    map.get(k).unwrap().hash(state);
249                }
250            }
251            PropertyValue::Vector(v) => {
252                7.hash(state);
253                for val in v {
254                    val.to_bits().hash(state);
255                }
256            }
257            PropertyValue::Duration {
258                months,
259                days,
260                seconds,
261                nanos,
262            } => {
263                8.hash(state);
264                months.hash(state);
265                days.hash(state);
266                seconds.hash(state);
267                nanos.hash(state);
268            }
269            PropertyValue::Null => {
270                9.hash(state);
271            }
272        }
273    }
274}
275
276impl PropertyValue {
277    /// Check if value is null
278    pub fn is_null(&self) -> bool {
279        matches!(self, PropertyValue::Null)
280    }
281
282    /// Get string value if this is a string
283    pub fn as_string(&self) -> Option<&str> {
284        match self {
285            PropertyValue::String(s) => Some(s),
286            _ => None,
287        }
288    }
289
290    /// Get integer value if this is an integer
291    pub fn as_integer(&self) -> Option<i64> {
292        match self {
293            PropertyValue::Integer(i) => Some(*i),
294            _ => None,
295        }
296    }
297
298    /// Get float value if this is a float
299    pub fn as_float(&self) -> Option<f64> {
300        match self {
301            PropertyValue::Float(f) => Some(*f),
302            _ => None,
303        }
304    }
305
306    /// Get boolean value if this is a boolean
307    pub fn as_boolean(&self) -> Option<bool> {
308        match self {
309            PropertyValue::Boolean(b) => Some(*b),
310            _ => None,
311        }
312    }
313
314    /// Get datetime value if this is a datetime
315    pub fn as_datetime(&self) -> Option<i64> {
316        match self {
317            PropertyValue::DateTime(dt) => Some(*dt),
318            _ => None,
319        }
320    }
321
322    /// Get array value if this is an array
323    pub fn as_array(&self) -> Option<&Vec<PropertyValue>> {
324        match self {
325            PropertyValue::Array(arr) => Some(arr),
326            _ => None,
327        }
328    }
329
330    /// Get map value if this is a map
331    pub fn as_map(&self) -> Option<&HashMap<String, PropertyValue>> {
332        match self {
333            PropertyValue::Map(map) => Some(map),
334            _ => None,
335        }
336    }
337
338    /// Get vector value if this is a vector
339    pub fn as_vector(&self) -> Option<&Vec<f32>> {
340        match self {
341            PropertyValue::Vector(v) => Some(v),
342            _ => None,
343        }
344    }
345
346    /// Get type name as string
347    pub fn type_name(&self) -> &'static str {
348        match self {
349            PropertyValue::String(_) => "String",
350            PropertyValue::Integer(_) => "Integer",
351            PropertyValue::Float(_) => "Float",
352            PropertyValue::Boolean(_) => "Boolean",
353            PropertyValue::DateTime(_) => "DateTime",
354            PropertyValue::Array(_) => "Array",
355            PropertyValue::Map(_) => "Map",
356            PropertyValue::Vector(_) => "Vector",
357            PropertyValue::Duration { .. } => "Duration",
358            PropertyValue::Null => "Null",
359        }
360    }
361
362    /// Convert to a flattened JSON value for API responses
363    pub fn to_json(&self) -> serde_json::Value {
364        use serde_json::json;
365        match self {
366            PropertyValue::String(s) => json!(s),
367            PropertyValue::Integer(i) => json!(i),
368            PropertyValue::Float(f) => json!(f),
369            PropertyValue::Boolean(b) => json!(b),
370            PropertyValue::DateTime(dt) => json!(dt),
371            PropertyValue::Array(arr) => {
372                json!(arr.iter().map(|v| v.to_json()).collect::<Vec<_>>())
373            }
374            PropertyValue::Map(map) => {
375                let mut json_map = serde_json::Map::new();
376                for (k, v) in map {
377                    json_map.insert(k.clone(), v.to_json());
378                }
379                serde_json::Value::Object(json_map)
380            }
381            PropertyValue::Vector(v) => json!(v),
382            PropertyValue::Duration {
383                months,
384                days,
385                seconds,
386                nanos,
387            } => {
388                json!({
389                    "months": months,
390                    "days": days,
391                    "seconds": seconds,
392                    "nanos": nanos
393                })
394            }
395            PropertyValue::Null => serde_json::Value::Null,
396        }
397    }
398}
399
400impl fmt::Display for PropertyValue {
401    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402        match self {
403            PropertyValue::String(s) => write!(f, "\"{}\"", s),
404            PropertyValue::Integer(i) => write!(f, "{}", i),
405            PropertyValue::Float(fl) => write!(f, "{}", fl),
406            PropertyValue::Boolean(b) => write!(f, "{}", b),
407            PropertyValue::DateTime(dt) => write!(f, "DateTime({})", dt),
408            PropertyValue::Array(arr) => {
409                write!(f, "[")?;
410                for (i, val) in arr.iter().enumerate() {
411                    if i > 0 {
412                        write!(f, ", ")?;
413                    }
414                    write!(f, "{}", val)?;
415                }
416                write!(f, "]")
417            }
418            PropertyValue::Map(map) => {
419                write!(f, "{{")?;
420                for (i, (key, val)) in map.iter().enumerate() {
421                    if i > 0 {
422                        write!(f, ", ")?;
423                    }
424                    write!(f, "{}: {}", key, val)?;
425                }
426                write!(f, "}}")
427            }
428            PropertyValue::Vector(v) => {
429                write!(f, "Vector([")?;
430                for (i, val) in v.iter().enumerate() {
431                    if i > 0 {
432                        write!(f, ", ")?;
433                    }
434                    write!(f, "{}", val)?;
435                }
436                write!(f, "])")
437            }
438            PropertyValue::Duration {
439                months,
440                days,
441                seconds,
442                nanos,
443            } => {
444                write!(f, "P")?;
445                if *months > 0 {
446                    let years = months / 12;
447                    let rem_months = months % 12;
448                    if years > 0 {
449                        write!(f, "{}Y", years)?;
450                    }
451                    if rem_months > 0 {
452                        write!(f, "{}M", rem_months)?;
453                    }
454                }
455                if *days > 0 {
456                    write!(f, "{}D", days)?;
457                }
458                if *seconds > 0 || *nanos > 0 {
459                    write!(f, "T")?;
460                    let h = seconds / 3600;
461                    let m = (seconds % 3600) / 60;
462                    let s = seconds % 60;
463                    if h > 0 {
464                        write!(f, "{}H", h)?;
465                    }
466                    if m > 0 {
467                        write!(f, "{}M", m)?;
468                    }
469                    if s > 0 || *nanos > 0 {
470                        write!(f, "{}S", s)?;
471                    }
472                }
473                Ok(())
474            }
475            PropertyValue::Null => write!(f, "null"),
476        }
477    }
478}
479
480// Convenience conversions
481impl From<String> for PropertyValue {
482    fn from(s: String) -> Self {
483        PropertyValue::String(s)
484    }
485}
486
487impl From<&str> for PropertyValue {
488    fn from(s: &str) -> Self {
489        PropertyValue::String(s.to_string())
490    }
491}
492
493impl From<i64> for PropertyValue {
494    fn from(i: i64) -> Self {
495        PropertyValue::Integer(i)
496    }
497}
498
499impl From<i32> for PropertyValue {
500    fn from(i: i32) -> Self {
501        PropertyValue::Integer(i as i64)
502    }
503}
504
505impl From<f64> for PropertyValue {
506    fn from(f: f64) -> Self {
507        PropertyValue::Float(f)
508    }
509}
510
511impl From<bool> for PropertyValue {
512    fn from(b: bool) -> Self {
513        PropertyValue::Boolean(b)
514    }
515}
516
517impl From<Vec<PropertyValue>> for PropertyValue {
518    fn from(arr: Vec<PropertyValue>) -> Self {
519        PropertyValue::Array(arr)
520    }
521}
522
523impl From<HashMap<String, PropertyValue>> for PropertyValue {
524    fn from(map: HashMap<String, PropertyValue>) -> Self {
525        PropertyValue::Map(map)
526    }
527}
528
529impl From<Vec<f32>> for PropertyValue {
530    fn from(v: Vec<f32>) -> Self {
531        PropertyValue::Vector(v)
532    }
533}
534
535/// Property map for storing node and edge properties
536pub type PropertyMap = HashMap<String, PropertyValue>;
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541
542    #[test]
543    fn test_property_value_types() {
544        // Test all property types (REQ-GRAPH-005)
545        assert_eq!(
546            PropertyValue::String("test".to_string()).type_name(),
547            "String"
548        );
549        assert_eq!(PropertyValue::Integer(42).type_name(), "Integer");
550        assert_eq!(PropertyValue::Float(3.14).type_name(), "Float");
551        assert_eq!(PropertyValue::Boolean(true).type_name(), "Boolean");
552        assert_eq!(PropertyValue::DateTime(1234567890).type_name(), "DateTime");
553        assert_eq!(PropertyValue::Array(vec![]).type_name(), "Array");
554        assert_eq!(PropertyValue::Map(HashMap::new()).type_name(), "Map");
555        assert_eq!(PropertyValue::Vector(vec![0.1]).type_name(), "Vector");
556        assert_eq!(PropertyValue::Null.type_name(), "Null");
557    }
558
559    #[test]
560    fn test_property_value_conversions() {
561        let string_prop: PropertyValue = "hello".into();
562        assert_eq!(string_prop.as_string(), Some("hello"));
563
564        let int_prop: PropertyValue = 42i64.into();
565        assert_eq!(int_prop.as_integer(), Some(42));
566
567        let float_prop: PropertyValue = 3.14.into();
568        assert_eq!(float_prop.as_float(), Some(3.14));
569
570        let bool_prop: PropertyValue = true.into();
571        assert_eq!(bool_prop.as_boolean(), Some(true));
572
573        let vector_prop: PropertyValue = vec![1.0f32, 2.0f32].into();
574        assert_eq!(vector_prop.as_vector(), Some(&vec![1.0f32, 2.0f32]));
575    }
576
577    #[test]
578    fn test_property_map() {
579        let mut props = PropertyMap::new();
580        props.insert("name".to_string(), "Alice".into());
581        props.insert("age".to_string(), 30i64.into());
582        props.insert("active".to_string(), true.into());
583
584        assert_eq!(props.get("name").unwrap().as_string(), Some("Alice"));
585        assert_eq!(props.get("age").unwrap().as_integer(), Some(30));
586        assert_eq!(props.get("active").unwrap().as_boolean(), Some(true));
587    }
588
589    #[test]
590    fn test_nested_properties() {
591        // Test array property
592        let arr = vec![
593            PropertyValue::Integer(1),
594            PropertyValue::Integer(2),
595            PropertyValue::Integer(3),
596        ];
597        let arr_prop = PropertyValue::Array(arr);
598        assert_eq!(arr_prop.as_array().unwrap().len(), 3);
599
600        // Test map property
601        let mut map = HashMap::new();
602        map.insert(
603            "key".to_string(),
604            PropertyValue::String("value".to_string()),
605        );
606        let map_prop = PropertyValue::Map(map);
607        assert!(map_prop.as_map().unwrap().contains_key("key"));
608    }
609
610    // ========== Batch 1: Ord impl tests ==========
611
612    #[test]
613    fn test_ord_null_less_than_everything() {
614        assert!(PropertyValue::Null < PropertyValue::Boolean(false));
615        assert!(PropertyValue::Null < PropertyValue::Integer(0));
616        assert!(PropertyValue::Null < PropertyValue::Float(0.0));
617        assert!(PropertyValue::Null < PropertyValue::String("".to_string()));
618    }
619
620    #[test]
621    fn test_ord_null_equal() {
622        assert_eq!(
623            PropertyValue::Null.cmp(&PropertyValue::Null),
624            std::cmp::Ordering::Equal
625        );
626    }
627
628    #[test]
629    fn test_ord_boolean_less_than_integer() {
630        assert!(PropertyValue::Boolean(true) < PropertyValue::Integer(0));
631    }
632
633    #[test]
634    fn test_ord_boolean_comparison() {
635        assert!(PropertyValue::Boolean(false) < PropertyValue::Boolean(true));
636    }
637
638    #[test]
639    fn test_ord_integer_comparison() {
640        assert!(PropertyValue::Integer(1) < PropertyValue::Integer(2));
641        assert_eq!(
642            PropertyValue::Integer(5).cmp(&PropertyValue::Integer(5)),
643            std::cmp::Ordering::Equal
644        );
645    }
646
647    #[test]
648    fn test_ord_integer_less_than_float() {
649        assert!(PropertyValue::Integer(100) < PropertyValue::Float(0.0));
650    }
651
652    #[test]
653    fn test_ord_float_comparison() {
654        assert!(PropertyValue::Float(1.0) < PropertyValue::Float(2.0));
655        assert_eq!(
656            PropertyValue::Float(3.14).cmp(&PropertyValue::Float(3.14)),
657            std::cmp::Ordering::Equal
658        );
659    }
660
661    #[test]
662    fn test_ord_float_less_than_string() {
663        assert!(PropertyValue::Float(1e100) < PropertyValue::String("a".to_string()));
664    }
665
666    #[test]
667    fn test_ord_string_comparison() {
668        assert!(PropertyValue::String("a".to_string()) < PropertyValue::String("b".to_string()));
669        assert!(PropertyValue::String("abc".to_string()) > PropertyValue::String("ab".to_string()));
670    }
671
672    #[test]
673    fn test_ord_datetime_comparison() {
674        assert!(PropertyValue::DateTime(100) < PropertyValue::DateTime(200));
675    }
676
677    #[test]
678    fn test_ord_array_comparison() {
679        let a1 = PropertyValue::Array(vec![PropertyValue::Integer(1)]);
680        let a2 = PropertyValue::Array(vec![PropertyValue::Integer(2)]);
681        assert!(a1 < a2);
682    }
683
684    #[test]
685    fn test_ord_map_comparison() {
686        let mut m1 = HashMap::new();
687        m1.insert("a".to_string(), PropertyValue::Integer(1));
688        let mut m2 = HashMap::new();
689        m2.insert("a".to_string(), PropertyValue::Integer(2));
690        let pv1 = PropertyValue::Map(m1);
691        let pv2 = PropertyValue::Map(m2);
692        assert!(pv1 < pv2);
693    }
694
695    #[test]
696    fn test_ord_map_different_keys() {
697        let mut m1 = HashMap::new();
698        m1.insert("a".to_string(), PropertyValue::Integer(1));
699        let mut m2 = HashMap::new();
700        m2.insert("b".to_string(), PropertyValue::Integer(1));
701        let pv1 = PropertyValue::Map(m1);
702        let pv2 = PropertyValue::Map(m2);
703        assert!(pv1 < pv2); // "a" < "b"
704    }
705
706    #[test]
707    fn test_ord_map_different_sizes() {
708        let mut m1 = HashMap::new();
709        m1.insert("a".to_string(), PropertyValue::Integer(1));
710        let mut m2 = HashMap::new();
711        m2.insert("a".to_string(), PropertyValue::Integer(1));
712        m2.insert("b".to_string(), PropertyValue::Integer(2));
713        let pv1 = PropertyValue::Map(m1);
714        let pv2 = PropertyValue::Map(m2);
715        assert!(pv1 < pv2); // fewer keys is less
716    }
717
718    #[test]
719    fn test_ord_vector_comparison() {
720        let v1 = PropertyValue::Vector(vec![1.0, 2.0]);
721        let v2 = PropertyValue::Vector(vec![1.0, 3.0]);
722        assert!(v1 < v2);
723    }
724
725    #[test]
726    fn test_ord_vector_different_lengths() {
727        let v1 = PropertyValue::Vector(vec![1.0]);
728        let v2 = PropertyValue::Vector(vec![1.0, 2.0]);
729        assert!(v1 < v2);
730    }
731
732    #[test]
733    fn test_ord_duration_comparison() {
734        let d1 = PropertyValue::Duration {
735            months: 1,
736            days: 0,
737            seconds: 0,
738            nanos: 0,
739        };
740        let d2 = PropertyValue::Duration {
741            months: 2,
742            days: 0,
743            seconds: 0,
744            nanos: 0,
745        };
746        assert!(d1 < d2);
747    }
748
749    #[test]
750    fn test_ord_duration_tiebreak_days() {
751        let d1 = PropertyValue::Duration {
752            months: 1,
753            days: 5,
754            seconds: 0,
755            nanos: 0,
756        };
757        let d2 = PropertyValue::Duration {
758            months: 1,
759            days: 10,
760            seconds: 0,
761            nanos: 0,
762        };
763        assert!(d1 < d2);
764    }
765
766    #[test]
767    fn test_ord_duration_tiebreak_seconds() {
768        let d1 = PropertyValue::Duration {
769            months: 1,
770            days: 5,
771            seconds: 100,
772            nanos: 0,
773        };
774        let d2 = PropertyValue::Duration {
775            months: 1,
776            days: 5,
777            seconds: 200,
778            nanos: 0,
779        };
780        assert!(d1 < d2);
781    }
782
783    #[test]
784    fn test_ord_duration_tiebreak_nanos() {
785        let d1 = PropertyValue::Duration {
786            months: 1,
787            days: 5,
788            seconds: 100,
789            nanos: 10,
790        };
791        let d2 = PropertyValue::Duration {
792            months: 1,
793            days: 5,
794            seconds: 100,
795            nanos: 20,
796        };
797        assert!(d1 < d2);
798    }
799
800    #[test]
801    fn test_partial_ord_consistency() {
802        let a = PropertyValue::Integer(42);
803        let b = PropertyValue::Integer(42);
804        assert_eq!(a.partial_cmp(&b), Some(std::cmp::Ordering::Equal));
805    }
806
807    // ========== Hash impl tests ==========
808
809    #[test]
810    fn test_hash_deterministic_map() {
811        use std::collections::hash_map::DefaultHasher;
812        use std::hash::{Hash, Hasher};
813
814        let mut m1 = HashMap::new();
815        m1.insert("b".to_string(), PropertyValue::Integer(2));
816        m1.insert("a".to_string(), PropertyValue::Integer(1));
817
818        let mut m2 = HashMap::new();
819        m2.insert("a".to_string(), PropertyValue::Integer(1));
820        m2.insert("b".to_string(), PropertyValue::Integer(2));
821
822        let hash1 = {
823            let mut h = DefaultHasher::new();
824            PropertyValue::Map(m1).hash(&mut h);
825            h.finish()
826        };
827        let hash2 = {
828            let mut h = DefaultHasher::new();
829            PropertyValue::Map(m2).hash(&mut h);
830            h.finish()
831        };
832        assert_eq!(hash1, hash2);
833    }
834
835    #[test]
836    fn test_hash_deterministic_vector() {
837        use std::collections::hash_map::DefaultHasher;
838        use std::hash::{Hash, Hasher};
839
840        let v1 = PropertyValue::Vector(vec![1.0, 2.0, 3.0]);
841        let v2 = PropertyValue::Vector(vec![1.0, 2.0, 3.0]);
842
843        let hash1 = {
844            let mut h = DefaultHasher::new();
845            v1.hash(&mut h);
846            h.finish()
847        };
848        let hash2 = {
849            let mut h = DefaultHasher::new();
850            v2.hash(&mut h);
851            h.finish()
852        };
853        assert_eq!(hash1, hash2);
854    }
855
856    #[test]
857    fn test_hash_deterministic_duration() {
858        use std::collections::hash_map::DefaultHasher;
859        use std::hash::{Hash, Hasher};
860
861        let d1 = PropertyValue::Duration {
862            months: 1,
863            days: 2,
864            seconds: 3,
865            nanos: 4,
866        };
867        let d2 = PropertyValue::Duration {
868            months: 1,
869            days: 2,
870            seconds: 3,
871            nanos: 4,
872        };
873
874        let hash1 = {
875            let mut h = DefaultHasher::new();
876            d1.hash(&mut h);
877            h.finish()
878        };
879        let hash2 = {
880            let mut h = DefaultHasher::new();
881            d2.hash(&mut h);
882            h.finish()
883        };
884        assert_eq!(hash1, hash2);
885    }
886
887    #[test]
888    fn test_hash_different_types_different_hashes() {
889        use std::collections::hash_map::DefaultHasher;
890        use std::hash::{Hash, Hasher};
891
892        let int_hash = {
893            let mut h = DefaultHasher::new();
894            PropertyValue::Integer(42).hash(&mut h);
895            h.finish()
896        };
897        let str_hash = {
898            let mut h = DefaultHasher::new();
899            PropertyValue::String("42".to_string()).hash(&mut h);
900            h.finish()
901        };
902        assert_ne!(int_hash, str_hash);
903    }
904
905    #[test]
906    fn test_hash_null() {
907        use std::collections::hash_map::DefaultHasher;
908        use std::hash::{Hash, Hasher};
909
910        let hash = {
911            let mut h = DefaultHasher::new();
912            PropertyValue::Null.hash(&mut h);
913            h.finish()
914        };
915        // Just verify it doesn't panic and produces some value
916        assert!(hash > 0 || hash == 0); // always true, just exercises the code
917    }
918
919    #[test]
920    fn test_hash_float() {
921        use std::collections::hash_map::DefaultHasher;
922        use std::hash::{Hash, Hasher};
923
924        let hash1 = {
925            let mut h = DefaultHasher::new();
926            PropertyValue::Float(3.14).hash(&mut h);
927            h.finish()
928        };
929        let hash2 = {
930            let mut h = DefaultHasher::new();
931            PropertyValue::Float(3.14).hash(&mut h);
932            h.finish()
933        };
934        assert_eq!(hash1, hash2);
935    }
936
937    #[test]
938    fn test_hash_array() {
939        use std::collections::hash_map::DefaultHasher;
940        use std::hash::{Hash, Hasher};
941
942        let arr = PropertyValue::Array(vec![PropertyValue::Integer(1), PropertyValue::Integer(2)]);
943        let hash = {
944            let mut h = DefaultHasher::new();
945            arr.hash(&mut h);
946            h.finish()
947        };
948        assert!(hash > 0 || hash == 0);
949    }
950
951    #[test]
952    fn test_hash_boolean() {
953        use std::collections::hash_map::DefaultHasher;
954        use std::hash::{Hash, Hasher};
955
956        let hash1 = {
957            let mut h = DefaultHasher::new();
958            PropertyValue::Boolean(true).hash(&mut h);
959            h.finish()
960        };
961        let hash2 = {
962            let mut h = DefaultHasher::new();
963            PropertyValue::Boolean(false).hash(&mut h);
964            h.finish()
965        };
966        assert_ne!(hash1, hash2);
967    }
968
969    #[test]
970    fn test_hash_datetime() {
971        use std::collections::hash_map::DefaultHasher;
972        use std::hash::{Hash, Hasher};
973
974        let hash1 = {
975            let mut h = DefaultHasher::new();
976            PropertyValue::DateTime(12345).hash(&mut h);
977            h.finish()
978        };
979        let hash2 = {
980            let mut h = DefaultHasher::new();
981            PropertyValue::DateTime(12345).hash(&mut h);
982            h.finish()
983        };
984        assert_eq!(hash1, hash2);
985    }
986
987    // ========== Display impl tests ==========
988
989    #[test]
990    fn test_display_string() {
991        assert_eq!(
992            format!("{}", PropertyValue::String("hello".to_string())),
993            "\"hello\""
994        );
995    }
996
997    #[test]
998    fn test_display_integer() {
999        assert_eq!(format!("{}", PropertyValue::Integer(42)), "42");
1000    }
1001
1002    #[test]
1003    fn test_display_float() {
1004        assert_eq!(format!("{}", PropertyValue::Float(3.14)), "3.14");
1005    }
1006
1007    #[test]
1008    fn test_display_boolean() {
1009        assert_eq!(format!("{}", PropertyValue::Boolean(true)), "true");
1010        assert_eq!(format!("{}", PropertyValue::Boolean(false)), "false");
1011    }
1012
1013    #[test]
1014    fn test_display_datetime() {
1015        assert_eq!(
1016            format!("{}", PropertyValue::DateTime(1234567890)),
1017            "DateTime(1234567890)"
1018        );
1019    }
1020
1021    #[test]
1022    fn test_display_array() {
1023        let arr = PropertyValue::Array(vec![PropertyValue::Integer(1), PropertyValue::Integer(2)]);
1024        assert_eq!(format!("{}", arr), "[1, 2]");
1025    }
1026
1027    #[test]
1028    fn test_display_array_empty() {
1029        let arr = PropertyValue::Array(vec![]);
1030        assert_eq!(format!("{}", arr), "[]");
1031    }
1032
1033    #[test]
1034    fn test_display_null() {
1035        assert_eq!(format!("{}", PropertyValue::Null), "null");
1036    }
1037
1038    #[test]
1039    fn test_display_vector() {
1040        let v = PropertyValue::Vector(vec![1.0, 2.5]);
1041        let s = format!("{}", v);
1042        assert!(s.starts_with("Vector(["));
1043        assert!(s.contains("1"));
1044        assert!(s.contains("2.5"));
1045        assert!(s.ends_with("])"));
1046    }
1047
1048    #[test]
1049    fn test_display_map() {
1050        let mut map = HashMap::new();
1051        map.insert("key".to_string(), PropertyValue::Integer(42));
1052        let pv = PropertyValue::Map(map);
1053        let s = format!("{}", pv);
1054        assert!(s.contains("key: 42"));
1055    }
1056
1057    #[test]
1058    fn test_display_duration_years_months() {
1059        let d = PropertyValue::Duration {
1060            months: 14,
1061            days: 0,
1062            seconds: 0,
1063            nanos: 0,
1064        };
1065        let s = format!("{}", d);
1066        assert!(s.contains("1Y"));
1067        assert!(s.contains("2M"));
1068    }
1069
1070    #[test]
1071    fn test_display_duration_days_time() {
1072        let d = PropertyValue::Duration {
1073            months: 0,
1074            days: 5,
1075            seconds: 7261,
1076            nanos: 0,
1077        };
1078        let s = format!("{}", d);
1079        assert!(s.contains("5D"));
1080        assert!(s.contains("T"));
1081        assert!(s.contains("2H"));
1082        assert!(s.contains("1M"));
1083        assert!(s.contains("1S"));
1084    }
1085
1086    #[test]
1087    fn test_display_duration_empty() {
1088        let d = PropertyValue::Duration {
1089            months: 0,
1090            days: 0,
1091            seconds: 0,
1092            nanos: 0,
1093        };
1094        let s = format!("{}", d);
1095        assert_eq!(s, "P");
1096    }
1097
1098    #[test]
1099    fn test_display_duration_nanos_only() {
1100        let d = PropertyValue::Duration {
1101            months: 0,
1102            days: 0,
1103            seconds: 0,
1104            nanos: 100,
1105        };
1106        let s = format!("{}", d);
1107        assert!(s.contains("T"));
1108        assert!(s.contains("0S"));
1109    }
1110
1111    // ========== to_json tests ==========
1112
1113    #[test]
1114    fn test_to_json_string() {
1115        let json = PropertyValue::String("hello".to_string()).to_json();
1116        assert_eq!(json, serde_json::json!("hello"));
1117    }
1118
1119    #[test]
1120    fn test_to_json_integer() {
1121        let json = PropertyValue::Integer(42).to_json();
1122        assert_eq!(json, serde_json::json!(42));
1123    }
1124
1125    #[test]
1126    fn test_to_json_float() {
1127        let json = PropertyValue::Float(3.14).to_json();
1128        assert_eq!(json, serde_json::json!(3.14));
1129    }
1130
1131    #[test]
1132    fn test_to_json_boolean() {
1133        let json = PropertyValue::Boolean(true).to_json();
1134        assert_eq!(json, serde_json::json!(true));
1135    }
1136
1137    #[test]
1138    fn test_to_json_null() {
1139        let json = PropertyValue::Null.to_json();
1140        assert!(json.is_null());
1141    }
1142
1143    #[test]
1144    fn test_to_json_datetime() {
1145        let json = PropertyValue::DateTime(1234567890).to_json();
1146        assert_eq!(json, serde_json::json!(1234567890));
1147    }
1148
1149    #[test]
1150    fn test_to_json_array() {
1151        let arr = PropertyValue::Array(vec![
1152            PropertyValue::Integer(1),
1153            PropertyValue::String("x".to_string()),
1154        ]);
1155        let json = arr.to_json();
1156        assert_eq!(json, serde_json::json!([1, "x"]));
1157    }
1158
1159    #[test]
1160    fn test_to_json_map() {
1161        let mut map = HashMap::new();
1162        map.insert(
1163            "name".to_string(),
1164            PropertyValue::String("Alice".to_string()),
1165        );
1166        map.insert("age".to_string(), PropertyValue::Integer(30));
1167        let json = PropertyValue::Map(map).to_json();
1168        assert_eq!(json["name"], serde_json::json!("Alice"));
1169        assert_eq!(json["age"], serde_json::json!(30));
1170    }
1171
1172    #[test]
1173    fn test_to_json_vector() {
1174        let v = PropertyValue::Vector(vec![1.0, 2.0, 3.0]);
1175        let json = v.to_json();
1176        let arr = json.as_array().unwrap();
1177        assert_eq!(arr.len(), 3);
1178    }
1179
1180    #[test]
1181    fn test_to_json_duration() {
1182        let d = PropertyValue::Duration {
1183            months: 1,
1184            days: 2,
1185            seconds: 3,
1186            nanos: 4,
1187        };
1188        let json = d.to_json();
1189        assert_eq!(json["months"], serde_json::json!(1));
1190        assert_eq!(json["days"], serde_json::json!(2));
1191        assert_eq!(json["seconds"], serde_json::json!(3));
1192        assert_eq!(json["nanos"], serde_json::json!(4));
1193    }
1194
1195    // ========== is_null tests ==========
1196
1197    #[test]
1198    fn test_is_null_true() {
1199        assert!(PropertyValue::Null.is_null());
1200    }
1201
1202    #[test]
1203    fn test_is_null_false() {
1204        assert!(!PropertyValue::Integer(0).is_null());
1205        assert!(!PropertyValue::String("".to_string()).is_null());
1206        assert!(!PropertyValue::Boolean(false).is_null());
1207    }
1208
1209    // ========== as_* accessor negative tests ==========
1210
1211    #[test]
1212    fn test_as_string_on_non_string() {
1213        assert_eq!(PropertyValue::Integer(42).as_string(), None);
1214    }
1215
1216    #[test]
1217    fn test_as_integer_on_non_integer() {
1218        assert_eq!(PropertyValue::String("x".to_string()).as_integer(), None);
1219    }
1220
1221    #[test]
1222    fn test_as_float_on_non_float() {
1223        assert_eq!(PropertyValue::Integer(1).as_float(), None);
1224    }
1225
1226    #[test]
1227    fn test_as_boolean_on_non_boolean() {
1228        assert_eq!(PropertyValue::Integer(1).as_boolean(), None);
1229    }
1230
1231    #[test]
1232    fn test_as_datetime_on_non_datetime() {
1233        assert_eq!(PropertyValue::Integer(1).as_datetime(), None);
1234    }
1235
1236    #[test]
1237    fn test_as_array_on_non_array() {
1238        assert_eq!(PropertyValue::Integer(1).as_array(), None);
1239    }
1240
1241    #[test]
1242    fn test_as_map_on_non_map() {
1243        assert_eq!(PropertyValue::Integer(1).as_map(), None);
1244    }
1245
1246    #[test]
1247    fn test_as_vector_on_non_vector() {
1248        assert_eq!(PropertyValue::Integer(1).as_vector(), None);
1249    }
1250
1251    // ========== From conversions ==========
1252
1253    #[test]
1254    fn test_from_i32() {
1255        let pv: PropertyValue = 42i32.into();
1256        assert_eq!(pv.as_integer(), Some(42));
1257    }
1258
1259    #[test]
1260    fn test_from_string_owned() {
1261        let pv: PropertyValue = String::from("test").into();
1262        assert_eq!(pv.as_string(), Some("test"));
1263    }
1264
1265    // ========== Duration type_name ==========
1266
1267    #[test]
1268    fn test_duration_type_name() {
1269        let d = PropertyValue::Duration {
1270            months: 0,
1271            days: 0,
1272            seconds: 0,
1273            nanos: 0,
1274        };
1275        assert_eq!(d.type_name(), "Duration");
1276    }
1277
1278    // ========== Eq impl for PartialEq ==========
1279
1280    #[test]
1281    fn test_eq_same_types() {
1282        assert_eq!(PropertyValue::Integer(1), PropertyValue::Integer(1));
1283        assert_ne!(PropertyValue::Integer(1), PropertyValue::Integer(2));
1284        assert_eq!(PropertyValue::Float(1.0), PropertyValue::Float(1.0));
1285        assert_eq!(
1286            PropertyValue::String("a".to_string()),
1287            PropertyValue::String("a".to_string())
1288        );
1289        assert_ne!(
1290            PropertyValue::String("a".to_string()),
1291            PropertyValue::String("b".to_string())
1292        );
1293    }
1294
1295    #[test]
1296    fn test_eq_different_types() {
1297        assert_ne!(PropertyValue::Integer(1), PropertyValue::Float(1.0));
1298        assert_ne!(
1299            PropertyValue::Integer(1),
1300            PropertyValue::String("1".to_string())
1301        );
1302    }
1303
1304    #[test]
1305    fn test_eq_duration() {
1306        let d1 = PropertyValue::Duration {
1307            months: 1,
1308            days: 2,
1309            seconds: 3,
1310            nanos: 4,
1311        };
1312        let d2 = PropertyValue::Duration {
1313            months: 1,
1314            days: 2,
1315            seconds: 3,
1316            nanos: 4,
1317        };
1318        let d3 = PropertyValue::Duration {
1319            months: 1,
1320            days: 2,
1321            seconds: 3,
1322            nanos: 5,
1323        };
1324        assert_eq!(d1, d2);
1325        assert_ne!(d1, d3);
1326    }
1327}