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    /// Property names (field_name) that differ from the snapshot, populated
56    /// by `detect_changes_with_properties`. Empty when no detection has run
57    /// or when the entity was directly marked Modified without detection.
58    modified_properties: Vec<String>,
59}
60
61/// A stored snapshot of a single property.
62#[derive(Debug, Clone)]
63struct PropertySnapshot {
64    /// Serialized string representation of the value at tracking time.
65    serialized: String,
66}
67
68impl std::fmt::Debug for TrackerEntry {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.debug_struct("TrackerEntry")
71            .field("id", &self.id)
72            .field("type_name", &self.type_name)
73            .field("state", &self.state)
74            .finish()
75    }
76}
77
78impl ChangeTracker {
79    pub fn new() -> Self {
80        Self {
81            entries: Vec::new(),
82            auto_detect_changes: true,
83            next_id: 0,
84        }
85    }
86
87    /// Takes a snapshot of the entity's current properties and begins tracking.
88    ///
89    /// The `snapshotter` closure should return a map of property-name �?string
90    /// serialization of the property's current value.
91    pub fn track_entity_with_snapshot(
92        &mut self,
93        type_id: std::any::TypeId,
94        type_name: &str,
95        state: EntityState,
96        snapshot: HashMap<String, String>,
97    ) -> u64 {
98        let id = self.next_id;
99        self.next_id += 1;
100
101        self.entries.push(TrackerEntry {
102            id,
103            type_id,
104            type_name: type_name.to_string(),
105            state,
106            snapshot: snapshot
107                .into_iter()
108                .map(|(k, v)| (k, PropertySnapshot { serialized: v }))
109                .collect(),
110            modified_properties: Vec::new(),
111        });
112
113        id
114    }
115
116    /// Begins tracking an entity without taking a snapshot (used for Added entities
117    /// that have no pre-existing state).
118    pub fn track_entity(
119        &mut self,
120        type_id: std::any::TypeId,
121        type_name: &str,
122        state: EntityState,
123    ) -> u64 {
124        self.track_entity_with_snapshot(type_id, type_name, state, HashMap::new())
125    }
126
127    /// Compares current property values (provided by the caller) against the
128    /// stored snapshots. Any property whose value differs is recorded in the
129    /// entry's `modified_properties`, and the entity transitions to
130    /// `EntityState::Modified`. When no differences are found,
131    /// `modified_properties` is cleared.
132    pub fn detect_changes_with_properties(
133        &mut self,
134        current_properties: &[(u64, HashMap<String, String>)],
135    ) {
136        // Build a lookup of current values by entry ID
137        let current_map: HashMap<u64, &HashMap<String, String>> = current_properties
138            .iter()
139            .map(|(id, props)| (*id, props))
140            .collect();
141
142        for entry in &mut self.entries {
143            // Only check Unchanged entities
144            if entry.state != EntityState::Unchanged {
145                continue;
146            }
147
148            if let Some(current) = current_map.get(&entry.id) {
149                let mut changed_props: Vec<String> = Vec::new();
150                for (prop_name, snapshot) in &entry.snapshot {
151                    let changed = match current.get(prop_name) {
152                        Some(current_val) => current_val != &snapshot.serialized,
153                        None => true, // Property removed = change
154                    };
155
156                    if changed {
157                        changed_props.push(prop_name.clone());
158                    }
159                }
160
161                if !changed_props.is_empty() {
162                    entry.state = EntityState::Modified;
163                    entry.modified_properties = changed_props;
164                } else {
165                    entry.modified_properties.clear();
166                }
167            }
168        }
169    }
170
171    /// Marks the property snapshot for the given entry as updated.
172    /// Used after a successful SaveChanges to update the "original" values.
173    pub fn update_snapshot(&mut self, entry_id: u64, properties: HashMap<String, String>) {
174        if let Some(entry) = self.entries.iter_mut().find(|e| e.id == entry_id) {
175            entry.snapshot = properties
176                .into_iter()
177                .map(|(k, v)| (k, PropertySnapshot { serialized: v }))
178                .collect();
179        }
180    }
181
182    /// Returns whether any tracked entity has changes pending.
183    pub fn has_changes(&self) -> bool {
184        self.entries.iter().any(|e| {
185            matches!(
186                e.state,
187                EntityState::Added | EntityState::Modified | EntityState::Deleted
188            )
189        })
190    }
191
192    /// Clears all tracked entities.
193    pub fn clear(&mut self) {
194        self.entries.clear();
195    }
196
197    /// Returns an iterator over tracked entry views.
198    pub fn entries(&self) -> Vec<EntityEntry> {
199        self.entries
200            .iter()
201            .map(|e| EntityEntry {
202                entry_id: e.id,
203                type_id: e.type_id,
204                type_name: e.type_name.clone(),
205                state: e.state,
206                modified_properties: e.modified_properties.clone(),
207            })
208            .collect()
209    }
210
211    /// Returns count of entities in a given state.
212    pub fn count_by_state(&self, state: EntityState) -> usize {
213        self.entries.iter().filter(|e| e.state == state).count()
214    }
215
216    /// Returns entities grouped by their state.
217    pub fn entries_by_state(&self, state: EntityState) -> Vec<EntityEntry> {
218        self.entries()
219            .into_iter()
220            .filter(|e| e.state == state)
221            .collect()
222    }
223
224    /// After successful SaveChanges:
225    /// - Added �?Unchanged (save snapshot)
226    /// - Modified �?Unchanged (save current as new snapshot)
227    /// - Deleted �?Detached (removed from tracker)
228    pub fn accept_all_changes(&mut self) {
229        self.entries.retain(|e| e.state != EntityState::Deleted);
230        for entry in &mut self.entries {
231            if entry.state == EntityState::Added || entry.state == EntityState::Modified {
232                entry.state = EntityState::Unchanged;
233                entry.modified_properties.clear();
234            }
235        }
236    }
237
238    /// Reverts all pending changes (detached state reverted).
239    pub fn reject_all_changes(&mut self) {
240        self.entries.retain(|e| e.state != EntityState::Added);
241        for entry in &mut self.entries {
242            if entry.state == EntityState::Modified || entry.state == EntityState::Deleted {
243                entry.state = EntityState::Unchanged;
244                entry.modified_properties.clear();
245            }
246        }
247    }
248
249    /// Detaches a specific entry by ID.
250    pub fn detach(&mut self, entry_id: u64) {
251        self.entries.retain(|e| e.id != entry_id);
252    }
253
254    pub fn is_auto_detect_changes_enabled(&self) -> bool {
255        self.auto_detect_changes
256    }
257
258    pub fn set_auto_detect_changes(&mut self, enabled: bool) {
259        self.auto_detect_changes = enabled;
260    }
261}
262
263impl Default for ChangeTracker {
264    fn default() -> Self {
265        Self::new()
266    }
267}
268
269// ---------------------------------------------------------------------------
270// TrackedEntity �?generic container for tracked entities
271// ---------------------------------------------------------------------------
272
273#[derive(Debug)]
274pub struct TrackedEntity<T> {
275    pub entity: T,
276    pub entry_id: u64,
277    pub state: EntityState,
278}
279
280impl<T> TrackedEntity<T> {
281    pub fn new(entity: T, entry_id: u64, state: EntityState) -> Self {
282        Self {
283            entity,
284            entry_id,
285            state,
286        }
287    }
288}