Skip to main content

grafeo_common/types/
value.rs

1//! Property values and keys for nodes and edges.
2//!
3//! [`Value`] is the dynamic type that can hold any property value - strings,
4//! numbers, lists, maps, etc. [`PropertyKey`] is an interned string for
5//! efficient property lookups.
6
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9use std::fmt;
10use std::hash::{Hash, Hasher};
11use std::sync::Arc;
12
13use super::Timestamp;
14
15/// An interned property name - cheap to clone and compare.
16///
17/// Property names like "name", "age", "created_at" get used repeatedly, so
18/// we intern them with `Arc<str>`. You can create these from strings directly.
19#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
20pub struct PropertyKey(Arc<str>);
21
22impl PropertyKey {
23    /// Creates a new property key from a string.
24    #[must_use]
25    pub fn new(s: impl Into<Arc<str>>) -> Self {
26        Self(s.into())
27    }
28
29    /// Returns the string representation.
30    #[must_use]
31    pub fn as_str(&self) -> &str {
32        &self.0
33    }
34}
35
36impl fmt::Debug for PropertyKey {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "PropertyKey({:?})", self.0)
39    }
40}
41
42impl fmt::Display for PropertyKey {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(f, "{}", self.0)
45    }
46}
47
48impl From<&str> for PropertyKey {
49    fn from(s: &str) -> Self {
50        Self::new(s)
51    }
52}
53
54impl From<String> for PropertyKey {
55    fn from(s: String) -> Self {
56        Self::new(s)
57    }
58}
59
60impl AsRef<str> for PropertyKey {
61    fn as_ref(&self) -> &str {
62        &self.0
63    }
64}
65
66/// A dynamically-typed property value.
67///
68/// Nodes and edges can have properties of various types - this enum holds
69/// them all. Follows the GQL type system, so you can store nulls, booleans,
70/// numbers, strings, timestamps, lists, and maps.
71///
72/// # Examples
73///
74/// ```
75/// use grafeo_common::types::Value;
76///
77/// let name = Value::from("Alice");
78/// let age = Value::from(30i64);
79/// let active = Value::from(true);
80///
81/// // Check types
82/// assert!(name.as_str().is_some());
83/// assert_eq!(age.as_int64(), Some(30));
84/// ```
85#[derive(Clone, PartialEq, Serialize, Deserialize)]
86pub enum Value {
87    /// Null/missing value
88    Null,
89
90    /// Boolean value
91    Bool(bool),
92
93    /// 64-bit signed integer
94    Int64(i64),
95
96    /// 64-bit floating point
97    Float64(f64),
98
99    /// UTF-8 string (uses Arc for cheap cloning)
100    String(Arc<str>),
101
102    /// Binary data
103    Bytes(Arc<[u8]>),
104
105    /// Timestamp with timezone
106    Timestamp(Timestamp),
107
108    /// Ordered list of values
109    List(Arc<[Value]>),
110
111    /// Key-value map (uses BTreeMap for deterministic ordering)
112    Map(Arc<BTreeMap<PropertyKey, Value>>),
113}
114
115impl Value {
116    /// Returns `true` if this value is null.
117    #[inline]
118    #[must_use]
119    pub const fn is_null(&self) -> bool {
120        matches!(self, Value::Null)
121    }
122
123    /// Returns the boolean value if this is a Bool, otherwise None.
124    #[inline]
125    #[must_use]
126    pub const fn as_bool(&self) -> Option<bool> {
127        match self {
128            Value::Bool(b) => Some(*b),
129            _ => None,
130        }
131    }
132
133    /// Returns the integer value if this is an Int64, otherwise None.
134    #[inline]
135    #[must_use]
136    pub const fn as_int64(&self) -> Option<i64> {
137        match self {
138            Value::Int64(i) => Some(*i),
139            _ => None,
140        }
141    }
142
143    /// Returns the float value if this is a Float64, otherwise None.
144    #[inline]
145    #[must_use]
146    pub const fn as_float64(&self) -> Option<f64> {
147        match self {
148            Value::Float64(f) => Some(*f),
149            _ => None,
150        }
151    }
152
153    /// Returns the string value if this is a String, otherwise None.
154    #[inline]
155    #[must_use]
156    pub fn as_str(&self) -> Option<&str> {
157        match self {
158            Value::String(s) => Some(s),
159            _ => None,
160        }
161    }
162
163    /// Returns the bytes value if this is Bytes, otherwise None.
164    #[inline]
165    #[must_use]
166    pub fn as_bytes(&self) -> Option<&[u8]> {
167        match self {
168            Value::Bytes(b) => Some(b),
169            _ => None,
170        }
171    }
172
173    /// Returns the timestamp value if this is a Timestamp, otherwise None.
174    #[inline]
175    #[must_use]
176    pub const fn as_timestamp(&self) -> Option<Timestamp> {
177        match self {
178            Value::Timestamp(t) => Some(*t),
179            _ => None,
180        }
181    }
182
183    /// Returns the list value if this is a List, otherwise None.
184    #[inline]
185    #[must_use]
186    pub fn as_list(&self) -> Option<&[Value]> {
187        match self {
188            Value::List(l) => Some(l),
189            _ => None,
190        }
191    }
192
193    /// Returns the map value if this is a Map, otherwise None.
194    #[inline]
195    #[must_use]
196    pub fn as_map(&self) -> Option<&BTreeMap<PropertyKey, Value>> {
197        match self {
198            Value::Map(m) => Some(m),
199            _ => None,
200        }
201    }
202
203    /// Returns the type name of this value.
204    #[must_use]
205    pub const fn type_name(&self) -> &'static str {
206        match self {
207            Value::Null => "NULL",
208            Value::Bool(_) => "BOOL",
209            Value::Int64(_) => "INT64",
210            Value::Float64(_) => "FLOAT64",
211            Value::String(_) => "STRING",
212            Value::Bytes(_) => "BYTES",
213            Value::Timestamp(_) => "TIMESTAMP",
214            Value::List(_) => "LIST",
215            Value::Map(_) => "MAP",
216        }
217    }
218
219    /// Serializes this value to bytes.
220    #[must_use]
221    pub fn serialize(&self) -> Vec<u8> {
222        bincode::serde::encode_to_vec(self, bincode::config::standard())
223            .expect("Value serialization should not fail")
224    }
225
226    /// Deserializes a value from bytes.
227    ///
228    /// # Errors
229    ///
230    /// Returns an error if the bytes do not represent a valid Value.
231    pub fn deserialize(bytes: &[u8]) -> Result<Self, bincode::error::DecodeError> {
232        let (value, _) = bincode::serde::decode_from_slice(bytes, bincode::config::standard())?;
233        Ok(value)
234    }
235}
236
237impl fmt::Debug for Value {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        match self {
240            Value::Null => write!(f, "Null"),
241            Value::Bool(b) => write!(f, "Bool({b})"),
242            Value::Int64(i) => write!(f, "Int64({i})"),
243            Value::Float64(fl) => write!(f, "Float64({fl})"),
244            Value::String(s) => write!(f, "String({s:?})"),
245            Value::Bytes(b) => write!(f, "Bytes([{}; {} bytes])", b.first().unwrap_or(&0), b.len()),
246            Value::Timestamp(t) => write!(f, "Timestamp({t:?})"),
247            Value::List(l) => write!(f, "List({l:?})"),
248            Value::Map(m) => write!(f, "Map({m:?})"),
249        }
250    }
251}
252
253impl fmt::Display for Value {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        match self {
256            Value::Null => write!(f, "NULL"),
257            Value::Bool(b) => write!(f, "{b}"),
258            Value::Int64(i) => write!(f, "{i}"),
259            Value::Float64(fl) => write!(f, "{fl}"),
260            Value::String(s) => write!(f, "{s:?}"),
261            Value::Bytes(b) => write!(f, "<bytes: {} bytes>", b.len()),
262            Value::Timestamp(t) => write!(f, "{t}"),
263            Value::List(l) => {
264                write!(f, "[")?;
265                for (i, v) in l.iter().enumerate() {
266                    if i > 0 {
267                        write!(f, ", ")?;
268                    }
269                    write!(f, "{v}")?;
270                }
271                write!(f, "]")
272            }
273            Value::Map(m) => {
274                write!(f, "{{")?;
275                for (i, (k, v)) in m.iter().enumerate() {
276                    if i > 0 {
277                        write!(f, ", ")?;
278                    }
279                    write!(f, "{k}: {v}")?;
280                }
281                write!(f, "}}")
282            }
283        }
284    }
285}
286
287// Convenient From implementations
288impl From<bool> for Value {
289    fn from(b: bool) -> Self {
290        Value::Bool(b)
291    }
292}
293
294impl From<i64> for Value {
295    fn from(i: i64) -> Self {
296        Value::Int64(i)
297    }
298}
299
300impl From<i32> for Value {
301    fn from(i: i32) -> Self {
302        Value::Int64(i64::from(i))
303    }
304}
305
306impl From<f64> for Value {
307    fn from(f: f64) -> Self {
308        Value::Float64(f)
309    }
310}
311
312impl From<f32> for Value {
313    fn from(f: f32) -> Self {
314        Value::Float64(f64::from(f))
315    }
316}
317
318impl From<&str> for Value {
319    fn from(s: &str) -> Self {
320        Value::String(s.into())
321    }
322}
323
324impl From<String> for Value {
325    fn from(s: String) -> Self {
326        Value::String(s.into())
327    }
328}
329
330impl From<Arc<str>> for Value {
331    fn from(s: Arc<str>) -> Self {
332        Value::String(s)
333    }
334}
335
336impl From<Vec<u8>> for Value {
337    fn from(b: Vec<u8>) -> Self {
338        Value::Bytes(b.into())
339    }
340}
341
342impl From<&[u8]> for Value {
343    fn from(b: &[u8]) -> Self {
344        Value::Bytes(b.into())
345    }
346}
347
348impl From<Timestamp> for Value {
349    fn from(t: Timestamp) -> Self {
350        Value::Timestamp(t)
351    }
352}
353
354impl<T: Into<Value>> From<Vec<T>> for Value {
355    fn from(v: Vec<T>) -> Self {
356        Value::List(v.into_iter().map(Into::into).collect())
357    }
358}
359
360impl<T: Into<Value>> From<Option<T>> for Value {
361    fn from(opt: Option<T>) -> Self {
362        match opt {
363            Some(v) => v.into(),
364            None => Value::Null,
365        }
366    }
367}
368
369/// A hashable wrapper around [`Value`] for use in hash-based indexes.
370///
371/// `Value` itself cannot implement `Hash` because it contains `f64` (which has
372/// NaN issues). This wrapper converts floats to their bit representation for
373/// hashing, allowing values to be used as keys in hash maps and sets.
374///
375/// # Note on Float Equality
376///
377/// Two `HashableValue`s containing `f64` are considered equal if they have
378/// identical bit representations. This means `NaN == NaN` (same bits) and
379/// positive/negative zero are considered different.
380#[derive(Clone, Debug)]
381pub struct HashableValue(pub Value);
382
383impl HashableValue {
384    /// Creates a new hashable value from a value.
385    #[must_use]
386    pub fn new(value: Value) -> Self {
387        Self(value)
388    }
389
390    /// Returns a reference to the inner value.
391    #[must_use]
392    pub fn inner(&self) -> &Value {
393        &self.0
394    }
395
396    /// Consumes the wrapper and returns the inner value.
397    #[must_use]
398    pub fn into_inner(self) -> Value {
399        self.0
400    }
401}
402
403impl Hash for HashableValue {
404    fn hash<H: Hasher>(&self, state: &mut H) {
405        // Hash the discriminant first
406        std::mem::discriminant(&self.0).hash(state);
407
408        match &self.0 {
409            Value::Null => {}
410            Value::Bool(b) => b.hash(state),
411            Value::Int64(i) => i.hash(state),
412            Value::Float64(f) => {
413                // Use bit representation for hashing floats
414                f.to_bits().hash(state);
415            }
416            Value::String(s) => s.hash(state),
417            Value::Bytes(b) => b.hash(state),
418            Value::Timestamp(t) => t.hash(state),
419            Value::List(l) => {
420                l.len().hash(state);
421                for v in l.iter() {
422                    HashableValue(v.clone()).hash(state);
423                }
424            }
425            Value::Map(m) => {
426                m.len().hash(state);
427                for (k, v) in m.iter() {
428                    k.hash(state);
429                    HashableValue(v.clone()).hash(state);
430                }
431            }
432        }
433    }
434}
435
436impl PartialEq for HashableValue {
437    fn eq(&self, other: &Self) -> bool {
438        match (&self.0, &other.0) {
439            (Value::Float64(a), Value::Float64(b)) => {
440                // Compare by bits for consistent hash/eq behavior
441                a.to_bits() == b.to_bits()
442            }
443            (Value::List(a), Value::List(b)) => {
444                if a.len() != b.len() {
445                    return false;
446                }
447                a.iter()
448                    .zip(b.iter())
449                    .all(|(x, y)| HashableValue(x.clone()) == HashableValue(y.clone()))
450            }
451            (Value::Map(a), Value::Map(b)) => {
452                if a.len() != b.len() {
453                    return false;
454                }
455                a.iter().all(|(k, v)| {
456                    b.get(k)
457                        .is_some_and(|bv| HashableValue(v.clone()) == HashableValue(bv.clone()))
458                })
459            }
460            // For other types, use normal Value equality
461            _ => self.0 == other.0,
462        }
463    }
464}
465
466impl Eq for HashableValue {}
467
468impl From<Value> for HashableValue {
469    fn from(value: Value) -> Self {
470        Self(value)
471    }
472}
473
474impl From<HashableValue> for Value {
475    fn from(hv: HashableValue) -> Self {
476        hv.0
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483
484    #[test]
485    fn test_value_type_checks() {
486        assert!(Value::Null.is_null());
487        assert!(!Value::Bool(true).is_null());
488
489        assert_eq!(Value::Bool(true).as_bool(), Some(true));
490        assert_eq!(Value::Bool(false).as_bool(), Some(false));
491        assert_eq!(Value::Int64(42).as_bool(), None);
492
493        assert_eq!(Value::Int64(42).as_int64(), Some(42));
494        assert_eq!(Value::String("test".into()).as_int64(), None);
495
496        assert_eq!(Value::Float64(1.234).as_float64(), Some(1.234));
497        assert_eq!(Value::String("hello".into()).as_str(), Some("hello"));
498    }
499
500    #[test]
501    fn test_value_from_conversions() {
502        let v: Value = true.into();
503        assert_eq!(v.as_bool(), Some(true));
504
505        let v: Value = 42i64.into();
506        assert_eq!(v.as_int64(), Some(42));
507
508        let v: Value = 1.234f64.into();
509        assert_eq!(v.as_float64(), Some(1.234));
510
511        let v: Value = "hello".into();
512        assert_eq!(v.as_str(), Some("hello"));
513
514        let v: Value = vec![1u8, 2, 3].into();
515        assert_eq!(v.as_bytes(), Some(&[1u8, 2, 3][..]));
516    }
517
518    #[test]
519    fn test_value_serialization_roundtrip() {
520        let values = vec![
521            Value::Null,
522            Value::Bool(true),
523            Value::Int64(i64::MAX),
524            Value::Float64(std::f64::consts::PI),
525            Value::String("hello world".into()),
526            Value::Bytes(vec![0, 1, 2, 255].into()),
527            Value::List(vec![Value::Int64(1), Value::Int64(2)].into()),
528        ];
529
530        for v in values {
531            let bytes = v.serialize();
532            let decoded = Value::deserialize(&bytes).unwrap();
533            assert_eq!(v, decoded);
534        }
535    }
536
537    #[test]
538    fn test_property_key() {
539        let key = PropertyKey::new("name");
540        assert_eq!(key.as_str(), "name");
541
542        let key2: PropertyKey = "age".into();
543        assert_eq!(key2.as_str(), "age");
544
545        // Keys should be comparable ("age" < "name" alphabetically)
546        assert!(key2 < key);
547    }
548
549    #[test]
550    fn test_value_type_name() {
551        assert_eq!(Value::Null.type_name(), "NULL");
552        assert_eq!(Value::Bool(true).type_name(), "BOOL");
553        assert_eq!(Value::Int64(0).type_name(), "INT64");
554        assert_eq!(Value::Float64(0.0).type_name(), "FLOAT64");
555        assert_eq!(Value::String("".into()).type_name(), "STRING");
556        assert_eq!(Value::Bytes(vec![].into()).type_name(), "BYTES");
557        assert_eq!(Value::List(vec![].into()).type_name(), "LIST");
558        assert_eq!(Value::Map(BTreeMap::new().into()).type_name(), "MAP");
559    }
560
561    #[test]
562    fn test_hashable_value_basic() {
563        use std::collections::HashMap;
564
565        let mut map: HashMap<HashableValue, i32> = HashMap::new();
566
567        // Test various value types as keys
568        map.insert(HashableValue::new(Value::Int64(42)), 1);
569        map.insert(HashableValue::new(Value::String("test".into())), 2);
570        map.insert(HashableValue::new(Value::Bool(true)), 3);
571        map.insert(HashableValue::new(Value::Float64(3.14)), 4);
572
573        assert_eq!(map.get(&HashableValue::new(Value::Int64(42))), Some(&1));
574        assert_eq!(
575            map.get(&HashableValue::new(Value::String("test".into()))),
576            Some(&2)
577        );
578        assert_eq!(map.get(&HashableValue::new(Value::Bool(true))), Some(&3));
579        assert_eq!(map.get(&HashableValue::new(Value::Float64(3.14))), Some(&4));
580    }
581
582    #[test]
583    fn test_hashable_value_float_edge_cases() {
584        use std::collections::HashMap;
585
586        let mut map: HashMap<HashableValue, i32> = HashMap::new();
587
588        // NaN should be hashable and equal to itself (same bits)
589        let nan = f64::NAN;
590        map.insert(HashableValue::new(Value::Float64(nan)), 1);
591        assert_eq!(map.get(&HashableValue::new(Value::Float64(nan))), Some(&1));
592
593        // Positive and negative zero have different bits
594        let pos_zero = 0.0f64;
595        let neg_zero = -0.0f64;
596        map.insert(HashableValue::new(Value::Float64(pos_zero)), 2);
597        map.insert(HashableValue::new(Value::Float64(neg_zero)), 3);
598        assert_eq!(
599            map.get(&HashableValue::new(Value::Float64(pos_zero))),
600            Some(&2)
601        );
602        assert_eq!(
603            map.get(&HashableValue::new(Value::Float64(neg_zero))),
604            Some(&3)
605        );
606    }
607
608    #[test]
609    fn test_hashable_value_equality() {
610        let v1 = HashableValue::new(Value::Int64(42));
611        let v2 = HashableValue::new(Value::Int64(42));
612        let v3 = HashableValue::new(Value::Int64(43));
613
614        assert_eq!(v1, v2);
615        assert_ne!(v1, v3);
616    }
617
618    #[test]
619    fn test_hashable_value_inner() {
620        let hv = HashableValue::new(Value::String("hello".into()));
621        assert_eq!(hv.inner().as_str(), Some("hello"));
622
623        let v = hv.into_inner();
624        assert_eq!(v.as_str(), Some("hello"));
625    }
626
627    #[test]
628    fn test_hashable_value_conversions() {
629        let v = Value::Int64(42);
630        let hv: HashableValue = v.clone().into();
631        let v2: Value = hv.into();
632        assert_eq!(v, v2);
633    }
634}