Skip to main content

sql_orm/
tracking.rs

1//! Stable explicit change tracking surface.
2//!
3//! Stage 21 promoted `Tracked<T>` and `save_changes()` to stable for explicit
4//! tracking of entities with simple primary keys. The stabilized contract has
5//! validated identity registration, registry-owned pending snapshots after
6//! wrapper drop, explicit state APIs, no-op change detection, operation
7//! ordering, transaction behavior for direct connections, policy integration,
8//! runtime SQL Server coverage and public compile-time coverage.
9//!
10//! Wrapper lifetime is not required for pending `Added`, `Modified` and
11//! `Deleted` entries: registry-owned snapshots keep that work available after
12//! wrapper drop or consume.
13//!
14//! This module intentionally defines only the minimal public contracts for the
15//! future tracking pipeline. In this stage it does not:
16//! - replace the explicit `DbSet`/`ActiveRecord` APIs
17//! - infer inserts, updates or deletes globally outside of `Tracked<T>`
18//! - keep dropped unchanged wrappers in the unit of work
19//! - support composite primary keys through `save_changes()`; that limit is
20//!   now an explicit first-stable-cut scope rather than an implicit behavior
21//!
22//! Stable entry points:
23//! - `DbSet::find_tracked(id)` for existing entities with single-column PK
24//! - `DbSet::add_tracked(entity)` for new entities pending insertion
25//! - `DbSet::remove_tracked(&mut tracked)` for explicit tracked deletion
26//! - `Tracked::mark_modified()`, `Tracked::mark_deleted()`,
27//!   `Tracked::mark_unchanged()` and `Tracked::detach()` for explicit state
28//!   transitions on a wrapper
29//! - `DbContext::save_changes()` for explicit persistence of registry entries
30//!
31//! Observable limits in the current stage:
32//! - dropped `Added`, `Modified` and `Deleted` wrappers still participate in
33//!   `save_changes()` through registry-owned snapshots
34//! - mutable access marks `Unchanged` entities as `Modified` immediately
35//! - loaded entities are registered with a deterministic identity made from
36//!   entity type, schema, table and single-column primary key value
37//! - reloading a detached loaded identity reattaches to the registry-owned
38//!   snapshot; loading the same identity while another wrapper is still
39//!   attached returns `OrmError` instead of keeping silent duplicates. This is
40//!   the first stable-cut public policy: one live `Tracked<T>` handle per
41//!   persisted identity per context.
42//! - added entities use temporary local identities until a successful insert
43//!   returns their persisted primary key
44//! - explicit detach removes an entry from the registry without touching the
45//!   database
46//! - clearing the tracker removes every current registry entry
47//! - dropping an unchanged wrapper is still equivalent to detach in this
48//!   stable explicit-tracking cut
49//! - removing a tracked `Added` entity cancels the pending insert locally
50//! - successful tracked deletes unregister the wrapper from the internal registry
51//! - rowversion conflicts are still surfaced as `OrmError::ConcurrencyConflict`
52//! - composite-primary-key entities fail with a stable `OrmError` when loaded
53//!   through `find_tracked(...)` or persisted through `save_changes()`; the
54//!   first stable cut intentionally keeps tracking persistence scoped to
55//!   single-column primary keys
56//! - `tracked.save(&db).await` and `tracked.delete(&db).await` have explicit
57//!   wrapper semantics, so they do not dereference to Active Record and leave
58//!   stale tracker state behind
59//! - navigation includes and explicit navigation loads attach values to the
60//!   root entity only; related entities are not automatically registered in the
61//!   tracker and relationship changes are not persisted as graph
62//!   updates
63//! - future relationship persistence is intentionally deferred until graph
64//!   update semantics can define dependent insert/delete behavior, foreign-key
65//!   updates, direct many-to-many exclusions and conflict handling without
66//!   bypassing the existing `DbSet` persistence paths
67
68use 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/// Lifecycle state for a tracked entity.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum EntityState {
79    /// Entity was loaded and has not requested mutable access.
80    Unchanged,
81    /// Entity was added locally and should be inserted by `save_changes()`.
82    Added,
83    /// Entity was loaded and then mutably accessed.
84    Modified,
85    /// Entity was explicitly marked for deletion.
86    Deleted,
87}
88
89/// Snapshot-based wrapper for explicitly tracked entities.
90///
91/// `Tracked<T>` keeps the original snapshot together with the current value so
92/// later stages can compare and persist changes without relying on runtime
93/// proxies or reflection. Registry-owned snapshots keep pending `Added`,
94/// `Modified` and `Deleted` work alive after a wrapper is dropped or consumed.
95/// Cloning a wrapper copies its visible state and snapshots, but the clone is
96/// detached from the registry.
97/// Calling [`Tracked::detach`] repeatedly is a no-op after the first detach and
98/// does not reset the visible wrapper state.
99///
100/// State can be inspected with [`Tracked::state`] and changed explicitly with
101/// [`Tracked::mark_modified`], [`Tracked::mark_deleted`],
102/// [`Tracked::mark_unchanged`] and [`Tracked::detach`]. Immediate
103/// persistence through [`Tracked::save`] and [`Tracked::delete`] delegates to
104/// the same `DbSet`/Active Record pipelines used by ordinary CRUD.
105pub 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    /// Creates a tracked value loaded from persistence.
207    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    /// Creates a tracked value that represents a new entity pending insertion.
220    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    /// Returns the original snapshot captured when tracking started.
235    pub fn original(&self) -> &T {
236        &self.inner.original
237    }
238
239    /// Returns the current in-memory value.
240    pub fn current(&self) -> &T {
241        &self.inner.current
242    }
243
244    /// Returns the current tracking state.
245    pub const fn state(&self) -> EntityState {
246        self.inner.state
247    }
248
249    /// Explicitly marks this tracked value as `Modified`.
250    ///
251    /// `Added` values remain `Added` because they still need an insert, and
252    /// `Deleted` values remain `Deleted` because deletion wins over pending
253    /// modifications until the caller explicitly marks the value unchanged.
254    pub fn mark_modified(&mut self) {
255        self.mark_modified_if_unchanged();
256    }
257
258    /// Explicitly marks this tracked value as `Deleted`.
259    ///
260    /// This is a state transition only; it does not execute SQL. Calling it
261    /// for an `Added` wrapper cancels the pending local insert by detaching the
262    /// wrapper from the tracker.
263    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    /// Explicitly accepts the current in-memory value as unchanged.
272    ///
273    /// The current value becomes the new original snapshot and later
274    /// `save_changes()` calls ignore this wrapper until it is marked or
275    /// mutably accessed again.
276    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    /// Detaches this wrapper from its context tracker without executing SQL.
294    ///
295    /// Detach removes the registration from the current context unit of work
296    /// and leaves the visible wrapper state unchanged.
297    pub fn detach(&mut self) {
298        self.detach_registry();
299    }
300
301    /// Returns mutable access to the current value and marks the entity as
302    /// modified when it was previously loaded as unchanged.
303    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    /// Persists this tracked entity immediately through the Active Record
336    /// pipeline and synchronizes the tracking snapshot after success.
337    ///
338    /// This method exists so `tracked.save(&db).await` has explicit tracking
339    /// semantics instead of dereferencing to `T::save(&db)` and leaving the
340    /// tracker with a stale original snapshot. `Unchanged` wrappers are a
341    /// no-op, `Added` and `Modified` wrappers use the same persistence path as
342    /// Active Record, and `Deleted` wrappers return an error.
343    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    /// Deletes this tracked entity immediately through the Active Record
385    /// pipeline and removes it from the context tracker after success.
386    ///
387    /// Calling `tracked.delete(&db).await` on an `Added` wrapper cancels the
388    /// local insert without touching the database. Persisted wrappers delegate
389    /// to Active Record delete and detach after the row is affected, so a later
390    /// `save_changes()` will not issue a second delete for the same wrapper.
391    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    /// Consumes the tracked wrapper and returns the current entity value.
429    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, &current))
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(&registry));
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(&registry));
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(&registry));
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(&registry));
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(&registry));
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(&registry), 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(&registry));
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(&registry));
1451        second.attach_registry_added(Arc::clone(&registry));
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(&registry));
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(&registry));
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(&registry));
1488        second.attach_registry_added(Arc::clone(&registry));
1489        registry.clear();
1490        third.attach_registry_added(Arc::clone(&registry));
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(&registry));
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(&registry));
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(&registry));
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(&registry));
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(&registry), 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(&registry), 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(&registry), 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(&registry), 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(&registry));
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(&registry));
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(&registry));
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(&registry), 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(&registry), 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(&registry), 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(&registry), 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(&registry), 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(&registry), 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(&registry), 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(&registry), 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(&registry), SqlValue::I64(7))
1905            .unwrap();
1906        second
1907            .attach_registry_loaded(Arc::clone(&registry), 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(&registry), 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(&registry), 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(&registry), 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(&registry), 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(&registry), SqlValue::I64(7))
2007            .unwrap();
2008        let error = second
2009            .attach_registry_loaded(Arc::clone(&registry), 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(&registry), 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(&registry), 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(&registry), SqlValue::I64(7))
2054            .unwrap();
2055        second
2056            .attach_registry_loaded(Arc::clone(&registry), 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(&registry));
2069        second.attach_registry_added(Arc::clone(&registry));
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(&registry));
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(&registry), 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(&registry), SqlValue::I64(11))
2103            .unwrap();
2104        pending.attach_registry_added(Arc::clone(&registry));
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(&registry), 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(&registry), 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(&registry), 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(&registry));
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(&registry), 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(&registry), 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(&registry), 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(&registry));
2222        second.attach_registry_added(Arc::clone(&registry));
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(&registry));
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(&registry));
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(&registry));
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(&registry));
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(&registry));
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}