Skip to main content

rust_ef/provider/
db_value_key.rs

1//! `DbValueKey` — hashable, `Eq`-satisfying key form of `DbValue`.
2//!
3//! `DbValue` cannot derive `Eq`/`Hash` because `f32`/`f64` are not `Eq` (NaN
4//! != NaN). `DbValueKey` normalizes every variant into a hashable shape:
5//!   - Integers (I16/I32/I64) collapse to `Int(i64)` — zero allocation.
6//!   - Floats (F32/F64) collapse to `FloatBits(u64)` via `to_bits()` — zero
7//!     allocation and correct hash semantics (NaN hashes consistently).
8//!   - String/Bytes keep their owned form (one clone, but no `format!`
9//!     machinery overhead).
10//!
11//! Used as the HashMap key in navigation loading (`group_rows`, `index_rows`,
12//! `group_join_rows`) to eliminate per-key `format!("{}", v)` allocation.
13
14use super::db_value::DbValue;
15
16#[cfg(feature = "chrono")]
17use chrono::Datelike;
18
19/// Hashable key derived from a `&DbValue`.
20///
21/// See the module docs for the normalization rationale.
22#[derive(Debug, Clone, PartialEq, Eq, Hash)]
23pub enum DbValueKey {
24    Null,
25    Bool(bool),
26    /// I16/I32/I64 unified — the most common PK/FK type, zero allocation.
27    Int(i64),
28    /// F32/F64 unified via `to_bits()` — correct hash semantics for NaN.
29    FloatBits(u64),
30    Str(String),
31    Bytes(Vec<u8>),
32    /// UTC timestamp millis — zero allocation.
33    #[cfg(feature = "chrono")]
34    DateTime(i64),
35    /// Naive timestamp millis — zero allocation.
36    #[cfg(feature = "chrono")]
37    NaiveDateTime(i64),
38    /// Days from Unix epoch — zero allocation.
39    #[cfg(feature = "chrono")]
40    NaiveDate(i32),
41    /// 128-bit UUID — zero allocation.
42    #[cfg(feature = "uuid")]
43    Uuid(u128),
44    /// Decimal serialized to string (rust_decimal lacks Hash).
45    #[cfg(feature = "decimal")]
46    Decimal(String),
47}
48
49impl From<&DbValue> for DbValueKey {
50    fn from(v: &DbValue) -> Self {
51        match v {
52            DbValue::Null => DbValueKey::Null,
53            DbValue::Bool(b) => DbValueKey::Bool(*b),
54            DbValue::I16(n) => DbValueKey::Int(*n as i64),
55            DbValue::I32(n) => DbValueKey::Int(*n as i64),
56            DbValue::I64(n) => DbValueKey::Int(*n),
57            DbValue::F32(f) => DbValueKey::FloatBits(f.to_bits() as u64),
58            DbValue::F64(f) => DbValueKey::FloatBits(f.to_bits()),
59            DbValue::String(s) => DbValueKey::Str(s.clone()),
60            DbValue::Bytes(b) => DbValueKey::Bytes(b.clone()),
61            #[cfg(feature = "chrono")]
62            DbValue::DateTime(dt) => DbValueKey::DateTime(dt.timestamp_millis()),
63            #[cfg(feature = "chrono")]
64            DbValue::NaiveDateTime(ndt) => {
65                DbValueKey::NaiveDateTime(ndt.and_utc().timestamp_millis())
66            }
67            #[cfg(feature = "chrono")]
68            DbValue::NaiveDate(nd) => DbValueKey::NaiveDate(nd.num_days_from_ce()),
69            #[cfg(feature = "uuid")]
70            DbValue::Uuid(u) => DbValueKey::Uuid(u.as_u128()),
71            #[cfg(feature = "decimal")]
72            DbValue::Decimal(d) => DbValueKey::Decimal(d.to_string()),
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn integer_variants_collapse_to_int() {
83        assert_eq!(DbValueKey::from(&DbValue::I16(7)), DbValueKey::Int(7));
84        assert_eq!(DbValueKey::from(&DbValue::I32(7)), DbValueKey::Int(7));
85        assert_eq!(DbValueKey::from(&DbValue::I64(7)), DbValueKey::Int(7));
86    }
87
88    #[test]
89    fn float_variants_use_bits() {
90        let key = DbValueKey::from(&DbValue::F64(2.5));
91        assert_eq!(key, DbValueKey::FloatBits(2.5f64.to_bits()));
92        let key32 = DbValueKey::from(&DbValue::F32(1.5));
93        assert_eq!(key32, DbValueKey::FloatBits(1.5f32.to_bits() as u64));
94    }
95
96    #[test]
97    fn nan_hashes_consistently() {
98        let nan = DbValue::F64(f64::NAN);
99        let k1 = DbValueKey::from(&nan);
100        let k2 = DbValueKey::from(&nan);
101        assert_eq!(k1, k2, "NaN must produce equal keys for HashMap lookup");
102    }
103
104    #[test]
105    fn string_and_bytes_clone() {
106        let s = DbValueKey::from(&DbValue::String("hello".into()));
107        assert_eq!(s, DbValueKey::Str("hello".into()));
108        let b = DbValueKey::from(&DbValue::Bytes(vec![1, 2, 3]));
109        assert_eq!(b, DbValueKey::Bytes(vec![1, 2, 3]));
110    }
111
112    #[test]
113    fn null_key() {
114        assert_eq!(DbValueKey::from(&DbValue::Null), DbValueKey::Null);
115    }
116
117    #[test]
118    fn hashmap_lookup_with_integer_key() {
119        let mut map = std::collections::HashMap::new();
120        map.insert(DbValueKey::from(&DbValue::I32(42)), "answer");
121        assert_eq!(
122            map.get(&DbValueKey::from(&DbValue::I64(42))),
123            Some(&"answer"),
124            "I32 and I64 with same value must collide in the map"
125        );
126    }
127
128    #[cfg(feature = "chrono")]
129    #[test]
130    fn datetime_key_is_stable() {
131        let dt = chrono::Utc::now();
132        let k1 = DbValueKey::from(&DbValue::DateTime(dt));
133        let k2 = DbValueKey::from(&DbValue::DateTime(dt));
134        assert_eq!(k1, k2);
135    }
136
137    #[cfg(feature = "uuid")]
138    #[test]
139    fn uuid_key_roundtrips() {
140        let u = uuid::Uuid::new_v4();
141        let key = DbValueKey::from(&DbValue::Uuid(u));
142        assert_eq!(key, DbValueKey::Uuid(u.as_u128()));
143    }
144}