Skip to main content

rust_ef/
entity_snapshot.rs

1//! Immutable entity snapshot — single-allocation property bag for change tracking.
2//!
3//! Replaces `HashMap<String, DbValue>` as the return type of `snapshot()` and
4//! `key_values()`. Field names are `&'static str` (generated by the macro via
5//! string literals), eliminating per-call `String` key allocations. The inner
6//! `Box<[T]>` is a single heap allocation with no hash table overhead.
7
8use crate::provider::DbValue;
9
10/// An immutable snapshot of an entity's scalar property values.
11///
12/// Constructed by `IEntitySnapshot::snapshot()` and `IGetKeyValues::key_values()`.
13/// Field lookups via [`get`](Self::get) are O(n) linear scans — for the typical
14/// 5–10 field entity this is faster than `HashMap` allocation + hashing.
15#[derive(Debug, Clone)]
16pub struct EntitySnapshot {
17    entries: Box<[(&'static str, DbValue)]>,
18}
19
20impl EntitySnapshot {
21    /// Creates a snapshot from a vector of `(field_name, value)` pairs.
22    pub fn new(entries: Vec<(&'static str, DbValue)>) -> Self {
23        Self {
24            entries: entries.into_boxed_slice(),
25        }
26    }
27
28    /// Creates an empty snapshot (used for `Added` entities with no original).
29    pub fn empty() -> Self {
30        Self {
31            entries: Box::new([]),
32        }
33    }
34
35    /// Looks up a value by field name. O(n) linear scan.
36    pub fn get(&self, field: &str) -> Option<&DbValue> {
37        self.entries
38            .iter()
39            .find(|(k, _)| *k == field)
40            .map(|(_, v)| v)
41    }
42
43    /// Iterates over all `(field_name, value)` pairs.
44    pub fn iter(&self) -> impl Iterator<Item = (&'static str, &DbValue)> {
45        self.entries.iter().map(|(k, v)| (*k, v))
46    }
47
48    /// Returns the number of properties in the snapshot.
49    pub fn len(&self) -> usize {
50        self.entries.len()
51    }
52
53    /// Returns `true` if the snapshot contains no properties.
54    pub fn is_empty(&self) -> bool {
55        self.entries.is_empty()
56    }
57}
58
59impl PartialEq for EntitySnapshot {
60    /// Order-independent comparison: same length + every pair finds a match.
61    fn eq(&self, other: &Self) -> bool {
62        self.entries.len() == other.entries.len()
63            && self.entries.iter().all(|(k, v)| other.get(k) == Some(v))
64    }
65}
66
67impl From<Vec<(&'static str, DbValue)>> for EntitySnapshot {
68    fn from(entries: Vec<(&'static str, DbValue)>) -> Self {
69        Self::new(entries)
70    }
71}
72
73impl Default for EntitySnapshot {
74    fn default() -> Self {
75        Self::empty()
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn construct_and_get() {
85        let snap = EntitySnapshot::new(vec![
86            ("id", DbValue::I32(42)),
87            ("name", DbValue::String("hello".into())),
88        ]);
89        assert_eq!(snap.get("id"), Some(&DbValue::I32(42)));
90        assert_eq!(snap.get("name"), Some(&DbValue::String("hello".into())));
91        assert_eq!(snap.get("missing"), None);
92    }
93
94    #[test]
95    fn empty_snapshot() {
96        let snap = EntitySnapshot::empty();
97        assert!(snap.is_empty());
98        assert_eq!(snap.len(), 0);
99        assert_eq!(snap.get("anything"), None);
100    }
101
102    #[test]
103    fn iter_all_fields() {
104        let snap = EntitySnapshot::new(vec![("a", DbValue::I32(1)), ("b", DbValue::I32(2))]);
105        let mut fields: Vec<&str> = snap.iter().map(|(k, _)| k).collect();
106        fields.sort();
107        assert_eq!(fields, vec!["a", "b"]);
108    }
109
110    #[test]
111    fn eq_order_independent() {
112        let a = EntitySnapshot::new(vec![
113            ("id", DbValue::I32(1)),
114            ("name", DbValue::String("x".into())),
115        ]);
116        let b = EntitySnapshot::new(vec![
117            ("name", DbValue::String("x".into())),
118            ("id", DbValue::I32(1)),
119        ]);
120        assert_eq!(a, b);
121    }
122
123    #[test]
124    fn ne_different_values() {
125        let a = EntitySnapshot::new(vec![("id", DbValue::I32(1))]);
126        let b = EntitySnapshot::new(vec![("id", DbValue::I32(2))]);
127        assert_ne!(a, b);
128    }
129
130    #[test]
131    fn ne_different_lengths() {
132        let a = EntitySnapshot::new(vec![("id", DbValue::I32(1))]);
133        let b = EntitySnapshot::new(vec![
134            ("id", DbValue::I32(1)),
135            ("name", DbValue::String("x".into())),
136        ]);
137        assert_ne!(a, b);
138    }
139
140    #[test]
141    fn clone_preserves_data() {
142        let snap = EntitySnapshot::new(vec![
143            ("id", DbValue::I64(99)),
144            ("active", DbValue::Bool(true)),
145        ]);
146        let cloned = snap.clone();
147        assert_eq!(snap, cloned);
148    }
149
150    #[test]
151    fn default_is_empty() {
152        let snap = EntitySnapshot::default();
153        assert!(snap.is_empty());
154    }
155}