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