1use crate::entity::EntityState;
8use crate::entity_snapshot::EntitySnapshot;
9use std::any::TypeId;
10use std::collections::HashMap;
11
12#[derive(Debug)]
19pub struct ChangeTracker {
20 entries: HashMap<u64, TrackerEntry>,
21 auto_detect_changes: bool,
22 next_id: u64,
23}
24
25#[derive(Debug, Clone)]
27pub struct EntityEntry {
28 pub entry_id: u64,
29 pub type_id: TypeId,
30 pub type_name: String,
31 pub state: EntityState,
32 pub modified_properties: Vec<String>,
33}
34
35#[derive(Debug, Clone)]
38pub struct EntityEntryView {
39 pub type_id: TypeId,
40 pub type_name: String,
41 pub state: EntityState,
42}
43
44struct TrackerEntry {
47 id: u64,
48 type_id: TypeId,
49 type_name: String,
50 state: EntityState,
51 original: Option<EntitySnapshot>,
52 modified_properties: Vec<String>,
53 is_upsert: bool,
54}
55
56impl std::fmt::Debug for TrackerEntry {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.debug_struct("TrackerEntry")
59 .field("id", &self.id)
60 .field("type_name", &self.type_name)
61 .field("state", &self.state)
62 .finish()
63 }
64}
65
66impl ChangeTracker {
67 pub fn new() -> Self {
68 Self {
69 entries: HashMap::new(),
70 auto_detect_changes: true,
71 next_id: 0,
72 }
73 }
74
75 pub fn track(
78 &mut self,
79 type_id: TypeId,
80 type_name: &str,
81 state: EntityState,
82 original: Option<EntitySnapshot>,
83 is_upsert: bool,
84 ) -> u64 {
85 let id = self.next_id;
86 self.next_id += 1;
87 self.entries.insert(
88 id,
89 TrackerEntry {
90 id,
91 type_id,
92 type_name: type_name.to_string(),
93 state,
94 original,
95 modified_properties: Vec::new(),
96 is_upsert,
97 },
98 );
99 id
100 }
101
102 pub fn detect_changes(&mut self, current_snapshots: &[(u64, EntitySnapshot)]) {
108 let snap_map: HashMap<u64, &EntitySnapshot> =
109 current_snapshots.iter().map(|(id, s)| (*id, s)).collect();
110
111 for entry in self.entries.values_mut() {
112 if entry.state != EntityState::Unchanged {
113 continue;
114 }
115 let Some(original) = &entry.original else {
116 continue;
117 };
118 let Some(current) = snap_map.get(&entry.id) else {
119 continue;
120 };
121
122 if **current == *original {
123 entry.modified_properties.clear();
124 continue;
125 }
126
127 let changed: Vec<String> = current
128 .iter()
129 .filter(|(k, v)| original.get(k) != Some(*v))
130 .map(|(k, _)| k.to_string())
131 .collect();
132
133 if !changed.is_empty() {
134 entry.state = EntityState::Modified;
135 entry.modified_properties = changed;
136 }
137 }
138 }
139
140 pub fn entry_state(&self, entry_id: u64) -> Option<EntityState> {
143 self.entries.get(&entry_id).map(|e| e.state)
144 }
145
146 pub fn entry_original(&self, entry_id: u64) -> Option<&EntitySnapshot> {
147 self.entries
148 .get(&entry_id)
149 .and_then(|e| e.original.as_ref())
150 }
151
152 pub fn entry_modified(&self, entry_id: u64) -> Option<&[String]> {
153 self.entries
154 .get(&entry_id)
155 .map(|e| e.modified_properties.as_slice())
156 }
157
158 pub fn entry_is_upsert(&self, entry_id: u64) -> bool {
159 self.entries.get(&entry_id).is_some_and(|e| e.is_upsert)
160 }
161
162 pub fn set_state(&mut self, entry_id: u64, state: EntityState) {
165 if let Some(entry) = self.entries.get_mut(&entry_id) {
166 entry.state = state;
167 }
168 }
169
170 pub fn set_modified(&mut self, entry_id: u64, modified: Vec<String>) {
171 if let Some(entry) = self.entries.get_mut(&entry_id) {
172 entry.modified_properties = modified;
173 }
174 }
175
176 pub fn accept_all_changes(&mut self, current_snapshots: &[(u64, EntitySnapshot)]) {
184 let snap_map: HashMap<u64, &EntitySnapshot> =
185 current_snapshots.iter().map(|(id, s)| (*id, s)).collect();
186
187 self.entries.retain(|_, e| e.state != EntityState::Deleted);
188
189 for (id, entry) in &mut self.entries {
190 if entry.state == EntityState::Added || entry.state == EntityState::Modified {
191 entry.state = EntityState::Unchanged;
192 entry.modified_properties.clear();
193 if let Some(snap) = snap_map.get(id) {
194 entry.original = Some((*snap).clone());
195 }
196 }
197 }
198 }
199
200 pub fn reject_all_changes(&mut self) {
205 self.entries.retain(|_, e| e.state != EntityState::Added);
206 for entry in self.entries.values_mut() {
207 if entry.state == EntityState::Modified || entry.state == EntityState::Deleted {
208 entry.state = EntityState::Unchanged;
209 entry.modified_properties.clear();
210 }
211 }
212 }
213
214 pub fn detach(&mut self, entry_id: u64) {
216 self.entries.remove(&entry_id);
217 }
218
219 pub fn clear_by_type(&mut self, type_id: TypeId) {
222 self.entries.retain(|_, e| e.type_id != type_id);
223 }
224
225 pub fn has_changes(&self) -> bool {
228 self.entries.values().any(|e| {
229 matches!(
230 e.state,
231 EntityState::Added | EntityState::Modified | EntityState::Deleted
232 )
233 })
234 }
235
236 pub fn clear(&mut self) {
237 self.entries.clear();
238 }
239
240 pub fn entries(&self) -> Vec<EntityEntry> {
241 self.entries
242 .values()
243 .map(|e| EntityEntry {
244 entry_id: e.id,
245 type_id: e.type_id,
246 type_name: e.type_name.clone(),
247 state: e.state,
248 modified_properties: e.modified_properties.clone(),
249 })
250 .collect()
251 }
252
253 pub fn count_by_state(&self, state: EntityState) -> usize {
254 self.entries.values().filter(|e| e.state == state).count()
255 }
256
257 pub fn entries_by_state(&self, state: EntityState) -> Vec<EntityEntry> {
258 self.entries
259 .values()
260 .filter(|e| e.state == state)
261 .map(|e| EntityEntry {
262 entry_id: e.id,
263 type_id: e.type_id,
264 type_name: e.type_name.clone(),
265 state: e.state,
266 modified_properties: e.modified_properties.clone(),
267 })
268 .collect()
269 }
270
271 pub fn entry_views(&self) -> Vec<EntityEntryView> {
274 self.entries
275 .values()
276 .map(|e| EntityEntryView {
277 type_id: e.type_id,
278 type_name: e.type_name.clone(),
279 state: e.state,
280 })
281 .collect()
282 }
283
284 pub fn is_auto_detect_changes_enabled(&self) -> bool {
285 self.auto_detect_changes
286 }
287
288 pub fn set_auto_detect_changes(&mut self, enabled: bool) {
289 self.auto_detect_changes = enabled;
290 }
291}
292
293impl Default for ChangeTracker {
294 fn default() -> Self {
295 Self::new()
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use crate::provider::DbValue;
303
304 #[test]
305 fn track_assigns_sequential_ids() {
306 let mut tracker = ChangeTracker::new();
307 let id1 = tracker.track(TypeId::of::<u32>(), "u32", EntityState::Added, None, false);
308 let id2 = tracker.track(TypeId::of::<u32>(), "u32", EntityState::Added, None, false);
309 assert_eq!(id1, 0);
310 assert_eq!(id2, 1);
311 assert_eq!(tracker.count_by_state(EntityState::Added), 2);
312 }
313
314 #[test]
315 fn entry_queries_return_tracked_data() {
316 let mut tracker = ChangeTracker::new();
317 let snap = EntitySnapshot::new(vec![("id", DbValue::I32(42))]);
318 let id = tracker.track(
319 TypeId::of::<u32>(),
320 "u32",
321 EntityState::Unchanged,
322 Some(snap),
323 false,
324 );
325 assert_eq!(tracker.entry_state(id), Some(EntityState::Unchanged));
326 assert!(tracker.entry_original(id).is_some());
327 assert_eq!(tracker.entry_modified(id), Some(&[][..]));
328 assert!(!tracker.entry_is_upsert(id));
329 }
330
331 #[test]
332 fn detect_changes_marks_modified() {
333 let mut tracker = ChangeTracker::new();
334 let original = EntitySnapshot::new(vec![
335 ("id", DbValue::I32(1)),
336 ("name", DbValue::String("old".into())),
337 ]);
338 let id = tracker.track(
339 TypeId::of::<u32>(),
340 "u32",
341 EntityState::Unchanged,
342 Some(original),
343 false,
344 );
345 let current = EntitySnapshot::new(vec![
346 ("id", DbValue::I32(1)),
347 ("name", DbValue::String("new".into())),
348 ]);
349 tracker.detect_changes(&[(id, current)]);
350 assert_eq!(tracker.entry_state(id), Some(EntityState::Modified));
351 let modified = tracker.entry_modified(id).unwrap();
352 assert_eq!(modified, &["name".to_string()]);
353 }
354
355 #[test]
356 fn detect_changes_noop_when_unchanged() {
357 let mut tracker = ChangeTracker::new();
358 let snap = EntitySnapshot::new(vec![("id", DbValue::I32(1))]);
359 let id = tracker.track(
360 TypeId::of::<u32>(),
361 "u32",
362 EntityState::Unchanged,
363 Some(snap.clone()),
364 false,
365 );
366 tracker.detect_changes(&[(id, snap)]);
367 assert_eq!(tracker.entry_state(id), Some(EntityState::Unchanged));
368 }
369
370 #[test]
371 fn accept_all_changes_transitions_and_updates_original() {
372 let mut tracker = ChangeTracker::new();
373 let id = tracker.track(TypeId::of::<u32>(), "u32", EntityState::Added, None, false);
374 let current = EntitySnapshot::new(vec![("id", DbValue::I32(10))]);
375 tracker.accept_all_changes(&[(id, current)]);
376 assert_eq!(tracker.entry_state(id), Some(EntityState::Unchanged));
377 assert!(tracker.entry_original(id).is_some());
378 }
379
380 #[test]
381 fn accept_all_changes_removes_deleted() {
382 let mut tracker = ChangeTracker::new();
383 let id = tracker.track(
384 TypeId::of::<u32>(),
385 "u32",
386 EntityState::Deleted,
387 None,
388 false,
389 );
390 tracker.accept_all_changes(&[]);
391 assert_eq!(tracker.entry_state(id), None);
392 }
393
394 #[test]
395 fn reject_all_changes_reverts_modified() {
396 let mut tracker = ChangeTracker::new();
397 let id = tracker.track(
398 TypeId::of::<u32>(),
399 "u32",
400 EntityState::Modified,
401 Some(EntitySnapshot::new(vec![("id", DbValue::I32(1))])),
402 false,
403 );
404 tracker.reject_all_changes();
405 assert_eq!(tracker.entry_state(id), Some(EntityState::Unchanged));
406 }
407
408 #[test]
409 fn reject_all_changes_removes_added() {
410 let mut tracker = ChangeTracker::new();
411 let id = tracker.track(TypeId::of::<u32>(), "u32", EntityState::Added, None, false);
412 tracker.reject_all_changes();
413 assert_eq!(tracker.entry_state(id), None);
414 }
415}