Skip to main content

rust_ef/
tracking.rs

1//! Change tracking �?entity state management, snapshots, and detection.
2//!
3//! Implements EFCore's change-tracking semantics:
4//!   - Entity states: Detached | Added | Unchanged | Modified | Deleted
5//!   - Property-level snapshots taken at tracking time
6//!   - `detect_changes()` compares current values against snapshots
7//!   - `has_changes()` quickly checks for any pending mutations
8//!   - `accept_all_changes()` resets states after successful SaveChanges
9
10use crate::entity::EntityState;
11use std::collections::HashMap;
12
13/// Tracks changes to entities within a DbContext.
14#[derive(Debug)]
15pub struct ChangeTracker {
16    entries: Vec<TrackerEntry>,
17    auto_detect_changes: bool,
18    /// Counter for generating stable entry IDs.
19    next_id: u64,
20}
21
22/// A public read-only view of a tracked entry.
23#[derive(Debug, Clone)]
24pub struct EntityEntry {
25    pub entry_id: u64,
26    pub type_id: std::any::TypeId,
27    pub type_name: String,
28    pub state: EntityState,
29    /// Property names that have been modified (populated after detect_changes).
30    pub modified_properties: Vec<String>,
31}
32
33/// Lightweight, type-erased view of a pending entity entry used to build
34/// `SaveChangesContext` from `DbSet.entries` (the real save data source).
35///
36/// Unlike `EntityEntry`, this carries no `entry_id` / `modified_properties`
37/// (which only have meaning inside `ChangeTracker`). It exists so that
38/// interceptors receive a snapshot consistent with what `save_changes()`
39/// will actually commit, instead of the legacy (empty) `change_tracker`.
40#[derive(Debug, Clone)]
41pub struct EntityEntryView {
42    pub type_id: std::any::TypeId,
43    pub type_name: String,
44    pub state: EntityState,
45}
46
47/// Internal tracker entry storing the entity, its state, and original snapshots.
48struct TrackerEntry {
49    id: u64,
50    type_id: std::any::TypeId,
51    type_name: String,
52    state: EntityState,
53    /// Original property values captured when the entity was first tracked.
54    snapshot: HashMap<String, PropertySnapshot>,
55}
56
57/// A stored snapshot of a single property.
58#[derive(Debug, Clone)]
59struct PropertySnapshot {
60    /// Serialized string representation of the value at tracking time.
61    serialized: String,
62}
63
64impl std::fmt::Debug for TrackerEntry {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        f.debug_struct("TrackerEntry")
67            .field("id", &self.id)
68            .field("type_name", &self.type_name)
69            .field("state", &self.state)
70            .finish()
71    }
72}
73
74impl ChangeTracker {
75    pub fn new() -> Self {
76        Self {
77            entries: Vec::new(),
78            auto_detect_changes: true,
79            next_id: 0,
80        }
81    }
82
83    /// Takes a snapshot of the entity's current properties and begins tracking.
84    ///
85    /// The `snapshotter` closure should return a map of property-name �?string
86    /// serialization of the property's current value.
87    pub fn track_entity_with_snapshot(
88        &mut self,
89        type_id: std::any::TypeId,
90        type_name: &str,
91        state: EntityState,
92        snapshot: HashMap<String, String>,
93    ) -> u64 {
94        let id = self.next_id;
95        self.next_id += 1;
96
97        self.entries.push(TrackerEntry {
98            id,
99            type_id,
100            type_name: type_name.to_string(),
101            state,
102            snapshot: snapshot
103                .into_iter()
104                .map(|(k, v)| (k, PropertySnapshot { serialized: v }))
105                .collect(),
106        });
107
108        id
109    }
110
111    /// Begins tracking an entity without taking a snapshot (used for Added entities
112    /// that have no pre-existing state).
113    pub fn track_entity(
114        &mut self,
115        type_id: std::any::TypeId,
116        type_name: &str,
117        state: EntityState,
118    ) -> u64 {
119        self.track_entity_with_snapshot(type_id, type_name, state, HashMap::new())
120    }
121
122    /// Compares current property values (provided by the caller) against the
123    /// stored snapshots. Any property whose value differs is marked as modified,
124    /// and the entity transitions to `EntityState::Modified`.
125    pub fn detect_changes_with_properties(
126        &mut self,
127        current_properties: &[(u64, HashMap<String, String>)],
128    ) {
129        // Build a lookup of current values by entry ID
130        let current_map: HashMap<u64, &HashMap<String, String>> = current_properties
131            .iter()
132            .map(|(id, props)| (*id, props))
133            .collect();
134
135        for entry in &mut self.entries {
136            // Only check Unchanged entities
137            if entry.state != EntityState::Unchanged {
138                continue;
139            }
140
141            if let Some(current) = current_map.get(&entry.id) {
142                for (prop_name, snapshot) in &entry.snapshot {
143                    let changed = match current.get(prop_name) {
144                        Some(current_val) => current_val != &snapshot.serialized,
145                        None => true, // Property removed = change
146                    };
147
148                    if changed {
149                        entry.state = EntityState::Modified;
150                        break; // One changed property is enough
151                    }
152                }
153            }
154        }
155    }
156
157    /// Marks the property snapshot for the given entry as updated.
158    /// Used after a successful SaveChanges to update the "original" values.
159    pub fn update_snapshot(&mut self, entry_id: u64, properties: HashMap<String, String>) {
160        if let Some(entry) = self.entries.iter_mut().find(|e| e.id == entry_id) {
161            entry.snapshot = properties
162                .into_iter()
163                .map(|(k, v)| (k, PropertySnapshot { serialized: v }))
164                .collect();
165        }
166    }
167
168    /// Returns whether any tracked entity has changes pending.
169    pub fn has_changes(&self) -> bool {
170        self.entries.iter().any(|e| {
171            matches!(
172                e.state,
173                EntityState::Added | EntityState::Modified | EntityState::Deleted
174            )
175        })
176    }
177
178    /// Clears all tracked entities.
179    pub fn clear(&mut self) {
180        self.entries.clear();
181    }
182
183    /// Returns an iterator over tracked entry views.
184    pub fn entries(&self) -> Vec<EntityEntry> {
185        self.entries
186            .iter()
187            .map(|e| EntityEntry {
188                entry_id: e.id,
189                type_id: e.type_id,
190                type_name: e.type_name.clone(),
191                state: e.state,
192                modified_properties: Vec::new(),
193            })
194            .collect()
195    }
196
197    /// Returns count of entities in a given state.
198    pub fn count_by_state(&self, state: EntityState) -> usize {
199        self.entries.iter().filter(|e| e.state == state).count()
200    }
201
202    /// Returns entities grouped by their state.
203    pub fn entries_by_state(&self, state: EntityState) -> Vec<EntityEntry> {
204        self.entries()
205            .into_iter()
206            .filter(|e| e.state == state)
207            .collect()
208    }
209
210    /// After successful SaveChanges:
211    /// - Added �?Unchanged (save snapshot)
212    /// - Modified �?Unchanged (save current as new snapshot)
213    /// - Deleted �?Detached (removed from tracker)
214    pub fn accept_all_changes(&mut self) {
215        self.entries.retain(|e| e.state != EntityState::Deleted);
216        for entry in &mut self.entries {
217            if entry.state == EntityState::Added || entry.state == EntityState::Modified {
218                entry.state = EntityState::Unchanged;
219            }
220        }
221    }
222
223    /// Reverts all pending changes (detached state reverted).
224    pub fn reject_all_changes(&mut self) {
225        self.entries.retain(|e| e.state != EntityState::Added);
226        for entry in &mut self.entries {
227            if entry.state == EntityState::Modified || entry.state == EntityState::Deleted {
228                entry.state = EntityState::Unchanged;
229            }
230        }
231    }
232
233    /// Detaches a specific entry by ID.
234    pub fn detach(&mut self, entry_id: u64) {
235        self.entries.retain(|e| e.id != entry_id);
236    }
237
238    pub fn is_auto_detect_changes_enabled(&self) -> bool {
239        self.auto_detect_changes
240    }
241
242    pub fn set_auto_detect_changes(&mut self, enabled: bool) {
243        self.auto_detect_changes = enabled;
244    }
245}
246
247impl Default for ChangeTracker {
248    fn default() -> Self {
249        Self::new()
250    }
251}
252
253// ---------------------------------------------------------------------------
254// TrackedEntity �?generic container for tracked entities
255// ---------------------------------------------------------------------------
256
257#[derive(Debug)]
258pub struct TrackedEntity<T> {
259    pub entity: T,
260    pub entry_id: u64,
261    pub state: EntityState,
262}
263
264impl<T> TrackedEntity<T> {
265    pub fn new(entity: T, entry_id: u64, state: EntityState) -> Self {
266        Self {
267            entity,
268            entry_id,
269            state,
270        }
271    }
272}