rust-ef 1.8.0

Rust Entity Framework - An EFCore-inspired ORM for Rust
Documentation
//! Immutable entity snapshot — single-allocation property bag for change tracking.
//!
//! Replaces `HashMap<String, DbValue>` as the return type of `snapshot()` and
//! `key_values()`. Field names are `&'static str` (generated by the macro via
//! string literals), eliminating per-call `String` key allocations. The inner
//! `Box<[T]>` is a single heap allocation with no hash table overhead.

use crate::provider::DbValue;

/// An immutable snapshot of an entity's scalar property values.
///
/// Constructed by `IEntitySnapshot::snapshot()` and `IGetKeyValues::key_values()`.
/// Field lookups via [`get`](Self::get) are O(n) linear scans — for the typical
/// 5–10 field entity this is faster than `HashMap` allocation + hashing.
#[derive(Debug, Clone)]
pub struct EntitySnapshot {
    entries: Box<[(&'static str, DbValue)]>,
}

impl EntitySnapshot {
    /// Creates a snapshot from a vector of `(field_name, value)` pairs.
    pub fn new(entries: Vec<(&'static str, DbValue)>) -> Self {
        Self {
            entries: entries.into_boxed_slice(),
        }
    }

    /// Creates an empty snapshot (used for `Added` entities with no original).
    pub fn empty() -> Self {
        Self {
            entries: Box::new([]),
        }
    }

    /// Looks up a value by field name. O(n) linear scan.
    pub fn get(&self, field: &str) -> Option<&DbValue> {
        self.entries
            .iter()
            .find(|(k, _)| *k == field)
            .map(|(_, v)| v)
    }

    /// Iterates over all `(field_name, value)` pairs.
    pub fn iter(&self) -> impl Iterator<Item = (&'static str, &DbValue)> {
        self.entries.iter().map(|(k, v)| (*k, v))
    }

    /// Returns the number of properties in the snapshot.
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// Returns `true` if the snapshot contains no properties.
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }
}

impl PartialEq for EntitySnapshot {
    /// Order-independent comparison: same length + every pair finds a match.
    fn eq(&self, other: &Self) -> bool {
        self.entries.len() == other.entries.len()
            && self.entries.iter().all(|(k, v)| other.get(k) == Some(v))
    }
}

impl From<Vec<(&'static str, DbValue)>> for EntitySnapshot {
    fn from(entries: Vec<(&'static str, DbValue)>) -> Self {
        Self::new(entries)
    }
}

impl Default for EntitySnapshot {
    fn default() -> Self {
        Self::empty()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn construct_and_get() {
        let snap = EntitySnapshot::new(vec![
            ("id", DbValue::I32(42)),
            ("name", DbValue::String("hello".into())),
        ]);
        assert_eq!(snap.get("id"), Some(&DbValue::I32(42)));
        assert_eq!(snap.get("name"), Some(&DbValue::String("hello".into())));
        assert_eq!(snap.get("missing"), None);
    }

    #[test]
    fn empty_snapshot() {
        let snap = EntitySnapshot::empty();
        assert!(snap.is_empty());
        assert_eq!(snap.len(), 0);
        assert_eq!(snap.get("anything"), None);
    }

    #[test]
    fn iter_all_fields() {
        let snap = EntitySnapshot::new(vec![("a", DbValue::I32(1)), ("b", DbValue::I32(2))]);
        let mut fields: Vec<&str> = snap.iter().map(|(k, _)| k).collect();
        fields.sort();
        assert_eq!(fields, vec!["a", "b"]);
    }

    #[test]
    fn eq_order_independent() {
        let a = EntitySnapshot::new(vec![
            ("id", DbValue::I32(1)),
            ("name", DbValue::String("x".into())),
        ]);
        let b = EntitySnapshot::new(vec![
            ("name", DbValue::String("x".into())),
            ("id", DbValue::I32(1)),
        ]);
        assert_eq!(a, b);
    }

    #[test]
    fn ne_different_values() {
        let a = EntitySnapshot::new(vec![("id", DbValue::I32(1))]);
        let b = EntitySnapshot::new(vec![("id", DbValue::I32(2))]);
        assert_ne!(a, b);
    }

    #[test]
    fn ne_different_lengths() {
        let a = EntitySnapshot::new(vec![("id", DbValue::I32(1))]);
        let b = EntitySnapshot::new(vec![
            ("id", DbValue::I32(1)),
            ("name", DbValue::String("x".into())),
        ]);
        assert_ne!(a, b);
    }

    #[test]
    fn clone_preserves_data() {
        let snap = EntitySnapshot::new(vec![
            ("id", DbValue::I64(99)),
            ("active", DbValue::Bool(true)),
        ]);
        let cloned = snap.clone();
        assert_eq!(snap, cloned);
    }

    #[test]
    fn default_is_empty() {
        let snap = EntitySnapshot::default();
        assert!(snap.is_empty());
    }
}