1use crate::EntityPersist;
69use core::ops::{Deref, DerefMut};
70use sql_orm_core::{Entity, EntityMetadata, OrmError, SqlValue};
71use std::any::{Any, TypeId};
72use std::fmt;
73use std::marker::PhantomData;
74use std::sync::{Arc, Mutex};
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum EntityState {
79 Unchanged,
81 Added,
83 Modified,
85 Deleted,
87}
88
89pub struct Tracked<T> {
106 inner: Box<TrackedInner<T>>,
107 registration_id: Option<usize>,
108 tracking_registry: Option<TrackingRegistryHandle>,
109}
110
111#[doc(hidden)]
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct TrackedEntityRegistration {
114 pub entry_id: usize,
115 pub entity_rust_name: &'static str,
116 pub state: EntityState,
117}
118
119#[doc(hidden)]
120#[derive(Debug, Default)]
121pub struct TrackingRegistry {
122 state: Mutex<TrackingRegistryState>,
123}
124
125#[doc(hidden)]
126pub type TrackingRegistryHandle = Arc<TrackingRegistry>;
127
128#[doc(hidden)]
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct SaveChangesOperationPlan {
131 added_order: Vec<usize>,
132 modified_order: Vec<usize>,
133 deleted_order: Vec<usize>,
134}
135
136struct TrackedInner<T> {
137 original: T,
138 current: T,
139 state: EntityState,
140}
141
142#[derive(Debug, Default)]
143struct TrackingRegistryState {
144 next_registration_id: usize,
145 next_temporary_identity: u64,
146 entries: Vec<TrackingRegistration>,
147}
148
149struct TrackingRegistration {
150 registration_id: usize,
151 identity: TrackedIdentity,
152 entity_type_id: TypeId,
153 entity_rust_name: &'static str,
154 inner_address: usize,
155 wrapper_attached: bool,
156 state: EntityState,
157 snapshots: Box<dyn Any + Send + Sync>,
158 sync_current_from_wrapper: unsafe fn(&mut Box<dyn Any + Send + Sync>, usize),
159}
160
161impl fmt::Debug for TrackingRegistration {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 f.debug_struct("TrackingRegistration")
164 .field("registration_id", &self.registration_id)
165 .field("identity", &self.identity)
166 .field("entity_type_id", &self.entity_type_id)
167 .field("entity_rust_name", &self.entity_rust_name)
168 .field("inner_address", &self.inner_address)
169 .field("wrapper_attached", &self.wrapper_attached)
170 .field("state", &self.state)
171 .finish_non_exhaustive()
172 }
173}
174
175#[derive(Debug, Clone, PartialEq)]
176struct TrackedIdentity {
177 entity_type_id: TypeId,
178 entity_rust_name: &'static str,
179 schema: &'static str,
180 table: &'static str,
181 primary_key: TrackedPrimaryKeyIdentity,
182}
183
184#[derive(Debug, Clone, PartialEq)]
185enum TrackedPrimaryKeyIdentity {
186 Simple(SqlValue),
187 Temporary(u64),
188}
189
190#[derive(Clone)]
191#[allow(dead_code)]
192struct TrackingSnapshots<E> {
193 original: E,
194 current: E,
195}
196
197#[derive(Clone)]
198pub(crate) struct RegisteredTracked<E> {
199 registration_id: usize,
200 inner_address: usize,
201 tracking_registry: TrackingRegistryHandle,
202 _entity: PhantomData<fn() -> E>,
203}
204
205impl<T: Clone> Tracked<T> {
206 pub fn from_loaded(entity: T) -> Self {
208 Self {
209 inner: Box::new(TrackedInner {
210 original: entity.clone(),
211 current: entity,
212 state: EntityState::Unchanged,
213 }),
214 registration_id: None,
215 tracking_registry: None,
216 }
217 }
218
219 pub fn from_added(entity: T) -> Self {
221 Self {
222 inner: Box::new(TrackedInner {
223 original: entity.clone(),
224 current: entity,
225 state: EntityState::Added,
226 }),
227 registration_id: None,
228 tracking_registry: None,
229 }
230 }
231}
232
233impl<T> Tracked<T> {
234 pub fn original(&self) -> &T {
236 &self.inner.original
237 }
238
239 pub fn current(&self) -> &T {
241 &self.inner.current
242 }
243
244 pub const fn state(&self) -> EntityState {
246 self.inner.state
247 }
248
249 pub fn mark_modified(&mut self) {
255 self.mark_modified_if_unchanged();
256 }
257
258 pub fn mark_deleted(&mut self) {
264 let was_added = self.inner.state == EntityState::Added;
265 self.set_state(EntityState::Deleted);
266 if was_added {
267 self.detach_registry();
268 }
269 }
270
271 pub fn mark_unchanged(&mut self)
277 where
278 T: Clone + Send + Sync + 'static,
279 {
280 self.inner.original = self.inner.current.clone();
281 if let (Some(registration_id), Some(registry)) =
282 (self.registration_id, self.tracking_registry.as_ref())
283 {
284 registry.set_snapshots(
285 registration_id,
286 self.inner.original.clone(),
287 self.inner.current.clone(),
288 );
289 }
290 self.set_state(EntityState::Unchanged);
291 }
292
293 pub fn detach(&mut self) {
298 self.detach_registry();
299 }
300
301 pub fn current_mut(&mut self) -> &mut T {
304 self.mark_modified_if_unchanged();
305 &mut self.inner.current
306 }
307
308 pub(crate) fn current_mut_without_state_change(&mut self) -> &mut T {
309 &mut self.inner.current
310 }
311
312 fn mark_modified_if_unchanged(&mut self) {
313 if self.inner.state == EntityState::Unchanged {
314 self.set_state(EntityState::Modified);
315 }
316 }
317
318 fn set_state(&mut self, state: EntityState) {
319 self.inner.state = state;
320 if let (Some(registration_id), Some(registry)) =
321 (self.registration_id, self.tracking_registry.as_ref())
322 {
323 registry.set_state(registration_id, state);
324 }
325 }
326
327 pub(crate) fn detach_registry(&mut self) {
328 if let (Some(registration_id), Some(registry)) =
329 (self.registration_id.take(), self.tracking_registry.take())
330 {
331 registry.unregister(registration_id);
332 }
333 }
334
335 pub fn save<C>(
344 &mut self,
345 db: &C,
346 ) -> impl core::future::Future<Output = Result<(), OrmError>> + Send
347 where
348 C: crate::DbContextEntitySet<T> + Sync,
349 T: crate::ActiveRecord
350 + crate::AuditEntity
351 + crate::EntityPersist
352 + crate::EntityPrimaryKey
353 + crate::SoftDeleteEntity
354 + crate::TenantScopedEntity
355 + Clone
356 + sql_orm_core::FromRow
357 + Send,
358 {
359 async move {
360 match self.inner.state {
361 EntityState::Unchanged => Ok(()),
362 EntityState::Deleted => Err(OrmError::new(
363 "tracked deleted entities cannot be saved; detach them or persist deletion",
364 )),
365 EntityState::Added | EntityState::Modified => {
366 crate::ActiveRecord::save(&mut self.inner.current, db).await?;
367 self.inner.original = self.inner.current.clone();
368 self.set_state(EntityState::Unchanged);
369
370 if let (Some(registration_id), Some(registry)) =
371 (self.registration_id, self.tracking_registry.as_ref())
372 {
373 let key =
374 <T as crate::EntityPrimaryKey>::primary_key_value(&self.inner.current)?;
375 registry.update_persisted_identity::<T>(registration_id, key)?;
376 }
377
378 Ok(())
379 }
380 }
381 }
382 }
383
384 pub fn delete<C>(
392 &mut self,
393 db: &C,
394 ) -> impl core::future::Future<Output = Result<bool, OrmError>> + Send
395 where
396 C: crate::DbContextEntitySet<T> + Sync,
397 T: crate::ActiveRecord
398 + crate::EntityPersist
399 + crate::EntityPrimaryKey
400 + crate::SoftDeleteEntity
401 + crate::TenantScopedEntity
402 + Clone
403 + sql_orm_core::FromRow
404 + Send,
405 {
406 async move {
407 match self.inner.state {
408 EntityState::Added => {
409 self.set_state(EntityState::Deleted);
410 self.detach_registry();
411 Ok(false)
412 }
413 EntityState::Deleted => Ok(false),
414 EntityState::Unchanged | EntityState::Modified => {
415 let deleted = crate::ActiveRecord::delete(&self.inner.current, db).await?;
416 if deleted {
417 self.set_state(EntityState::Deleted);
418 self.detach_registry();
419 }
420 Ok(deleted)
421 }
422 }
423 }
424 }
425}
426
427impl<T: Clone> Tracked<T> {
428 pub fn into_current(self) -> T {
430 self.current().clone()
431 }
432}
433
434impl<T: Entity + Clone> Tracked<T> {
435 pub(crate) fn attach_registry_loaded(
436 &mut self,
437 registry: TrackingRegistryHandle,
438 key: SqlValue,
439 ) -> Result<(), OrmError> {
440 let registration_id = registry.register_or_attach_loaded(self, key)?;
441 self.registration_id = Some(registration_id);
442 self.tracking_registry = Some(registry);
443 Ok(())
444 }
445
446 pub(crate) fn attach_registry_added(&mut self, registry: TrackingRegistryHandle) {
447 let registration_id = registry.register_added(self);
448 self.registration_id = Some(registration_id);
449 self.tracking_registry = Some(registry);
450 }
451
452 #[cfg(test)]
453 pub(crate) fn attach_registry(&mut self, registry: TrackingRegistryHandle) {
454 self.attach_registry_added(registry);
455 }
456}
457
458impl<T> Deref for Tracked<T> {
459 type Target = T;
460
461 fn deref(&self) -> &Self::Target {
462 self.current()
463 }
464}
465
466impl<T> DerefMut for Tracked<T> {
467 fn deref_mut(&mut self) -> &mut Self::Target {
468 self.current_mut()
469 }
470}
471
472impl TrackingRegistry {
473 pub(crate) fn register_or_attach_loaded<E: Entity + Clone>(
474 &self,
475 tracked: &mut Tracked<E>,
476 key: SqlValue,
477 ) -> Result<usize, OrmError> {
478 let identity =
479 TrackedIdentity::for_entity::<E>(TrackedPrimaryKeyIdentity::Simple(key.clone()));
480 let mut state = self.state.lock().expect("tracking registry mutex poisoned");
481
482 if let Some(entry) = state
483 .entries
484 .iter_mut()
485 .find(|entry| entry.identity == identity)
486 {
487 if entry.wrapper_attached {
488 return Err(duplicate_live_identity_error::<E>(&key));
489 }
490
491 let Some(snapshots) = entry.snapshots.downcast_ref::<TrackingSnapshots<E>>() else {
492 return Err(OrmError::new(format!(
493 "tracked entity `{}` has incompatible registry snapshots",
494 E::metadata().rust_name,
495 )));
496 };
497
498 tracked.inner.original = snapshots.original.clone();
499 tracked.inner.current = snapshots.current.clone();
500 tracked.inner.state = entry.state;
501 entry.inner_address = tracked.inner.as_ref() as *const TrackedInner<E> as usize;
502 entry.wrapper_attached = true;
503 return Ok(entry.registration_id);
504 }
505
506 Ok(state.push_registration(tracked, identity))
507 }
508
509 pub(crate) fn register_added<E: Entity + Clone>(&self, tracked: &Tracked<E>) -> usize {
510 let mut state = self.state.lock().expect("tracking registry mutex poisoned");
511 let temporary_identity = state.next_temporary_identity;
512 state.next_temporary_identity += 1;
513 let identity = TrackedIdentity::for_entity::<E>(TrackedPrimaryKeyIdentity::Temporary(
514 temporary_identity,
515 ));
516 state.push_registration(tracked, identity)
517 }
518
519 pub(crate) fn unregister(&self, registration_id: usize) {
520 let mut state = self.state.lock().expect("tracking registry mutex poisoned");
521 state
522 .entries
523 .retain(|entry| entry.registration_id != registration_id);
524 }
525
526 pub(crate) fn set_state(&self, registration_id: usize, tracked_state: EntityState) {
527 let mut state = self.state.lock().expect("tracking registry mutex poisoned");
528 if let Some(entry) = state
529 .entries
530 .iter_mut()
531 .find(|entry| entry.registration_id == registration_id)
532 {
533 entry.state = tracked_state;
534 }
535 }
536
537 pub(crate) fn detach_wrapper(&self, registration_id: usize) {
538 let mut state = self.state.lock().expect("tracking registry mutex poisoned");
539 if let Some(entry) = state
540 .entries
541 .iter_mut()
542 .find(|entry| entry.registration_id == registration_id)
543 {
544 if entry.wrapper_attached {
545 unsafe {
546 (entry.sync_current_from_wrapper)(&mut entry.snapshots, entry.inner_address);
547 }
548 }
549 entry.wrapper_attached = false;
550 entry.inner_address = 0;
551 }
552 }
553
554 pub(crate) fn set_snapshots<E: Clone + Send + Sync + 'static>(
555 &self,
556 registration_id: usize,
557 original: E,
558 current: E,
559 ) {
560 let mut state = self.state.lock().expect("tracking registry mutex poisoned");
561 if let Some(entry) = state
562 .entries
563 .iter_mut()
564 .find(|entry| entry.registration_id == registration_id)
565 {
566 entry.snapshots = Box::new(TrackingSnapshots::<E> { original, current });
567 }
568 }
569
570 fn sync_current_snapshot_from_wrapper(&self, registration_id: usize) {
571 let mut state = self.state.lock().expect("tracking registry mutex poisoned");
572 if let Some(entry) = state
573 .entries
574 .iter_mut()
575 .find(|entry| entry.registration_id == registration_id)
576 .filter(|entry| entry.wrapper_attached)
577 {
578 unsafe {
579 (entry.sync_current_from_wrapper)(&mut entry.snapshots, entry.inner_address);
580 }
581 }
582 }
583
584 fn is_wrapper_attached(&self, registration_id: usize) -> bool {
585 self.state
586 .lock()
587 .expect("tracking registry mutex poisoned")
588 .entries
589 .iter()
590 .find(|entry| entry.registration_id == registration_id)
591 .is_some_and(|entry| entry.wrapper_attached)
592 }
593
594 fn state_of(&self, registration_id: usize) -> Option<EntityState> {
595 self.state
596 .lock()
597 .expect("tracking registry mutex poisoned")
598 .entries
599 .iter()
600 .find(|entry| entry.registration_id == registration_id)
601 .map(|entry| entry.state)
602 }
603
604 #[allow(dead_code)]
605 fn original_snapshot_of<E: Clone + Send + Sync + 'static>(
606 &self,
607 registration_id: usize,
608 ) -> Option<E> {
609 self.state
610 .lock()
611 .expect("tracking registry mutex poisoned")
612 .entries
613 .iter()
614 .find(|entry| entry.registration_id == registration_id)
615 .and_then(|entry| entry.snapshots.downcast_ref::<TrackingSnapshots<E>>())
616 .map(|snapshots| snapshots.original.clone())
617 }
618
619 #[allow(dead_code)]
620 fn current_snapshot_of<E: Clone + Send + Sync + 'static>(
621 &self,
622 registration_id: usize,
623 ) -> Option<E> {
624 self.state
625 .lock()
626 .expect("tracking registry mutex poisoned")
627 .entries
628 .iter()
629 .find(|entry| entry.registration_id == registration_id)
630 .and_then(|entry| entry.snapshots.downcast_ref::<TrackingSnapshots<E>>())
631 .map(|snapshots| snapshots.current.clone())
632 }
633
634 fn snapshot_pair_of<E: Clone + Send + Sync + 'static>(
635 &self,
636 registration_id: usize,
637 ) -> Option<(E, E)> {
638 self.state
639 .lock()
640 .expect("tracking registry mutex poisoned")
641 .entries
642 .iter()
643 .find(|entry| entry.registration_id == registration_id)
644 .and_then(|entry| entry.snapshots.downcast_ref::<TrackingSnapshots<E>>())
645 .map(|snapshots| (snapshots.original.clone(), snapshots.current.clone()))
646 }
647
648 pub(crate) fn current_snapshot_for_key<E: Entity + Clone + Send + Sync + 'static>(
649 &self,
650 key: SqlValue,
651 ) -> Option<E> {
652 let identity = TrackedIdentity::for_entity::<E>(TrackedPrimaryKeyIdentity::Simple(key));
653 let mut state = self.state.lock().expect("tracking registry mutex poisoned");
654 let entry = state
655 .entries
656 .iter_mut()
657 .find(|entry| entry.identity == identity)?;
658
659 if entry.wrapper_attached {
660 unsafe {
661 (entry.sync_current_from_wrapper)(&mut entry.snapshots, entry.inner_address);
662 }
663 }
664
665 entry
666 .snapshots
667 .downcast_ref::<TrackingSnapshots<E>>()
668 .map(|snapshots| snapshots.current.clone())
669 }
670
671 pub fn clear(&self) {
672 self.state
673 .lock()
674 .expect("tracking registry mutex poisoned")
675 .entries
676 .clear();
677 }
678
679 pub(crate) fn tracked_for<E: Entity>(self: &Arc<Self>) -> Vec<RegisteredTracked<E>> {
680 let state = self.state.lock().expect("tracking registry mutex poisoned");
681
682 state
683 .entries
684 .iter()
685 .filter(|entry| entry.entity_type_id == TypeId::of::<E>())
686 .map(|entry| RegisteredTracked::<E> {
687 registration_id: entry.registration_id,
688 inner_address: entry.inner_address,
689 tracking_registry: Arc::clone(self),
690 _entity: PhantomData,
691 })
692 .collect()
693 }
694
695 pub(crate) fn update_persisted_identity<E: Entity>(
696 &self,
697 registration_id: usize,
698 key: SqlValue,
699 ) -> Result<(), OrmError> {
700 let identity =
701 TrackedIdentity::for_entity::<E>(TrackedPrimaryKeyIdentity::Simple(key.clone()));
702 let mut state = self.state.lock().expect("tracking registry mutex poisoned");
703
704 let target_index = state
705 .entries
706 .iter()
707 .position(|entry| entry.registration_id == registration_id)
708 .ok_or_else(|| OrmError::new("tracked entity registration was not found"))?;
709
710 if state
711 .entries
712 .iter()
713 .any(|entry| entry.registration_id != registration_id && entry.identity == identity)
714 {
715 return Err(OrmError::new(format!(
716 "entity `{}` with primary key value `{:?}` is already tracked in this context",
717 E::metadata().rust_name,
718 key
719 )));
720 }
721
722 state.entries[target_index].identity = identity;
723 Ok(())
724 }
725
726 pub fn entry_count(&self) -> usize {
727 self.state
728 .lock()
729 .expect("tracking registry mutex poisoned")
730 .entries
731 .len()
732 }
733
734 pub fn registrations(&self) -> Vec<TrackedEntityRegistration> {
735 self.state
736 .lock()
737 .expect("tracking registry mutex poisoned")
738 .entries
739 .iter()
740 .map(|entry| TrackedEntityRegistration {
741 entry_id: entry.registration_id,
742 entity_rust_name: entry.entity_rust_name,
743 state: entry.state,
744 })
745 .collect()
746 }
747}
748
749#[doc(hidden)]
750pub fn save_changes_operation_plan(
751 entities: &[&'static EntityMetadata],
752) -> Result<SaveChangesOperationPlan, OrmError> {
753 let insert_order = topological_entity_order(entities)?;
754 let mut delete_order = insert_order.clone();
755 delete_order.reverse();
756
757 Ok(SaveChangesOperationPlan {
758 added_order: insert_order.clone(),
759 modified_order: insert_order,
760 deleted_order: delete_order,
761 })
762}
763
764impl SaveChangesOperationPlan {
765 pub fn added_order(&self) -> &[usize] {
766 &self.added_order
767 }
768
769 pub fn modified_order(&self) -> &[usize] {
770 &self.modified_order
771 }
772
773 pub fn deleted_order(&self) -> &[usize] {
774 &self.deleted_order
775 }
776}
777
778fn topological_entity_order(entities: &[&'static EntityMetadata]) -> Result<Vec<usize>, OrmError> {
779 let mut outgoing_edges = vec![Vec::<usize>::new(); entities.len()];
780 let mut incoming_edge_count = vec![0usize; entities.len()];
781
782 for (child_index, child) in entities.iter().enumerate() {
783 for foreign_key in child.foreign_keys {
784 if foreign_key.columns.len() != 1 || foreign_key.referenced_columns.len() != 1 {
785 continue;
786 }
787
788 let Some(parent_index) = entities.iter().position(|candidate| {
789 candidate.schema == foreign_key.referenced_schema
790 && candidate.table == foreign_key.referenced_table
791 }) else {
792 continue;
793 };
794
795 if parent_index == child_index || outgoing_edges[parent_index].contains(&child_index) {
796 continue;
797 }
798
799 outgoing_edges[parent_index].push(child_index);
800 incoming_edge_count[child_index] += 1;
801 }
802 }
803
804 let mut order = Vec::with_capacity(entities.len());
805 let mut ready: Vec<usize> = incoming_edge_count
806 .iter()
807 .enumerate()
808 .filter_map(|(index, count)| (*count == 0).then_some(index))
809 .collect();
810
811 while !ready.is_empty() {
812 ready.sort_unstable();
813 let entity_index = ready.remove(0);
814 order.push(entity_index);
815
816 for child_index in &outgoing_edges[entity_index] {
817 incoming_edge_count[*child_index] -= 1;
818 if incoming_edge_count[*child_index] == 0 {
819 ready.push(*child_index);
820 }
821 }
822 }
823
824 if order.len() != entities.len() {
825 return Err(OrmError::new(
826 "save_changes cannot determine a deterministic order for tracked operations because the context contains a foreign-key cycle",
827 ));
828 }
829
830 Ok(order)
831}
832
833impl TrackingRegistryState {
834 fn push_registration<E: Entity + Clone>(
835 &mut self,
836 tracked: &Tracked<E>,
837 identity: TrackedIdentity,
838 ) -> usize {
839 let registration_id = self.next_registration_id;
840 self.next_registration_id += 1;
841 self.entries.push(TrackingRegistration {
842 registration_id,
843 identity,
844 entity_type_id: TypeId::of::<E>(),
845 entity_rust_name: E::metadata().rust_name,
846 inner_address: tracked.inner.as_ref() as *const TrackedInner<E> as usize,
847 wrapper_attached: true,
848 state: tracked.inner.state,
849 snapshots: Box::new(TrackingSnapshots::<E> {
850 original: tracked.inner.original.clone(),
851 current: tracked.inner.current.clone(),
852 }),
853 sync_current_from_wrapper: sync_current_snapshot_from_wrapper::<E>,
854 });
855 registration_id
856 }
857}
858
859impl TrackedIdentity {
860 fn for_entity<E: Entity>(primary_key: TrackedPrimaryKeyIdentity) -> Self {
861 let metadata = E::metadata();
862 Self {
863 entity_type_id: TypeId::of::<E>(),
864 entity_rust_name: metadata.rust_name,
865 schema: metadata.schema,
866 table: metadata.table,
867 primary_key,
868 }
869 }
870}
871
872impl<E: Clone + Send + Sync + 'static> RegisteredTracked<E> {
873 pub(crate) fn registration_id(&self) -> usize {
874 self.registration_id
875 }
876
877 pub(crate) fn state(&self) -> EntityState {
878 self.tracking_registry
879 .state_of(self.registration_id)
880 .unwrap_or_else(|| unsafe { (&*(self.inner_address as *const TrackedInner<E>)).state })
881 }
882
883 pub(crate) fn current_clone(&self) -> E {
884 self.sync_current_snapshot_from_wrapper();
885 self.tracking_registry
886 .current_snapshot_of::<E>(self.registration_id)
887 .unwrap_or_else(|| unsafe {
888 (&*(self.inner_address as *const TrackedInner<E>))
889 .current
890 .clone()
891 })
892 }
893
894 fn sync_current_snapshot_from_wrapper(&self) {
895 self.tracking_registry
896 .sync_current_snapshot_from_wrapper(self.registration_id);
897 }
898
899 pub(crate) fn accept_current(&self) {
900 let current = self.current_clone();
901 if self
902 .tracking_registry
903 .is_wrapper_attached(self.registration_id)
904 {
905 unsafe {
906 let inner = self.inner_address as *mut TrackedInner<E>;
907 (*inner).original = current.clone();
908 (*inner).state = EntityState::Unchanged;
909 }
910 }
911 self.tracking_registry
912 .set_snapshots(self.registration_id, current.clone(), current);
913 self.tracking_registry
914 .set_state(self.registration_id, EntityState::Unchanged);
915 }
916
917 pub(crate) fn sync_persisted(&self, persisted: E) {
918 let snapshot = persisted.clone();
919 if self
920 .tracking_registry
921 .is_wrapper_attached(self.registration_id)
922 {
923 unsafe {
924 let inner = self.inner_address as *mut TrackedInner<E>;
925 (*inner).original = persisted.clone();
926 (*inner).current = persisted;
927 (*inner).state = EntityState::Unchanged;
928 }
929 }
930 self.tracking_registry
931 .set_snapshots(self.registration_id, snapshot.clone(), snapshot);
932 self.tracking_registry
933 .set_state(self.registration_id, EntityState::Unchanged);
934 }
935}
936
937impl<E: EntityPersist + Clone + Send + Sync + 'static> RegisteredTracked<E> {
938 pub(crate) fn has_persisted_changes(&self) -> bool {
939 self.sync_current_snapshot_from_wrapper();
940 self.tracking_registry
941 .snapshot_pair_of::<E>(self.registration_id)
942 .map(|(original, current)| E::has_persisted_changes(&original, ¤t))
943 .unwrap_or_else(|| unsafe {
944 let inner = &*(self.inner_address as *const TrackedInner<E>);
945 E::has_persisted_changes(&inner.original, &inner.current)
946 })
947 }
948}
949
950fn duplicate_live_identity_error<E: Entity>(key: &SqlValue) -> OrmError {
951 OrmError::new(format!(
952 "entity `{}` with primary key value `{:?}` already has a live tracked handle in this context; detach or drop the existing handle before loading it again",
953 E::metadata().rust_name,
954 key
955 ))
956}
957
958unsafe fn sync_current_snapshot_from_wrapper<E: Clone + Send + Sync + 'static>(
959 snapshots: &mut Box<dyn Any + Send + Sync>,
960 inner_address: usize,
961) {
962 let Some(snapshots) = snapshots.downcast_mut::<TrackingSnapshots<E>>() else {
963 return;
964 };
965 let inner = unsafe { &*(inner_address as *const TrackedInner<E>) };
966 snapshots.current = inner.current.clone();
967}
968
969impl<T: Clone> Clone for Tracked<T> {
970 fn clone(&self) -> Self {
971 Self {
972 inner: Box::new(TrackedInner {
973 original: self.original().clone(),
974 current: self.current().clone(),
975 state: self.state(),
976 }),
977 registration_id: None,
978 tracking_registry: None,
979 }
980 }
981}
982
983impl<T: core::fmt::Debug> core::fmt::Debug for Tracked<T> {
984 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
985 f.debug_struct("Tracked")
986 .field("original", self.original())
987 .field("current", self.current())
988 .field("state", &self.state())
989 .finish()
990 }
991}
992
993impl<T: PartialEq> PartialEq for Tracked<T> {
994 fn eq(&self, other: &Self) -> bool {
995 self.original() == other.original()
996 && self.current() == other.current()
997 && self.state() == other.state()
998 }
999}
1000
1001impl<T: Eq> Eq for Tracked<T> {}
1002
1003impl<T> Drop for Tracked<T> {
1004 fn drop(&mut self) {
1005 if let (Some(registration_id), Some(registry)) =
1006 (self.registration_id.take(), self.tracking_registry.take())
1007 {
1008 if matches!(
1009 self.inner.state,
1010 EntityState::Added | EntityState::Modified | EntityState::Deleted
1011 ) {
1012 registry.detach_wrapper(registration_id);
1013 } else {
1014 registry.unregister(registration_id);
1015 }
1016 }
1017 }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use super::{
1023 EntityState, Tracked, TrackedEntityRegistration, TrackingRegistry,
1024 save_changes_operation_plan,
1025 };
1026 use crate::{EntityPersist, EntityPersistMode};
1027 use sql_orm_core::{
1028 ColumnValue, Entity, EntityMetadata, ForeignKeyMetadata, OrmError, PrimaryKeyMetadata,
1029 ReferentialAction, SqlValue,
1030 };
1031 use std::sync::Arc;
1032
1033 #[derive(Clone)]
1034 struct DummyEntity;
1035
1036 #[derive(Clone)]
1037 struct DummyEntityAlias;
1038
1039 #[derive(Clone, Debug, PartialEq, Eq)]
1040 struct SnapshotEntity {
1041 name: String,
1042 }
1043
1044 #[derive(Clone, Debug, PartialEq, Eq)]
1045 struct SnapshotEntityAlias {
1046 name: String,
1047 }
1048
1049 static DUMMY_ENTITY_METADATA: EntityMetadata = EntityMetadata {
1050 rust_name: "DummyEntity",
1051 schema: "dbo",
1052 table: "dummy_entities",
1053 renamed_from: None,
1054 columns: &[],
1055 primary_key: PrimaryKeyMetadata {
1056 name: None,
1057 columns: &[],
1058 },
1059 indexes: &[],
1060 foreign_keys: &[],
1061 navigations: &[],
1062 };
1063
1064 static ORDER_METADATA: EntityMetadata = EntityMetadata {
1065 rust_name: "Order",
1066 schema: "sales",
1067 table: "orders",
1068 renamed_from: None,
1069 columns: &[],
1070 primary_key: PrimaryKeyMetadata {
1071 name: None,
1072 columns: &["id"],
1073 },
1074 indexes: &[],
1075 foreign_keys: &[],
1076 navigations: &[],
1077 };
1078
1079 static ORDER_ITEM_FOREIGN_KEYS: [ForeignKeyMetadata; 1] = [ForeignKeyMetadata::new(
1080 "fk_order_items_orders",
1081 &["order_id"],
1082 "sales",
1083 "orders",
1084 &["id"],
1085 ReferentialAction::NoAction,
1086 ReferentialAction::NoAction,
1087 )];
1088
1089 static ORDER_ITEM_METADATA: EntityMetadata = EntityMetadata {
1090 rust_name: "OrderItem",
1091 schema: "sales",
1092 table: "order_items",
1093 renamed_from: None,
1094 columns: &[],
1095 primary_key: PrimaryKeyMetadata {
1096 name: None,
1097 columns: &["id"],
1098 },
1099 indexes: &[],
1100 foreign_keys: &ORDER_ITEM_FOREIGN_KEYS,
1101 navigations: &[],
1102 };
1103
1104 static CATEGORY_FOREIGN_KEYS: [ForeignKeyMetadata; 1] = [ForeignKeyMetadata::new(
1105 "fk_categories_parent",
1106 &["parent_id"],
1107 "catalog",
1108 "categories",
1109 &["id"],
1110 ReferentialAction::NoAction,
1111 ReferentialAction::NoAction,
1112 )];
1113
1114 static CATEGORY_METADATA: EntityMetadata = EntityMetadata {
1115 rust_name: "Category",
1116 schema: "catalog",
1117 table: "categories",
1118 renamed_from: None,
1119 columns: &[],
1120 primary_key: PrimaryKeyMetadata {
1121 name: None,
1122 columns: &["id"],
1123 },
1124 indexes: &[],
1125 foreign_keys: &CATEGORY_FOREIGN_KEYS,
1126 navigations: &[],
1127 };
1128
1129 static CYCLE_A_FOREIGN_KEYS: [ForeignKeyMetadata; 1] = [ForeignKeyMetadata::new(
1130 "fk_cycle_a_cycle_b",
1131 &["cycle_b_id"],
1132 "dbo",
1133 "cycle_b",
1134 &["id"],
1135 ReferentialAction::NoAction,
1136 ReferentialAction::NoAction,
1137 )];
1138
1139 static CYCLE_B_FOREIGN_KEYS: [ForeignKeyMetadata; 1] = [ForeignKeyMetadata::new(
1140 "fk_cycle_b_cycle_a",
1141 &["cycle_a_id"],
1142 "dbo",
1143 "cycle_a",
1144 &["id"],
1145 ReferentialAction::NoAction,
1146 ReferentialAction::NoAction,
1147 )];
1148
1149 static CYCLE_A_METADATA: EntityMetadata = EntityMetadata {
1150 rust_name: "CycleA",
1151 schema: "dbo",
1152 table: "cycle_a",
1153 renamed_from: None,
1154 columns: &[],
1155 primary_key: PrimaryKeyMetadata {
1156 name: None,
1157 columns: &["id"],
1158 },
1159 indexes: &[],
1160 foreign_keys: &CYCLE_A_FOREIGN_KEYS,
1161 navigations: &[],
1162 };
1163
1164 static CYCLE_B_METADATA: EntityMetadata = EntityMetadata {
1165 rust_name: "CycleB",
1166 schema: "dbo",
1167 table: "cycle_b",
1168 renamed_from: None,
1169 columns: &[],
1170 primary_key: PrimaryKeyMetadata {
1171 name: None,
1172 columns: &["id"],
1173 },
1174 indexes: &[],
1175 foreign_keys: &CYCLE_B_FOREIGN_KEYS,
1176 navigations: &[],
1177 };
1178
1179 impl Entity for DummyEntity {
1180 fn metadata() -> &'static EntityMetadata {
1181 &DUMMY_ENTITY_METADATA
1182 }
1183 }
1184
1185 impl Entity for DummyEntityAlias {
1186 fn metadata() -> &'static EntityMetadata {
1187 &DUMMY_ENTITY_METADATA
1188 }
1189 }
1190
1191 impl Entity for SnapshotEntity {
1192 fn metadata() -> &'static EntityMetadata {
1193 &DUMMY_ENTITY_METADATA
1194 }
1195 }
1196
1197 impl Entity for SnapshotEntityAlias {
1198 fn metadata() -> &'static EntityMetadata {
1199 &DUMMY_ENTITY_METADATA
1200 }
1201 }
1202
1203 impl EntityPersist for SnapshotEntity {
1204 fn persist_mode(&self) -> Result<EntityPersistMode, OrmError> {
1205 Ok(EntityPersistMode::Update(SqlValue::I64(1)))
1206 }
1207
1208 fn insert_values(&self) -> Vec<ColumnValue> {
1209 Vec::new()
1210 }
1211
1212 fn update_changes(&self) -> Vec<ColumnValue> {
1213 vec![ColumnValue::new(
1214 "name",
1215 SqlValue::String(self.name.clone()),
1216 )]
1217 }
1218
1219 fn concurrency_token(&self) -> Result<Option<SqlValue>, OrmError> {
1220 Ok(None)
1221 }
1222
1223 fn sync_persisted(&mut self, persisted: Self) {
1224 *self = persisted;
1225 }
1226 }
1227
1228 #[test]
1229 fn tracked_loaded_value_keeps_original_and_current_snapshots() {
1230 let tracked = Tracked::from_loaded(String::from("Ana"));
1231
1232 assert_eq!(tracked.state(), EntityState::Unchanged);
1233 assert_eq!(tracked.original(), "Ana");
1234 assert_eq!(tracked.current(), "Ana");
1235 }
1236
1237 #[test]
1238 fn tracked_added_value_starts_in_added_state() {
1239 let tracked = Tracked::from_added(String::from("Luis"));
1240
1241 assert_eq!(tracked.state(), EntityState::Added);
1242 assert_eq!(tracked.original(), "Luis");
1243 assert_eq!(tracked.current(), "Luis");
1244 }
1245
1246 #[test]
1247 fn tracked_can_release_current_value() {
1248 let tracked = Tracked::from_loaded(String::from("Maria"));
1249
1250 assert_eq!(tracked.into_current(), "Maria");
1251 }
1252
1253 #[test]
1254 fn into_current_consumes_registered_wrapper_and_unregisters_it() {
1255 let registry = Arc::new(TrackingRegistry::default());
1256 let mut tracked = Tracked::from_loaded(DummyEntity);
1257 tracked.attach_registry(Arc::clone(®istry));
1258
1259 assert_eq!(registry.entry_count(), 1);
1260
1261 let _current = tracked.into_current();
1262
1263 assert_eq!(registry.entry_count(), 0);
1264 }
1265
1266 #[test]
1267 fn cloned_tracked_wrapper_is_detached_from_original_registry_entry() {
1268 let registry = Arc::new(TrackingRegistry::default());
1269 let mut original = Tracked::from_loaded(DummyEntity);
1270 original.attach_registry(Arc::clone(®istry));
1271 original.mark_modified();
1272
1273 let clone = original.clone();
1274
1275 assert_eq!(registry.entry_count(), 1);
1276 assert_eq!(clone.state(), EntityState::Modified);
1277
1278 drop(clone);
1279
1280 assert_eq!(registry.entry_count(), 1);
1281 assert_eq!(registry.registrations()[0].state, EntityState::Modified);
1282 }
1283
1284 #[test]
1285 fn mutable_access_transitions_loaded_entity_to_modified() {
1286 let mut tracked = Tracked::from_loaded(String::from("Ana"));
1287
1288 tracked.push_str(" Maria");
1289
1290 assert_eq!(tracked.state(), EntityState::Modified);
1291 assert_eq!(tracked.original(), "Ana");
1292 assert_eq!(tracked.current(), "Ana Maria");
1293 }
1294
1295 #[test]
1296 fn current_mut_transitions_loaded_entity_to_modified() {
1297 let mut tracked = Tracked::from_loaded(String::from("Luis"));
1298
1299 tracked.current_mut().push_str(" Alberto");
1300
1301 assert_eq!(tracked.state(), EntityState::Modified);
1302 assert_eq!(tracked.original(), "Luis");
1303 assert_eq!(tracked.current(), "Luis Alberto");
1304 }
1305
1306 #[test]
1307 fn explicit_mark_modified_transitions_unchanged_only() {
1308 let mut loaded = Tracked::from_loaded(String::from("Ana"));
1309 loaded.mark_modified();
1310
1311 let mut added = Tracked::from_added(String::from("Luis"));
1312 added.mark_modified();
1313
1314 let mut deleted = Tracked::from_loaded(String::from("Maria"));
1315 deleted.mark_deleted();
1316 deleted.mark_modified();
1317
1318 assert_eq!(loaded.state(), EntityState::Modified);
1319 assert_eq!(added.state(), EntityState::Added);
1320 assert_eq!(deleted.state(), EntityState::Deleted);
1321 }
1322
1323 #[test]
1324 fn explicit_mark_deleted_transitions_wrapper_to_deleted() {
1325 let mut tracked = Tracked::from_loaded(String::from("Ana"));
1326
1327 tracked.mark_deleted();
1328
1329 assert_eq!(tracked.state(), EntityState::Deleted);
1330 }
1331
1332 #[test]
1333 fn explicit_mark_unchanged_accepts_current_snapshot() {
1334 let mut tracked = Tracked::from_loaded(String::from("Ana"));
1335 tracked.current_mut().push_str(" Maria");
1336
1337 tracked.mark_unchanged();
1338
1339 assert_eq!(tracked.state(), EntityState::Unchanged);
1340 assert_eq!(tracked.original(), "Ana Maria");
1341 assert_eq!(tracked.current(), "Ana Maria");
1342 }
1343
1344 #[test]
1345 fn explicit_mark_unchanged_restores_deleted_wrapper_with_current_snapshot() {
1346 let mut tracked = Tracked::from_loaded(String::from("Ana"));
1347 tracked.current_mut().push_str(" Maria");
1348 tracked.mark_deleted();
1349
1350 tracked.mark_unchanged();
1351
1352 assert_eq!(tracked.state(), EntityState::Unchanged);
1353 assert_eq!(tracked.original(), "Ana Maria");
1354 assert_eq!(tracked.current(), "Ana Maria");
1355 }
1356
1357 #[test]
1358 fn explicit_mark_unchanged_on_registered_wrapper_updates_registry_state() {
1359 let registry = Arc::new(TrackingRegistry::default());
1360 let mut tracked = Tracked::from_loaded(DummyEntity);
1361 tracked.attach_registry(Arc::clone(®istry));
1362 tracked.mark_deleted();
1363
1364 tracked.mark_unchanged();
1365
1366 assert_eq!(tracked.state(), EntityState::Unchanged);
1367 assert_eq!(registry.entry_count(), 1);
1368 assert_eq!(registry.registrations()[0].state, EntityState::Unchanged);
1369 }
1370
1371 #[test]
1372 fn mark_deleted_transitions_any_registered_entity_to_deleted() {
1373 let registry = Arc::new(TrackingRegistry::default());
1374 let mut tracked = Tracked::from_loaded(DummyEntity);
1375 tracked.attach_registry(Arc::clone(®istry));
1376
1377 tracked.mark_deleted();
1378
1379 assert_eq!(tracked.state(), EntityState::Deleted);
1380 assert_eq!(registry.registrations()[0].state, EntityState::Deleted);
1381 }
1382
1383 #[test]
1384 fn mark_deleted_on_added_registered_entry_cancels_pending_insert() {
1385 let registry = Arc::new(TrackingRegistry::default());
1386 let mut tracked = Tracked::from_added(DummyEntity);
1387 tracked.attach_registry_added(Arc::clone(®istry));
1388
1389 tracked.mark_deleted();
1390
1391 assert_eq!(tracked.state(), EntityState::Deleted);
1392 assert_eq!(registry.entry_count(), 0);
1393 }
1394
1395 #[test]
1396 fn mutable_access_keeps_added_state_for_new_entities() {
1397 let mut tracked = Tracked::from_added(String::from("Maria"));
1398
1399 tracked.push_str(" Fernanda");
1400
1401 assert_eq!(tracked.state(), EntityState::Added);
1402 assert_eq!(tracked.original(), "Maria");
1403 assert_eq!(tracked.current(), "Maria Fernanda");
1404 }
1405
1406 #[test]
1407 fn tracking_registry_records_loaded_entities() {
1408 let registry = Arc::new(TrackingRegistry::default());
1409 let mut tracked = Tracked::from_loaded(DummyEntity);
1410
1411 tracked
1412 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1413 .unwrap();
1414
1415 assert_eq!(registry.entry_count(), 1);
1416 assert_eq!(
1417 registry.registrations(),
1418 vec![TrackedEntityRegistration {
1419 entry_id: 0,
1420 entity_rust_name: "DummyEntity",
1421 state: EntityState::Unchanged,
1422 }]
1423 );
1424 }
1425
1426 #[test]
1427 fn tracking_registry_records_added_entities() {
1428 let registry = Arc::new(TrackingRegistry::default());
1429 let mut tracked = Tracked::from_added(DummyEntity);
1430
1431 tracked.attach_registry(Arc::clone(®istry));
1432
1433 assert_eq!(registry.entry_count(), 1);
1434 assert_eq!(
1435 registry.registrations(),
1436 vec![TrackedEntityRegistration {
1437 entry_id: 0,
1438 entity_rust_name: "DummyEntity",
1439 state: EntityState::Added,
1440 }]
1441 );
1442 }
1443
1444 #[test]
1445 fn tracking_registry_diagnostics_expose_stable_entry_ids() {
1446 let registry = Arc::new(TrackingRegistry::default());
1447 let mut first = Tracked::from_added(DummyEntity);
1448 let mut second = Tracked::from_added(DummyEntity);
1449
1450 first.attach_registry_added(Arc::clone(®istry));
1451 second.attach_registry_added(Arc::clone(®istry));
1452
1453 let registrations = registry.registrations();
1454
1455 assert_eq!(registrations.len(), 2);
1456 assert_eq!(registrations[0].entry_id, 0);
1457 assert_eq!(registrations[1].entry_id, 1);
1458 assert_eq!(registrations[0].entity_rust_name, "DummyEntity");
1459 assert_eq!(registrations[1].entity_rust_name, "DummyEntity");
1460 }
1461
1462 #[test]
1463 fn tracking_registry_diagnostic_entry_ids_are_not_reused_after_unregister() {
1464 let registry = Arc::new(TrackingRegistry::default());
1465 let mut first = Tracked::from_added(DummyEntity);
1466 let mut second = Tracked::from_added(DummyEntity);
1467
1468 first.attach_registry_added(Arc::clone(®istry));
1469 let first_registration_id = first.registration_id.expect("registered first entity");
1470 registry.unregister(first_registration_id);
1471 second.attach_registry_added(Arc::clone(®istry));
1472
1473 let registrations = registry.registrations();
1474
1475 assert_eq!(registrations.len(), 1);
1476 assert_eq!(registrations[0].entry_id, 1);
1477 assert_eq!(registrations[0].state, EntityState::Added);
1478 }
1479
1480 #[test]
1481 fn tracking_registry_diagnostic_entry_ids_are_not_reused_after_clear() {
1482 let registry = Arc::new(TrackingRegistry::default());
1483 let mut first = Tracked::from_added(DummyEntity);
1484 let mut second = Tracked::from_added(DummyEntity);
1485 let mut third = Tracked::from_added(DummyEntity);
1486
1487 first.attach_registry_added(Arc::clone(®istry));
1488 second.attach_registry_added(Arc::clone(®istry));
1489 registry.clear();
1490 third.attach_registry_added(Arc::clone(®istry));
1491
1492 let registrations = registry.registrations();
1493
1494 assert_eq!(registrations.len(), 1);
1495 assert_eq!(registrations[0].entry_id, 2);
1496 assert_eq!(registrations[0].state, EntityState::Added);
1497 }
1498
1499 #[test]
1500 fn tracking_registry_owns_observable_state_for_registered_entries() {
1501 let registry = Arc::new(TrackingRegistry::default());
1502 let mut tracked = Tracked::from_loaded(DummyEntity);
1503 tracked.attach_registry(Arc::clone(®istry));
1504
1505 tracked.inner.state = EntityState::Deleted;
1506
1507 assert_eq!(tracked.state(), EntityState::Deleted);
1508 assert_eq!(registry.registrations()[0].state, EntityState::Unchanged);
1509 assert_eq!(
1510 registry.tracked_for::<DummyEntity>()[0].state(),
1511 EntityState::Unchanged
1512 );
1513
1514 tracked.mark_unchanged();
1515
1516 assert_eq!(tracked.state(), EntityState::Unchanged);
1517 assert_eq!(registry.registrations()[0].state, EntityState::Unchanged);
1518 }
1519
1520 #[test]
1521 fn tracking_registry_owns_initial_snapshots_for_registered_entries() {
1522 let registry = Arc::new(TrackingRegistry::default());
1523 let mut tracked = Tracked::from_loaded(SnapshotEntity {
1524 name: "loaded".to_string(),
1525 });
1526 tracked.attach_registry(Arc::clone(®istry));
1527 let registration_id = tracked.registration_id.expect("registered");
1528
1529 tracked.inner.original.name = "wrapper original changed".to_string();
1530 tracked.inner.current.name = "wrapper current changed".to_string();
1531
1532 assert_eq!(
1533 registry
1534 .original_snapshot_of::<SnapshotEntity>(registration_id)
1535 .unwrap()
1536 .name,
1537 "loaded"
1538 );
1539 assert_eq!(
1540 registry
1541 .current_snapshot_of::<SnapshotEntity>(registration_id)
1542 .unwrap()
1543 .name,
1544 "loaded"
1545 );
1546 }
1547
1548 #[test]
1549 fn mark_unchanged_syncs_registry_owned_snapshots() {
1550 let registry = Arc::new(TrackingRegistry::default());
1551 let mut tracked = Tracked::from_loaded(SnapshotEntity {
1552 name: "loaded".to_string(),
1553 });
1554 tracked.attach_registry(Arc::clone(®istry));
1555 let registration_id = tracked.registration_id.expect("registered");
1556
1557 tracked.current_mut().name = "accepted".to_string();
1558 tracked.mark_unchanged();
1559
1560 assert_eq!(
1561 registry
1562 .original_snapshot_of::<SnapshotEntity>(registration_id)
1563 .unwrap()
1564 .name,
1565 "accepted"
1566 );
1567 assert_eq!(
1568 registry
1569 .current_snapshot_of::<SnapshotEntity>(registration_id)
1570 .unwrap()
1571 .name,
1572 "accepted"
1573 );
1574 }
1575
1576 #[test]
1577 fn registered_tracked_helpers_read_snapshots_from_registry() {
1578 let registry = Arc::new(TrackingRegistry::default());
1579 let mut tracked = Tracked::from_loaded(SnapshotEntity {
1580 name: "loaded".to_string(),
1581 });
1582 tracked.attach_registry(Arc::clone(®istry));
1583 let registration_id = tracked.registration_id.expect("registered");
1584
1585 tracked.inner.original.name = "wrapper original changed".to_string();
1586 tracked.inner.current.name = "changed".to_string();
1587
1588 let registered = registry.tracked_for::<SnapshotEntity>()[0].clone();
1589
1590 assert!(registered.has_persisted_changes());
1591 assert_eq!(registered.current_clone().name, "changed");
1592 assert_eq!(
1593 registry
1594 .original_snapshot_of::<SnapshotEntity>(registration_id)
1595 .unwrap()
1596 .name,
1597 "loaded"
1598 );
1599 assert_eq!(
1600 registry
1601 .current_snapshot_of::<SnapshotEntity>(registration_id)
1602 .unwrap()
1603 .name,
1604 "changed"
1605 );
1606 }
1607
1608 #[test]
1609 fn registered_tracked_sync_persisted_updates_detached_registry_owned_snapshots() {
1610 let registry = Arc::new(TrackingRegistry::default());
1611
1612 {
1613 let mut tracked = Tracked::from_loaded(SnapshotEntity {
1614 name: "loaded".to_string(),
1615 });
1616 tracked
1617 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1618 .unwrap();
1619 tracked.current_mut().name = "changed before drop".to_string();
1620 }
1621
1622 let registered = registry.tracked_for::<SnapshotEntity>()[0].clone();
1623 registered.sync_persisted(SnapshotEntity {
1624 name: "persisted value".to_string(),
1625 });
1626
1627 assert_eq!(registry.entry_count(), 1);
1628 assert_eq!(registry.registrations()[0].state, EntityState::Unchanged);
1629 assert!(!registered.has_persisted_changes());
1630 assert_eq!(registered.current_clone().name, "persisted value");
1631
1632 let mut reattached = Tracked::from_loaded(SnapshotEntity {
1633 name: "stale database value".to_string(),
1634 });
1635 reattached
1636 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1637 .unwrap();
1638
1639 assert_eq!(reattached.state(), EntityState::Unchanged);
1640 assert_eq!(reattached.original().name, "persisted value");
1641 assert_eq!(reattached.current().name, "persisted value");
1642 }
1643
1644 #[test]
1645 fn registered_tracked_accept_current_updates_detached_registry_owned_snapshots() {
1646 let registry = Arc::new(TrackingRegistry::default());
1647
1648 {
1649 let mut tracked = Tracked::from_loaded(SnapshotEntity {
1650 name: "loaded".to_string(),
1651 });
1652 tracked
1653 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1654 .unwrap();
1655 tracked.current_mut().name = "accepted detached current".to_string();
1656 }
1657
1658 let registered = registry.tracked_for::<SnapshotEntity>()[0].clone();
1659 assert!(registered.has_persisted_changes());
1660
1661 registered.accept_current();
1662
1663 assert_eq!(registry.entry_count(), 1);
1664 assert_eq!(registry.registrations()[0].state, EntityState::Unchanged);
1665 assert!(!registered.has_persisted_changes());
1666 assert_eq!(registered.current_clone().name, "accepted detached current");
1667
1668 let mut reattached = Tracked::from_loaded(SnapshotEntity {
1669 name: "stale database value".to_string(),
1670 });
1671 reattached
1672 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1673 .unwrap();
1674
1675 assert_eq!(reattached.state(), EntityState::Unchanged);
1676 assert_eq!(reattached.original().name, "accepted detached current");
1677 assert_eq!(reattached.current().name, "accepted detached current");
1678 }
1679
1680 #[test]
1681 fn dropping_added_wrapper_detaches_handle_without_removing_registry_entry() {
1682 let registry = Arc::new(TrackingRegistry::default());
1683
1684 {
1685 let mut tracked = Tracked::from_added(SnapshotEntity {
1686 name: "new".to_string(),
1687 });
1688 tracked.attach_registry_added(Arc::clone(®istry));
1689 tracked.current_mut().name = "changed before drop".to_string();
1690
1691 assert_eq!(registry.entry_count(), 1);
1692 }
1693
1694 let registered = registry.tracked_for::<SnapshotEntity>()[0].clone();
1695
1696 assert_eq!(registry.entry_count(), 1);
1697 assert_eq!(registry.registrations()[0].state, EntityState::Added);
1698 assert_eq!(registered.current_clone().name, "changed before drop");
1699 }
1700
1701 #[test]
1702 fn dropping_modified_wrapper_detaches_handle_without_removing_registry_entry() {
1703 let registry = Arc::new(TrackingRegistry::default());
1704
1705 {
1706 let mut tracked = Tracked::from_loaded(SnapshotEntity {
1707 name: "loaded".to_string(),
1708 });
1709 tracked.attach_registry(Arc::clone(®istry));
1710 tracked.current_mut().name = "changed before drop".to_string();
1711
1712 assert_eq!(registry.entry_count(), 1);
1713 assert_eq!(registry.registrations()[0].state, EntityState::Modified);
1714 }
1715
1716 let registered = registry.tracked_for::<SnapshotEntity>()[0].clone();
1717
1718 assert_eq!(registry.entry_count(), 1);
1719 assert_eq!(registry.registrations()[0].state, EntityState::Modified);
1720 assert_eq!(registered.current_clone().name, "changed before drop");
1721 assert!(registered.has_persisted_changes());
1722 }
1723
1724 #[test]
1725 fn dropping_deleted_wrapper_detaches_handle_without_removing_registry_entry() {
1726 let registry = Arc::new(TrackingRegistry::default());
1727
1728 {
1729 let mut tracked = Tracked::from_loaded(SnapshotEntity {
1730 name: "loaded".to_string(),
1731 });
1732 tracked.attach_registry(Arc::clone(®istry));
1733 tracked.current_mut().name = "changed before delete".to_string();
1734 tracked.mark_deleted();
1735
1736 assert_eq!(registry.entry_count(), 1);
1737 assert_eq!(registry.registrations()[0].state, EntityState::Deleted);
1738 }
1739
1740 let registered = registry.tracked_for::<SnapshotEntity>()[0].clone();
1741
1742 assert_eq!(registry.entry_count(), 1);
1743 assert_eq!(registry.registrations()[0].state, EntityState::Deleted);
1744 assert_eq!(registered.current_clone().name, "changed before delete");
1745 }
1746
1747 #[test]
1748 fn loaded_identity_reattaches_detached_registry_entry_with_owned_snapshots() {
1749 let registry = Arc::new(TrackingRegistry::default());
1750
1751 {
1752 let mut tracked = Tracked::from_loaded(SnapshotEntity {
1753 name: "loaded".to_string(),
1754 });
1755 tracked
1756 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1757 .unwrap();
1758 tracked.current_mut().name = "changed before drop".to_string();
1759
1760 assert_eq!(tracked.state(), EntityState::Modified);
1761 assert_eq!(registry.entry_count(), 1);
1762 }
1763
1764 let mut reattached = Tracked::from_loaded(SnapshotEntity {
1765 name: "stale database value".to_string(),
1766 });
1767 reattached
1768 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1769 .unwrap();
1770
1771 assert_eq!(registry.entry_count(), 1);
1772 assert_eq!(reattached.state(), EntityState::Modified);
1773 assert_eq!(reattached.original().name, "loaded");
1774 assert_eq!(reattached.current().name, "changed before drop");
1775 assert_eq!(registry.registrations()[0].state, EntityState::Modified);
1776 assert_eq!(
1777 registry.tracked_for::<SnapshotEntity>()[0]
1778 .current_clone()
1779 .name,
1780 "changed before drop"
1781 );
1782 }
1783
1784 #[test]
1785 fn loaded_identity_reattach_rejects_incompatible_registry_snapshots() {
1786 let registry = Arc::new(TrackingRegistry::default());
1787 let registration_id;
1788
1789 {
1790 let mut tracked = Tracked::from_loaded(SnapshotEntity {
1791 name: "loaded".to_string(),
1792 });
1793 tracked
1794 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1795 .unwrap();
1796 tracked.current_mut().name = "changed before drop".to_string();
1797 registration_id = tracked.registration_id.expect("registered");
1798 }
1799
1800 registry.set_snapshots(
1801 registration_id,
1802 SnapshotEntityAlias {
1803 name: "wrong original type".to_string(),
1804 },
1805 SnapshotEntityAlias {
1806 name: "wrong current type".to_string(),
1807 },
1808 );
1809
1810 let mut reattached = Tracked::from_loaded(SnapshotEntity {
1811 name: "fresh database value".to_string(),
1812 });
1813 let error = reattached
1814 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1815 .unwrap_err();
1816
1817 assert_eq!(
1818 error.message(),
1819 "tracked entity `DummyEntity` has incompatible registry snapshots"
1820 );
1821 assert_eq!(registry.entry_count(), 1);
1822 assert_eq!(reattached.state(), EntityState::Unchanged);
1823 assert_eq!(reattached.current().name, "fresh database value");
1824 }
1825
1826 #[test]
1827 fn detached_loaded_identity_can_be_registered_again() {
1828 let registry = Arc::new(TrackingRegistry::default());
1829 let mut first = Tracked::from_loaded(SnapshotEntity {
1830 name: "first".to_string(),
1831 });
1832 first
1833 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1834 .unwrap();
1835
1836 first.detach();
1837
1838 let mut second = Tracked::from_loaded(SnapshotEntity {
1839 name: "second".to_string(),
1840 });
1841 second
1842 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1843 .unwrap();
1844
1845 let registrations = registry.registrations();
1846
1847 assert_eq!(registry.entry_count(), 1);
1848 assert_eq!(registrations[0].entry_id, 1);
1849 assert_eq!(second.current().name, "second");
1850 assert_eq!(second.state(), EntityState::Unchanged);
1851 assert_eq!(
1852 registry
1853 .current_snapshot_for_key::<SnapshotEntity>(SqlValue::I64(7))
1854 .expect("newly registered identity should be available")
1855 .name,
1856 "second"
1857 );
1858 }
1859
1860 #[test]
1861 fn cleared_loaded_identity_can_be_registered_again() {
1862 let registry = Arc::new(TrackingRegistry::default());
1863 let mut first = Tracked::from_loaded(SnapshotEntity {
1864 name: "first".to_string(),
1865 });
1866 first
1867 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1868 .unwrap();
1869
1870 registry.clear();
1871
1872 let mut second = Tracked::from_loaded(SnapshotEntity {
1873 name: "second".to_string(),
1874 });
1875 second
1876 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1877 .unwrap();
1878
1879 let registrations = registry.registrations();
1880
1881 assert_eq!(registry.entry_count(), 1);
1882 assert_eq!(registrations[0].entry_id, 1);
1883 assert_eq!(second.current().name, "second");
1884 assert_eq!(second.state(), EntityState::Unchanged);
1885 assert_eq!(
1886 registry
1887 .current_snapshot_for_key::<SnapshotEntity>(SqlValue::I64(7))
1888 .expect("newly registered identity should be available")
1889 .name,
1890 "second"
1891 );
1892 }
1893
1894 #[test]
1895 fn tracked_for_does_not_return_stale_handles_after_clear() {
1896 let registry = Arc::new(TrackingRegistry::default());
1897 let mut first = Tracked::from_loaded(SnapshotEntity {
1898 name: "first".to_string(),
1899 });
1900 let mut second = Tracked::from_loaded(SnapshotEntity {
1901 name: "second".to_string(),
1902 });
1903 first
1904 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1905 .unwrap();
1906 second
1907 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(8))
1908 .unwrap();
1909
1910 registry.clear();
1911
1912 assert!(registry.tracked_for::<SnapshotEntity>().is_empty());
1913 assert!(registry.registrations().is_empty());
1914 assert_eq!(first.state(), EntityState::Unchanged);
1915 assert_eq!(second.state(), EntityState::Unchanged);
1916 }
1917
1918 #[test]
1919 fn current_snapshot_for_key_syncs_attached_wrapper_current() {
1920 let registry = Arc::new(TrackingRegistry::default());
1921 let mut tracked = Tracked::from_loaded(SnapshotEntity {
1922 name: "loaded".to_string(),
1923 });
1924 tracked
1925 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1926 .unwrap();
1927
1928 tracked.current_mut().name = "changed through wrapper".to_string();
1929
1930 let snapshot = registry
1931 .current_snapshot_for_key::<SnapshotEntity>(SqlValue::I64(7))
1932 .expect("tracked identity should have a current snapshot");
1933
1934 assert_eq!(snapshot.name, "changed through wrapper");
1935 assert_eq!(tracked.state(), EntityState::Modified);
1936 assert_eq!(registry.registrations()[0].state, EntityState::Modified);
1937 }
1938
1939 #[test]
1940 fn current_snapshot_for_key_scopes_lookup_by_rust_type() {
1941 let registry = Arc::new(TrackingRegistry::default());
1942 let mut tracked = Tracked::from_loaded(SnapshotEntity {
1943 name: "loaded".to_string(),
1944 });
1945 tracked
1946 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1947 .unwrap();
1948
1949 assert_eq!(
1950 registry.current_snapshot_for_key::<SnapshotEntityAlias>(SqlValue::I64(7)),
1951 None
1952 );
1953 assert_eq!(
1954 registry
1955 .current_snapshot_for_key::<SnapshotEntity>(SqlValue::I64(7))
1956 .expect("tracked identity should have a snapshot")
1957 .name,
1958 "loaded"
1959 );
1960 }
1961
1962 #[test]
1963 fn current_snapshot_for_key_ignores_unregistered_identity() {
1964 let registry = Arc::new(TrackingRegistry::default());
1965 let mut tracked = Tracked::from_loaded(SnapshotEntity {
1966 name: "loaded".to_string(),
1967 });
1968 tracked
1969 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1970 .unwrap();
1971 let registration_id = tracked.registration_id.expect("registered");
1972
1973 registry.unregister(registration_id);
1974
1975 assert_eq!(
1976 registry.current_snapshot_for_key::<SnapshotEntity>(SqlValue::I64(7)),
1977 None
1978 );
1979 }
1980
1981 #[test]
1982 fn current_snapshot_for_key_ignores_cleared_identity() {
1983 let registry = Arc::new(TrackingRegistry::default());
1984 let mut tracked = Tracked::from_loaded(SnapshotEntity {
1985 name: "loaded".to_string(),
1986 });
1987 tracked
1988 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
1989 .unwrap();
1990
1991 registry.clear();
1992
1993 assert_eq!(
1994 registry.current_snapshot_for_key::<SnapshotEntity>(SqlValue::I64(7)),
1995 None
1996 );
1997 }
1998
1999 #[test]
2000 fn tracking_registry_rejects_duplicate_loaded_identity() {
2001 let registry = Arc::new(TrackingRegistry::default());
2002 let mut first = Tracked::from_loaded(DummyEntity);
2003 let mut second = Tracked::from_loaded(DummyEntity);
2004
2005 first
2006 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
2007 .unwrap();
2008 let error = second
2009 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
2010 .unwrap_err();
2011
2012 assert_eq!(registry.entry_count(), 1);
2013 assert_eq!(
2014 error.message(),
2015 "entity `DummyEntity` with primary key value `I64(7)` already has a live tracked handle in this context; detach or drop the existing handle before loading it again"
2016 );
2017 }
2018
2019 #[test]
2020 fn duplicate_loaded_identity_error_leaves_rejected_wrapper_detached() {
2021 let registry = Arc::new(TrackingRegistry::default());
2022 let mut first = Tracked::from_loaded(DummyEntity);
2023
2024 first
2025 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
2026 .unwrap();
2027
2028 {
2029 let mut duplicate = Tracked::from_loaded(DummyEntity);
2030 let error = duplicate
2031 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
2032 .unwrap_err();
2033
2034 assert_eq!(
2035 error.message(),
2036 "entity `DummyEntity` with primary key value `I64(7)` already has a live tracked handle in this context; detach or drop the existing handle before loading it again"
2037 );
2038 assert_eq!(duplicate.state(), EntityState::Unchanged);
2039 assert_eq!(registry.entry_count(), 1);
2040 }
2041
2042 assert_eq!(registry.entry_count(), 1);
2043 assert_eq!(registry.registrations()[0].state, EntityState::Unchanged);
2044 }
2045
2046 #[test]
2047 fn tracking_registry_scopes_loaded_identity_by_rust_type() {
2048 let registry = Arc::new(TrackingRegistry::default());
2049 let mut first = Tracked::from_loaded(DummyEntity);
2050 let mut second = Tracked::from_loaded(DummyEntityAlias);
2051
2052 first
2053 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
2054 .unwrap();
2055 second
2056 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
2057 .unwrap();
2058
2059 assert_eq!(registry.entry_count(), 2);
2060 }
2061
2062 #[test]
2063 fn tracking_registry_allows_multiple_added_entities_with_temporary_identities() {
2064 let registry = Arc::new(TrackingRegistry::default());
2065 let mut first = Tracked::from_added(DummyEntity);
2066 let mut second = Tracked::from_added(DummyEntity);
2067
2068 first.attach_registry_added(Arc::clone(®istry));
2069 second.attach_registry_added(Arc::clone(®istry));
2070
2071 assert_eq!(registry.entry_count(), 2);
2072 }
2073
2074 #[test]
2075 fn tracking_registry_updates_temporary_identity_to_persisted_identity() {
2076 let registry = Arc::new(TrackingRegistry::default());
2077 let mut tracked = Tracked::from_added(DummyEntity);
2078 tracked.attach_registry_added(Arc::clone(®istry));
2079
2080 registry
2081 .update_persisted_identity::<DummyEntity>(
2082 tracked.registration_id.expect("registered"),
2083 SqlValue::I64(11),
2084 )
2085 .unwrap();
2086
2087 let mut duplicate = Tracked::from_loaded(DummyEntity);
2088 let error = duplicate
2089 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(11))
2090 .unwrap_err();
2091
2092 assert!(error.message().contains("live tracked handle"));
2093 }
2094
2095 #[test]
2096 fn tracking_registry_rejects_persisted_identity_update_collision_without_mutating_entry() {
2097 let registry = Arc::new(TrackingRegistry::default());
2098 let mut existing = Tracked::from_loaded(DummyEntity);
2099 let mut pending = Tracked::from_added(DummyEntity);
2100
2101 existing
2102 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(11))
2103 .unwrap();
2104 pending.attach_registry_added(Arc::clone(®istry));
2105
2106 let pending_registration = pending.registration_id.expect("registered pending entity");
2107 let error = registry
2108 .update_persisted_identity::<DummyEntity>(pending_registration, SqlValue::I64(11))
2109 .unwrap_err();
2110
2111 assert!(error.message().contains("already tracked"));
2112 assert_eq!(registry.entry_count(), 2);
2113
2114 let mut duplicate = Tracked::from_loaded(DummyEntity);
2115 let duplicate_error = duplicate
2116 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(11))
2117 .unwrap_err();
2118 assert!(duplicate_error.message().contains("live tracked handle"));
2119
2120 registry
2121 .update_persisted_identity::<DummyEntity>(pending_registration, SqlValue::I64(12))
2122 .unwrap();
2123
2124 let mut second_duplicate = Tracked::from_loaded(DummyEntity);
2125 let second_duplicate_error = second_duplicate
2126 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(12))
2127 .unwrap_err();
2128 assert!(
2129 second_duplicate_error
2130 .message()
2131 .contains("live tracked handle")
2132 );
2133 }
2134
2135 #[test]
2136 fn tracking_registry_rejects_persisted_identity_update_collision_with_detached_entry() {
2137 let registry = Arc::new(TrackingRegistry::default());
2138
2139 {
2140 let mut existing = Tracked::from_loaded(SnapshotEntity {
2141 name: "existing".to_string(),
2142 });
2143 existing
2144 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(11))
2145 .unwrap();
2146 existing.current_mut().name = "existing changed".to_string();
2147 }
2148
2149 let mut pending = Tracked::from_added(SnapshotEntity {
2150 name: "pending".to_string(),
2151 });
2152 pending.attach_registry_added(Arc::clone(®istry));
2153 let pending_registration = pending.registration_id.expect("registered pending entity");
2154
2155 let error = registry
2156 .update_persisted_identity::<SnapshotEntity>(pending_registration, SqlValue::I64(11))
2157 .unwrap_err();
2158
2159 assert_eq!(
2160 error.message(),
2161 "entity `DummyEntity` with primary key value `I64(11)` is already tracked in this context"
2162 );
2163 assert_eq!(registry.entry_count(), 2);
2164
2165 let mut reattached_existing = Tracked::from_loaded(SnapshotEntity {
2166 name: "fresh database value".to_string(),
2167 });
2168 reattached_existing
2169 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(11))
2170 .unwrap();
2171
2172 assert_eq!(reattached_existing.current().name, "existing changed");
2173
2174 registry
2175 .update_persisted_identity::<SnapshotEntity>(pending_registration, SqlValue::I64(12))
2176 .unwrap();
2177
2178 let mut duplicate_pending = Tracked::from_loaded(SnapshotEntity {
2179 name: "duplicate pending".to_string(),
2180 });
2181 let duplicate_error = duplicate_pending
2182 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(12))
2183 .unwrap_err();
2184
2185 assert!(duplicate_error.message().contains("live tracked handle"));
2186 }
2187
2188 #[test]
2189 fn tracking_registry_rejects_persisted_identity_update_for_missing_registration() {
2190 let registry = TrackingRegistry::default();
2191
2192 let error = registry
2193 .update_persisted_identity::<DummyEntity>(99, SqlValue::I64(11))
2194 .unwrap_err();
2195
2196 assert_eq!(error.message(), "tracked entity registration was not found");
2197 }
2198
2199 #[test]
2200 fn tracking_registry_missing_registration_error_precedes_identity_collision() {
2201 let registry = Arc::new(TrackingRegistry::default());
2202 let mut existing = Tracked::from_loaded(DummyEntity);
2203 existing
2204 .attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(11))
2205 .unwrap();
2206
2207 let error = registry
2208 .update_persisted_identity::<DummyEntity>(99, SqlValue::I64(11))
2209 .unwrap_err();
2210
2211 assert_eq!(error.message(), "tracked entity registration was not found");
2212 assert_eq!(registry.entry_count(), 1);
2213 assert_eq!(registry.registrations()[0].entry_id, 0);
2214 }
2215
2216 #[test]
2217 fn tracking_registry_clear_removes_all_entries() {
2218 let registry = Arc::new(TrackingRegistry::default());
2219 let mut first = Tracked::from_added(DummyEntity);
2220 let mut second = Tracked::from_added(DummyEntity);
2221 first.attach_registry_added(Arc::clone(®istry));
2222 second.attach_registry_added(Arc::clone(®istry));
2223
2224 registry.clear();
2225
2226 assert_eq!(registry.entry_count(), 0);
2227 assert!(registry.registrations().is_empty());
2228 }
2229
2230 #[test]
2231 fn detach_registry_unregisters_without_dropping_wrapper() {
2232 let registry = Arc::new(TrackingRegistry::default());
2233 let mut tracked = Tracked::from_loaded(DummyEntity);
2234 tracked.attach_registry(Arc::clone(®istry));
2235
2236 tracked.detach_registry();
2237
2238 assert_eq!(registry.entry_count(), 0);
2239 assert_eq!(tracked.state(), EntityState::Unchanged);
2240 }
2241
2242 #[test]
2243 fn public_detach_is_idempotent_and_keeps_visible_state() {
2244 let registry = Arc::new(TrackingRegistry::default());
2245 let mut tracked = Tracked::from_loaded(DummyEntity);
2246 tracked.attach_registry(Arc::clone(®istry));
2247 tracked.mark_deleted();
2248
2249 tracked.detach();
2250 tracked.detach();
2251
2252 assert_eq!(registry.entry_count(), 0);
2253 assert_eq!(tracked.state(), EntityState::Deleted);
2254 }
2255
2256 #[test]
2257 fn public_detach_unregisters_without_resetting_state() {
2258 let registry = Arc::new(TrackingRegistry::default());
2259 let mut tracked = Tracked::from_loaded(DummyEntity);
2260 tracked.attach_registry(Arc::clone(®istry));
2261 tracked.mark_modified();
2262
2263 tracked.detach();
2264
2265 assert_eq!(registry.entry_count(), 0);
2266 assert_eq!(tracked.state(), EntityState::Modified);
2267 }
2268
2269 #[test]
2270 fn tracking_registry_unregister_missing_registration_is_noop() {
2271 let registry = Arc::new(TrackingRegistry::default());
2272 let mut tracked = Tracked::from_loaded(DummyEntity);
2273 tracked.attach_registry(Arc::clone(®istry));
2274
2275 registry.unregister(99);
2276
2277 assert_eq!(registry.entry_count(), 1);
2278 assert_eq!(registry.registrations()[0].state, EntityState::Unchanged);
2279 }
2280
2281 #[test]
2282 fn dropping_tracked_entity_unregisters_it_from_registry() {
2283 let registry = Arc::new(TrackingRegistry::default());
2284
2285 {
2286 let mut tracked = Tracked::from_loaded(DummyEntity);
2287 tracked.attach_registry(Arc::clone(®istry));
2288 assert_eq!(registry.entry_count(), 1);
2289 }
2290
2291 assert_eq!(registry.entry_count(), 0);
2292 }
2293
2294 #[test]
2295 fn save_changes_plan_orders_added_parents_before_children() {
2296 let plan = save_changes_operation_plan(&[
2297 &ORDER_ITEM_METADATA,
2298 &DUMMY_ENTITY_METADATA,
2299 &ORDER_METADATA,
2300 ])
2301 .unwrap();
2302
2303 assert_eq!(plan.added_order(), &[1, 2, 0]);
2304 assert_eq!(plan.modified_order(), &[1, 2, 0]);
2305 }
2306
2307 #[test]
2308 fn save_changes_plan_orders_deleted_children_before_parents() {
2309 let plan = save_changes_operation_plan(&[
2310 &ORDER_ITEM_METADATA,
2311 &DUMMY_ENTITY_METADATA,
2312 &ORDER_METADATA,
2313 ])
2314 .unwrap();
2315
2316 assert_eq!(plan.deleted_order(), &[0, 2, 1]);
2317 }
2318
2319 #[test]
2320 fn save_changes_plan_preserves_context_order_without_dependencies() {
2321 let plan = save_changes_operation_plan(&[&ORDER_METADATA, &DUMMY_ENTITY_METADATA]).unwrap();
2322
2323 assert_eq!(plan.added_order(), &[0, 1]);
2324 assert_eq!(plan.modified_order(), &[0, 1]);
2325 assert_eq!(plan.deleted_order(), &[1, 0]);
2326 }
2327
2328 #[test]
2329 fn save_changes_plan_ignores_foreign_keys_to_entities_outside_context() {
2330 let plan =
2331 save_changes_operation_plan(&[&ORDER_ITEM_METADATA, &DUMMY_ENTITY_METADATA]).unwrap();
2332
2333 assert_eq!(plan.added_order(), &[0, 1]);
2334 assert_eq!(plan.modified_order(), &[0, 1]);
2335 assert_eq!(plan.deleted_order(), &[1, 0]);
2336 }
2337
2338 #[test]
2339 fn save_changes_plan_ignores_simple_self_references() {
2340 let plan =
2341 save_changes_operation_plan(&[&CATEGORY_METADATA, &DUMMY_ENTITY_METADATA]).unwrap();
2342
2343 assert_eq!(plan.added_order(), &[0, 1]);
2344 assert_eq!(plan.modified_order(), &[0, 1]);
2345 assert_eq!(plan.deleted_order(), &[1, 0]);
2346 }
2347
2348 #[test]
2349 fn save_changes_plan_rejects_foreign_key_cycles() {
2350 let error =
2351 save_changes_operation_plan(&[&CYCLE_A_METADATA, &CYCLE_B_METADATA]).unwrap_err();
2352
2353 assert!(error.message().contains("foreign-key cycle"));
2354 }
2355}