use crate::EntityPersist;
use core::ops::{Deref, DerefMut};
use sql_orm_core::{Entity, EntityMetadata, OrmError, SqlValue};
use std::any::TypeId;
use std::marker::PhantomData;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntityState {
Unchanged,
Added,
Modified,
Deleted,
}
pub struct Tracked<T> {
inner: Box<TrackedInner<T>>,
registration_id: Option<usize>,
tracking_registry: Option<TrackingRegistryHandle>,
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrackedEntityRegistration {
pub entity_rust_name: &'static str,
pub state: EntityState,
}
#[doc(hidden)]
#[derive(Debug, Default)]
pub struct TrackingRegistry {
state: Mutex<TrackingRegistryState>,
}
#[doc(hidden)]
pub type TrackingRegistryHandle = Arc<TrackingRegistry>;
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SaveChangesOperationPlan {
added_order: Vec<usize>,
modified_order: Vec<usize>,
deleted_order: Vec<usize>,
}
struct TrackedInner<T> {
original: T,
current: T,
state: EntityState,
}
#[derive(Debug, Default)]
struct TrackingRegistryState {
next_registration_id: usize,
next_temporary_identity: u64,
entries: Vec<TrackingRegistration>,
}
#[derive(Debug)]
struct TrackingRegistration {
registration_id: usize,
identity: TrackedIdentity,
entity_type_id: TypeId,
entity_rust_name: &'static str,
inner_address: usize,
state_reader: unsafe fn(*const ()) -> EntityState,
}
#[derive(Debug, Clone, PartialEq)]
struct TrackedIdentity {
entity_type_id: TypeId,
entity_rust_name: &'static str,
schema: &'static str,
table: &'static str,
primary_key: TrackedPrimaryKeyIdentity,
}
#[derive(Debug, Clone, PartialEq)]
enum TrackedPrimaryKeyIdentity {
Simple(SqlValue),
Temporary(u64),
}
#[derive(Clone, Copy)]
pub(crate) struct RegisteredTracked<E> {
registration_id: usize,
inner_address: usize,
_entity: PhantomData<fn() -> E>,
}
impl<T: Clone> Tracked<T> {
pub fn from_loaded(entity: T) -> Self {
Self {
inner: Box::new(TrackedInner {
original: entity.clone(),
current: entity,
state: EntityState::Unchanged,
}),
registration_id: None,
tracking_registry: None,
}
}
pub fn from_added(entity: T) -> Self {
Self {
inner: Box::new(TrackedInner {
original: entity.clone(),
current: entity,
state: EntityState::Added,
}),
registration_id: None,
tracking_registry: None,
}
}
}
impl<T> Tracked<T> {
pub fn original(&self) -> &T {
&self.inner.original
}
pub fn current(&self) -> &T {
&self.inner.current
}
pub const fn state(&self) -> EntityState {
self.inner.state
}
pub fn mark_modified(&mut self) {
self.mark_modified_if_unchanged();
}
pub fn mark_deleted(&mut self) {
let was_added = self.inner.state == EntityState::Added;
self.inner.state = EntityState::Deleted;
if was_added {
self.detach_registry();
}
}
pub fn mark_unchanged(&mut self)
where
T: Clone,
{
self.inner.original = self.inner.current.clone();
self.inner.state = EntityState::Unchanged;
}
pub fn detach(&mut self) {
self.detach_registry();
}
pub fn current_mut(&mut self) -> &mut T {
self.mark_modified_if_unchanged();
&mut self.inner.current
}
pub(crate) fn current_mut_without_state_change(&mut self) -> &mut T {
&mut self.inner.current
}
fn mark_modified_if_unchanged(&mut self) {
if self.inner.state == EntityState::Unchanged {
self.inner.state = EntityState::Modified;
}
}
pub(crate) fn detach_registry(&mut self) {
if let (Some(registration_id), Some(registry)) =
(self.registration_id.take(), self.tracking_registry.take())
{
registry.unregister(registration_id);
}
}
pub fn save<C>(
&mut self,
db: &C,
) -> impl core::future::Future<Output = Result<(), OrmError>> + Send
where
C: crate::DbContextEntitySet<T> + Sync,
T: crate::ActiveRecord
+ crate::AuditEntity
+ crate::EntityPersist
+ crate::EntityPrimaryKey
+ crate::SoftDeleteEntity
+ crate::TenantScopedEntity
+ Clone
+ sql_orm_core::FromRow
+ Send,
{
async move {
match self.inner.state {
EntityState::Unchanged => Ok(()),
EntityState::Deleted => Err(OrmError::new(
"tracked deleted entities cannot be saved; detach them or persist deletion",
)),
EntityState::Added | EntityState::Modified => {
crate::ActiveRecord::save(&mut self.inner.current, db).await?;
self.inner.original = self.inner.current.clone();
self.inner.state = EntityState::Unchanged;
if let (Some(registration_id), Some(registry)) =
(self.registration_id, self.tracking_registry.as_ref())
{
let key =
<T as crate::EntityPrimaryKey>::primary_key_value(&self.inner.current)?;
registry.update_persisted_identity::<T>(registration_id, key)?;
}
Ok(())
}
}
}
}
pub fn delete<C>(
&mut self,
db: &C,
) -> impl core::future::Future<Output = Result<bool, OrmError>> + Send
where
C: crate::DbContextEntitySet<T> + Sync,
T: crate::ActiveRecord
+ crate::EntityPersist
+ crate::EntityPrimaryKey
+ crate::SoftDeleteEntity
+ crate::TenantScopedEntity
+ Clone
+ sql_orm_core::FromRow
+ Send,
{
async move {
match self.inner.state {
EntityState::Added => {
self.inner.state = EntityState::Deleted;
self.detach_registry();
Ok(false)
}
EntityState::Deleted => Ok(false),
EntityState::Unchanged | EntityState::Modified => {
let deleted = crate::ActiveRecord::delete(&self.inner.current, db).await?;
if deleted {
self.inner.state = EntityState::Deleted;
self.detach_registry();
}
Ok(deleted)
}
}
}
}
}
impl<T: Clone> Tracked<T> {
pub fn into_current(self) -> T {
self.current().clone()
}
}
impl<T: Entity> Tracked<T> {
pub(crate) fn attach_registry_loaded(
&mut self,
registry: TrackingRegistryHandle,
key: SqlValue,
) -> Result<(), OrmError> {
let registration_id = registry.register_loaded(self, key)?;
self.registration_id = Some(registration_id);
self.tracking_registry = Some(registry);
Ok(())
}
pub(crate) fn attach_registry_added(&mut self, registry: TrackingRegistryHandle) {
let registration_id = registry.register_added(self);
self.registration_id = Some(registration_id);
self.tracking_registry = Some(registry);
}
#[cfg(test)]
pub(crate) fn attach_registry(&mut self, registry: TrackingRegistryHandle) {
self.attach_registry_added(registry);
}
}
impl<T> Deref for Tracked<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.current()
}
}
impl<T> DerefMut for Tracked<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.current_mut()
}
}
impl TrackingRegistry {
pub(crate) fn register_loaded<E: Entity>(
&self,
tracked: &Tracked<E>,
key: SqlValue,
) -> Result<usize, OrmError> {
let identity =
TrackedIdentity::for_entity::<E>(TrackedPrimaryKeyIdentity::Simple(key.clone()));
let mut state = self.state.lock().expect("tracking registry mutex poisoned");
if state.entries.iter().any(|entry| entry.identity == identity) {
return Err(OrmError::new(format!(
"entity `{}` with primary key value `{:?}` is already tracked in this context",
E::metadata().rust_name,
key
)));
}
Ok(state.push_registration(tracked, identity))
}
pub(crate) fn register_added<E: Entity>(&self, tracked: &Tracked<E>) -> usize {
let mut state = self.state.lock().expect("tracking registry mutex poisoned");
let temporary_identity = state.next_temporary_identity;
state.next_temporary_identity += 1;
let identity = TrackedIdentity::for_entity::<E>(TrackedPrimaryKeyIdentity::Temporary(
temporary_identity,
));
state.push_registration(tracked, identity)
}
pub(crate) fn unregister(&self, registration_id: usize) {
let mut state = self.state.lock().expect("tracking registry mutex poisoned");
state
.entries
.retain(|entry| entry.registration_id != registration_id);
}
pub fn clear(&self) {
self.state
.lock()
.expect("tracking registry mutex poisoned")
.entries
.clear();
}
pub(crate) fn tracked_for<E: Entity>(&self) -> Vec<RegisteredTracked<E>> {
let state = self.state.lock().expect("tracking registry mutex poisoned");
state
.entries
.iter()
.filter(|entry| entry.entity_type_id == TypeId::of::<E>())
.map(|entry| RegisteredTracked::<E> {
registration_id: entry.registration_id,
inner_address: entry.inner_address,
_entity: PhantomData,
})
.collect()
}
pub(crate) fn update_persisted_identity<E: Entity>(
&self,
registration_id: usize,
key: SqlValue,
) -> Result<(), OrmError> {
let identity =
TrackedIdentity::for_entity::<E>(TrackedPrimaryKeyIdentity::Simple(key.clone()));
let mut state = self.state.lock().expect("tracking registry mutex poisoned");
if state
.entries
.iter()
.any(|entry| entry.registration_id != registration_id && entry.identity == identity)
{
return Err(OrmError::new(format!(
"entity `{}` with primary key value `{:?}` is already tracked in this context",
E::metadata().rust_name,
key
)));
}
let entry = state
.entries
.iter_mut()
.find(|entry| entry.registration_id == registration_id)
.ok_or_else(|| OrmError::new("tracked entity registration was not found"))?;
entry.identity = identity;
Ok(())
}
pub fn entry_count(&self) -> usize {
self.state
.lock()
.expect("tracking registry mutex poisoned")
.entries
.len()
}
pub fn registrations(&self) -> Vec<TrackedEntityRegistration> {
self.state
.lock()
.expect("tracking registry mutex poisoned")
.entries
.iter()
.map(|entry| TrackedEntityRegistration {
entity_rust_name: entry.entity_rust_name,
state: unsafe { (entry.state_reader)(entry.inner_address as *const ()) },
})
.collect()
}
}
#[doc(hidden)]
pub fn save_changes_operation_plan(
entities: &[&'static EntityMetadata],
) -> Result<SaveChangesOperationPlan, OrmError> {
let insert_order = topological_entity_order(entities)?;
let mut delete_order = insert_order.clone();
delete_order.reverse();
Ok(SaveChangesOperationPlan {
added_order: insert_order.clone(),
modified_order: insert_order,
deleted_order: delete_order,
})
}
impl SaveChangesOperationPlan {
pub fn added_order(&self) -> &[usize] {
&self.added_order
}
pub fn modified_order(&self) -> &[usize] {
&self.modified_order
}
pub fn deleted_order(&self) -> &[usize] {
&self.deleted_order
}
}
fn topological_entity_order(entities: &[&'static EntityMetadata]) -> Result<Vec<usize>, OrmError> {
let mut outgoing_edges = vec![Vec::<usize>::new(); entities.len()];
let mut incoming_edge_count = vec![0usize; entities.len()];
for (child_index, child) in entities.iter().enumerate() {
for foreign_key in child.foreign_keys {
if foreign_key.columns.len() != 1 || foreign_key.referenced_columns.len() != 1 {
continue;
}
let Some(parent_index) = entities.iter().position(|candidate| {
candidate.schema == foreign_key.referenced_schema
&& candidate.table == foreign_key.referenced_table
}) else {
continue;
};
if parent_index == child_index || outgoing_edges[parent_index].contains(&child_index) {
continue;
}
outgoing_edges[parent_index].push(child_index);
incoming_edge_count[child_index] += 1;
}
}
let mut order = Vec::with_capacity(entities.len());
let mut ready: Vec<usize> = incoming_edge_count
.iter()
.enumerate()
.filter_map(|(index, count)| (*count == 0).then_some(index))
.collect();
while !ready.is_empty() {
ready.sort_unstable();
let entity_index = ready.remove(0);
order.push(entity_index);
for child_index in &outgoing_edges[entity_index] {
incoming_edge_count[*child_index] -= 1;
if incoming_edge_count[*child_index] == 0 {
ready.push(*child_index);
}
}
}
if order.len() != entities.len() {
return Err(OrmError::new(
"save_changes cannot determine a deterministic order for tracked operations because the context contains a foreign-key cycle",
));
}
Ok(order)
}
impl TrackingRegistryState {
fn push_registration<E: Entity>(
&mut self,
tracked: &Tracked<E>,
identity: TrackedIdentity,
) -> usize {
let registration_id = self.next_registration_id;
self.next_registration_id += 1;
self.entries.push(TrackingRegistration {
registration_id,
identity,
entity_type_id: TypeId::of::<E>(),
entity_rust_name: E::metadata().rust_name,
inner_address: tracked.inner.as_ref() as *const TrackedInner<E> as usize,
state_reader: state_reader::<E>,
});
registration_id
}
}
impl TrackedIdentity {
fn for_entity<E: Entity>(primary_key: TrackedPrimaryKeyIdentity) -> Self {
let metadata = E::metadata();
Self {
entity_type_id: TypeId::of::<E>(),
entity_rust_name: metadata.rust_name,
schema: metadata.schema,
table: metadata.table,
primary_key,
}
}
}
impl<E: Clone> RegisteredTracked<E> {
pub(crate) fn registration_id(&self) -> usize {
self.registration_id
}
pub(crate) fn state(&self) -> EntityState {
unsafe { (&*(self.inner_address as *const TrackedInner<E>)).state }
}
pub(crate) fn current_clone(&self) -> E {
unsafe {
(&*(self.inner_address as *const TrackedInner<E>))
.current
.clone()
}
}
pub(crate) fn accept_current(&self) {
unsafe {
let inner = self.inner_address as *mut TrackedInner<E>;
(*inner).original = (*inner).current.clone();
(*inner).state = EntityState::Unchanged;
}
}
pub(crate) fn sync_persisted(&self, persisted: E) {
unsafe {
let inner = self.inner_address as *mut TrackedInner<E>;
(*inner).original = persisted.clone();
(*inner).current = persisted;
(*inner).state = EntityState::Unchanged;
}
}
}
impl<E: EntityPersist> RegisteredTracked<E> {
pub(crate) fn has_persisted_changes(&self) -> bool {
unsafe {
let inner = &*(self.inner_address as *const TrackedInner<E>);
E::has_persisted_changes(&inner.original, &inner.current)
}
}
}
unsafe fn state_reader<E>(ptr: *const ()) -> EntityState {
unsafe { (&*(ptr.cast::<TrackedInner<E>>())).state }
}
impl<T: Clone> Clone for Tracked<T> {
fn clone(&self) -> Self {
Self {
inner: Box::new(TrackedInner {
original: self.original().clone(),
current: self.current().clone(),
state: self.state(),
}),
registration_id: None,
tracking_registry: None,
}
}
}
impl<T: core::fmt::Debug> core::fmt::Debug for Tracked<T> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Tracked")
.field("original", self.original())
.field("current", self.current())
.field("state", &self.state())
.finish()
}
}
impl<T: PartialEq> PartialEq for Tracked<T> {
fn eq(&self, other: &Self) -> bool {
self.original() == other.original()
&& self.current() == other.current()
&& self.state() == other.state()
}
}
impl<T: Eq> Eq for Tracked<T> {}
impl<T> Drop for Tracked<T> {
fn drop(&mut self) {
if let (Some(registration_id), Some(registry)) =
(self.registration_id.take(), self.tracking_registry.take())
{
registry.unregister(registration_id);
}
}
}
#[cfg(test)]
mod tests {
use super::{
EntityState, Tracked, TrackedEntityRegistration, TrackingRegistry,
save_changes_operation_plan,
};
use sql_orm_core::{
Entity, EntityMetadata, ForeignKeyMetadata, PrimaryKeyMetadata, ReferentialAction, SqlValue,
};
use std::sync::Arc;
#[derive(Clone)]
struct DummyEntity;
#[derive(Clone)]
struct DummyEntityAlias;
static DUMMY_ENTITY_METADATA: EntityMetadata = EntityMetadata {
rust_name: "DummyEntity",
schema: "dbo",
table: "dummy_entities",
renamed_from: None,
columns: &[],
primary_key: PrimaryKeyMetadata {
name: None,
columns: &[],
},
indexes: &[],
foreign_keys: &[],
navigations: &[],
};
static ORDER_METADATA: EntityMetadata = EntityMetadata {
rust_name: "Order",
schema: "sales",
table: "orders",
renamed_from: None,
columns: &[],
primary_key: PrimaryKeyMetadata {
name: None,
columns: &["id"],
},
indexes: &[],
foreign_keys: &[],
navigations: &[],
};
static ORDER_ITEM_FOREIGN_KEYS: [ForeignKeyMetadata; 1] = [ForeignKeyMetadata::new(
"fk_order_items_orders",
&["order_id"],
"sales",
"orders",
&["id"],
ReferentialAction::NoAction,
ReferentialAction::NoAction,
)];
static ORDER_ITEM_METADATA: EntityMetadata = EntityMetadata {
rust_name: "OrderItem",
schema: "sales",
table: "order_items",
renamed_from: None,
columns: &[],
primary_key: PrimaryKeyMetadata {
name: None,
columns: &["id"],
},
indexes: &[],
foreign_keys: &ORDER_ITEM_FOREIGN_KEYS,
navigations: &[],
};
static CATEGORY_FOREIGN_KEYS: [ForeignKeyMetadata; 1] = [ForeignKeyMetadata::new(
"fk_categories_parent",
&["parent_id"],
"catalog",
"categories",
&["id"],
ReferentialAction::NoAction,
ReferentialAction::NoAction,
)];
static CATEGORY_METADATA: EntityMetadata = EntityMetadata {
rust_name: "Category",
schema: "catalog",
table: "categories",
renamed_from: None,
columns: &[],
primary_key: PrimaryKeyMetadata {
name: None,
columns: &["id"],
},
indexes: &[],
foreign_keys: &CATEGORY_FOREIGN_KEYS,
navigations: &[],
};
static CYCLE_A_FOREIGN_KEYS: [ForeignKeyMetadata; 1] = [ForeignKeyMetadata::new(
"fk_cycle_a_cycle_b",
&["cycle_b_id"],
"dbo",
"cycle_b",
&["id"],
ReferentialAction::NoAction,
ReferentialAction::NoAction,
)];
static CYCLE_B_FOREIGN_KEYS: [ForeignKeyMetadata; 1] = [ForeignKeyMetadata::new(
"fk_cycle_b_cycle_a",
&["cycle_a_id"],
"dbo",
"cycle_a",
&["id"],
ReferentialAction::NoAction,
ReferentialAction::NoAction,
)];
static CYCLE_A_METADATA: EntityMetadata = EntityMetadata {
rust_name: "CycleA",
schema: "dbo",
table: "cycle_a",
renamed_from: None,
columns: &[],
primary_key: PrimaryKeyMetadata {
name: None,
columns: &["id"],
},
indexes: &[],
foreign_keys: &CYCLE_A_FOREIGN_KEYS,
navigations: &[],
};
static CYCLE_B_METADATA: EntityMetadata = EntityMetadata {
rust_name: "CycleB",
schema: "dbo",
table: "cycle_b",
renamed_from: None,
columns: &[],
primary_key: PrimaryKeyMetadata {
name: None,
columns: &["id"],
},
indexes: &[],
foreign_keys: &CYCLE_B_FOREIGN_KEYS,
navigations: &[],
};
impl Entity for DummyEntity {
fn metadata() -> &'static EntityMetadata {
&DUMMY_ENTITY_METADATA
}
}
impl Entity for DummyEntityAlias {
fn metadata() -> &'static EntityMetadata {
&DUMMY_ENTITY_METADATA
}
}
#[test]
fn tracked_loaded_value_keeps_original_and_current_snapshots() {
let tracked = Tracked::from_loaded(String::from("Ana"));
assert_eq!(tracked.state(), EntityState::Unchanged);
assert_eq!(tracked.original(), "Ana");
assert_eq!(tracked.current(), "Ana");
}
#[test]
fn tracked_added_value_starts_in_added_state() {
let tracked = Tracked::from_added(String::from("Luis"));
assert_eq!(tracked.state(), EntityState::Added);
assert_eq!(tracked.original(), "Luis");
assert_eq!(tracked.current(), "Luis");
}
#[test]
fn tracked_can_release_current_value() {
let tracked = Tracked::from_loaded(String::from("Maria"));
assert_eq!(tracked.into_current(), "Maria");
}
#[test]
fn into_current_consumes_registered_wrapper_and_unregisters_it() {
let registry = Arc::new(TrackingRegistry::default());
let mut tracked = Tracked::from_loaded(DummyEntity);
tracked.attach_registry(Arc::clone(®istry));
assert_eq!(registry.entry_count(), 1);
let _current = tracked.into_current();
assert_eq!(registry.entry_count(), 0);
}
#[test]
fn cloned_tracked_wrapper_is_detached_from_original_registry_entry() {
let registry = Arc::new(TrackingRegistry::default());
let mut original = Tracked::from_loaded(DummyEntity);
original.attach_registry(Arc::clone(®istry));
original.mark_modified();
let clone = original.clone();
assert_eq!(registry.entry_count(), 1);
assert_eq!(clone.state(), EntityState::Modified);
drop(clone);
assert_eq!(registry.entry_count(), 1);
assert_eq!(registry.registrations()[0].state, EntityState::Modified);
}
#[test]
fn mutable_access_transitions_loaded_entity_to_modified() {
let mut tracked = Tracked::from_loaded(String::from("Ana"));
tracked.push_str(" Maria");
assert_eq!(tracked.state(), EntityState::Modified);
assert_eq!(tracked.original(), "Ana");
assert_eq!(tracked.current(), "Ana Maria");
}
#[test]
fn current_mut_transitions_loaded_entity_to_modified() {
let mut tracked = Tracked::from_loaded(String::from("Luis"));
tracked.current_mut().push_str(" Alberto");
assert_eq!(tracked.state(), EntityState::Modified);
assert_eq!(tracked.original(), "Luis");
assert_eq!(tracked.current(), "Luis Alberto");
}
#[test]
fn explicit_mark_modified_transitions_unchanged_only() {
let mut loaded = Tracked::from_loaded(String::from("Ana"));
loaded.mark_modified();
let mut added = Tracked::from_added(String::from("Luis"));
added.mark_modified();
let mut deleted = Tracked::from_loaded(String::from("Maria"));
deleted.mark_deleted();
deleted.mark_modified();
assert_eq!(loaded.state(), EntityState::Modified);
assert_eq!(added.state(), EntityState::Added);
assert_eq!(deleted.state(), EntityState::Deleted);
}
#[test]
fn explicit_mark_deleted_transitions_wrapper_to_deleted() {
let mut tracked = Tracked::from_loaded(String::from("Ana"));
tracked.mark_deleted();
assert_eq!(tracked.state(), EntityState::Deleted);
}
#[test]
fn explicit_mark_unchanged_accepts_current_snapshot() {
let mut tracked = Tracked::from_loaded(String::from("Ana"));
tracked.current_mut().push_str(" Maria");
tracked.mark_unchanged();
assert_eq!(tracked.state(), EntityState::Unchanged);
assert_eq!(tracked.original(), "Ana Maria");
assert_eq!(tracked.current(), "Ana Maria");
}
#[test]
fn explicit_mark_unchanged_restores_deleted_wrapper_with_current_snapshot() {
let mut tracked = Tracked::from_loaded(String::from("Ana"));
tracked.current_mut().push_str(" Maria");
tracked.mark_deleted();
tracked.mark_unchanged();
assert_eq!(tracked.state(), EntityState::Unchanged);
assert_eq!(tracked.original(), "Ana Maria");
assert_eq!(tracked.current(), "Ana Maria");
}
#[test]
fn explicit_mark_unchanged_on_registered_wrapper_updates_registry_state() {
let registry = Arc::new(TrackingRegistry::default());
let mut tracked = Tracked::from_loaded(DummyEntity);
tracked.attach_registry(Arc::clone(®istry));
tracked.mark_deleted();
tracked.mark_unchanged();
assert_eq!(tracked.state(), EntityState::Unchanged);
assert_eq!(registry.entry_count(), 1);
assert_eq!(registry.registrations()[0].state, EntityState::Unchanged);
}
#[test]
fn mark_deleted_transitions_any_registered_entity_to_deleted() {
let registry = Arc::new(TrackingRegistry::default());
let mut tracked = Tracked::from_loaded(DummyEntity);
tracked.attach_registry(Arc::clone(®istry));
tracked.mark_deleted();
assert_eq!(tracked.state(), EntityState::Deleted);
assert_eq!(registry.registrations()[0].state, EntityState::Deleted);
}
#[test]
fn mark_deleted_on_added_registered_entry_cancels_pending_insert() {
let registry = Arc::new(TrackingRegistry::default());
let mut tracked = Tracked::from_added(DummyEntity);
tracked.attach_registry_added(Arc::clone(®istry));
tracked.mark_deleted();
assert_eq!(tracked.state(), EntityState::Deleted);
assert_eq!(registry.entry_count(), 0);
}
#[test]
fn mutable_access_keeps_added_state_for_new_entities() {
let mut tracked = Tracked::from_added(String::from("Maria"));
tracked.push_str(" Fernanda");
assert_eq!(tracked.state(), EntityState::Added);
assert_eq!(tracked.original(), "Maria");
assert_eq!(tracked.current(), "Maria Fernanda");
}
#[test]
fn tracking_registry_records_loaded_entities() {
let registry = Arc::new(TrackingRegistry::default());
let mut tracked = Tracked::from_loaded(DummyEntity);
tracked
.attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
.unwrap();
assert_eq!(registry.entry_count(), 1);
assert_eq!(
registry.registrations(),
vec![TrackedEntityRegistration {
entity_rust_name: "DummyEntity",
state: EntityState::Unchanged,
}]
);
}
#[test]
fn tracking_registry_records_added_entities() {
let registry = Arc::new(TrackingRegistry::default());
let mut tracked = Tracked::from_added(DummyEntity);
tracked.attach_registry(Arc::clone(®istry));
assert_eq!(registry.entry_count(), 1);
assert_eq!(
registry.registrations(),
vec![TrackedEntityRegistration {
entity_rust_name: "DummyEntity",
state: EntityState::Added,
}]
);
}
#[test]
fn tracking_registry_rejects_duplicate_loaded_identity() {
let registry = Arc::new(TrackingRegistry::default());
let mut first = Tracked::from_loaded(DummyEntity);
let mut second = Tracked::from_loaded(DummyEntity);
first
.attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
.unwrap();
let error = second
.attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
.unwrap_err();
assert_eq!(registry.entry_count(), 1);
assert!(error.message().contains("already tracked"));
}
#[test]
fn duplicate_loaded_identity_error_leaves_rejected_wrapper_detached() {
let registry = Arc::new(TrackingRegistry::default());
let mut first = Tracked::from_loaded(DummyEntity);
first
.attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
.unwrap();
{
let mut duplicate = Tracked::from_loaded(DummyEntity);
let error = duplicate
.attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
.unwrap_err();
assert!(error.message().contains("already tracked"));
assert_eq!(duplicate.state(), EntityState::Unchanged);
assert_eq!(registry.entry_count(), 1);
}
assert_eq!(registry.entry_count(), 1);
assert_eq!(registry.registrations()[0].state, EntityState::Unchanged);
}
#[test]
fn tracking_registry_scopes_loaded_identity_by_rust_type() {
let registry = Arc::new(TrackingRegistry::default());
let mut first = Tracked::from_loaded(DummyEntity);
let mut second = Tracked::from_loaded(DummyEntityAlias);
first
.attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
.unwrap();
second
.attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(7))
.unwrap();
assert_eq!(registry.entry_count(), 2);
}
#[test]
fn tracking_registry_allows_multiple_added_entities_with_temporary_identities() {
let registry = Arc::new(TrackingRegistry::default());
let mut first = Tracked::from_added(DummyEntity);
let mut second = Tracked::from_added(DummyEntity);
first.attach_registry_added(Arc::clone(®istry));
second.attach_registry_added(Arc::clone(®istry));
assert_eq!(registry.entry_count(), 2);
}
#[test]
fn tracking_registry_updates_temporary_identity_to_persisted_identity() {
let registry = Arc::new(TrackingRegistry::default());
let mut tracked = Tracked::from_added(DummyEntity);
tracked.attach_registry_added(Arc::clone(®istry));
registry
.update_persisted_identity::<DummyEntity>(
tracked.registration_id.expect("registered"),
SqlValue::I64(11),
)
.unwrap();
let mut duplicate = Tracked::from_loaded(DummyEntity);
let error = duplicate
.attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(11))
.unwrap_err();
assert!(error.message().contains("already tracked"));
}
#[test]
fn tracking_registry_rejects_persisted_identity_update_collision_without_mutating_entry() {
let registry = Arc::new(TrackingRegistry::default());
let mut existing = Tracked::from_loaded(DummyEntity);
let mut pending = Tracked::from_added(DummyEntity);
existing
.attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(11))
.unwrap();
pending.attach_registry_added(Arc::clone(®istry));
let pending_registration = pending.registration_id.expect("registered pending entity");
let error = registry
.update_persisted_identity::<DummyEntity>(pending_registration, SqlValue::I64(11))
.unwrap_err();
assert!(error.message().contains("already tracked"));
assert_eq!(registry.entry_count(), 2);
let mut duplicate = Tracked::from_loaded(DummyEntity);
let duplicate_error = duplicate
.attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(11))
.unwrap_err();
assert!(duplicate_error.message().contains("already tracked"));
registry
.update_persisted_identity::<DummyEntity>(pending_registration, SqlValue::I64(12))
.unwrap();
let mut second_duplicate = Tracked::from_loaded(DummyEntity);
let second_duplicate_error = second_duplicate
.attach_registry_loaded(Arc::clone(®istry), SqlValue::I64(12))
.unwrap_err();
assert!(second_duplicate_error.message().contains("already tracked"));
}
#[test]
fn tracking_registry_rejects_persisted_identity_update_for_missing_registration() {
let registry = TrackingRegistry::default();
let error = registry
.update_persisted_identity::<DummyEntity>(99, SqlValue::I64(11))
.unwrap_err();
assert_eq!(error.message(), "tracked entity registration was not found");
}
#[test]
fn tracking_registry_clear_removes_all_entries() {
let registry = Arc::new(TrackingRegistry::default());
let mut first = Tracked::from_added(DummyEntity);
let mut second = Tracked::from_added(DummyEntity);
first.attach_registry_added(Arc::clone(®istry));
second.attach_registry_added(Arc::clone(®istry));
registry.clear();
assert_eq!(registry.entry_count(), 0);
assert!(registry.registrations().is_empty());
}
#[test]
fn detach_registry_unregisters_without_dropping_wrapper() {
let registry = Arc::new(TrackingRegistry::default());
let mut tracked = Tracked::from_loaded(DummyEntity);
tracked.attach_registry(Arc::clone(®istry));
tracked.detach_registry();
assert_eq!(registry.entry_count(), 0);
assert_eq!(tracked.state(), EntityState::Unchanged);
}
#[test]
fn public_detach_is_idempotent_and_keeps_visible_state() {
let registry = Arc::new(TrackingRegistry::default());
let mut tracked = Tracked::from_loaded(DummyEntity);
tracked.attach_registry(Arc::clone(®istry));
tracked.mark_deleted();
tracked.detach();
tracked.detach();
assert_eq!(registry.entry_count(), 0);
assert_eq!(tracked.state(), EntityState::Deleted);
}
#[test]
fn public_detach_unregisters_without_resetting_state() {
let registry = Arc::new(TrackingRegistry::default());
let mut tracked = Tracked::from_loaded(DummyEntity);
tracked.attach_registry(Arc::clone(®istry));
tracked.mark_modified();
tracked.detach();
assert_eq!(registry.entry_count(), 0);
assert_eq!(tracked.state(), EntityState::Modified);
}
#[test]
fn tracking_registry_unregister_missing_registration_is_noop() {
let registry = Arc::new(TrackingRegistry::default());
let mut tracked = Tracked::from_loaded(DummyEntity);
tracked.attach_registry(Arc::clone(®istry));
registry.unregister(99);
assert_eq!(registry.entry_count(), 1);
assert_eq!(registry.registrations()[0].state, EntityState::Unchanged);
}
#[test]
fn dropping_tracked_entity_unregisters_it_from_registry() {
let registry = Arc::new(TrackingRegistry::default());
{
let mut tracked = Tracked::from_loaded(DummyEntity);
tracked.attach_registry(Arc::clone(®istry));
assert_eq!(registry.entry_count(), 1);
}
assert_eq!(registry.entry_count(), 0);
}
#[test]
fn save_changes_plan_orders_added_parents_before_children() {
let plan = save_changes_operation_plan(&[
&ORDER_ITEM_METADATA,
&DUMMY_ENTITY_METADATA,
&ORDER_METADATA,
])
.unwrap();
assert_eq!(plan.added_order(), &[1, 2, 0]);
assert_eq!(plan.modified_order(), &[1, 2, 0]);
}
#[test]
fn save_changes_plan_orders_deleted_children_before_parents() {
let plan = save_changes_operation_plan(&[
&ORDER_ITEM_METADATA,
&DUMMY_ENTITY_METADATA,
&ORDER_METADATA,
])
.unwrap();
assert_eq!(plan.deleted_order(), &[0, 2, 1]);
}
#[test]
fn save_changes_plan_preserves_context_order_without_dependencies() {
let plan = save_changes_operation_plan(&[&ORDER_METADATA, &DUMMY_ENTITY_METADATA]).unwrap();
assert_eq!(plan.added_order(), &[0, 1]);
assert_eq!(plan.modified_order(), &[0, 1]);
assert_eq!(plan.deleted_order(), &[1, 0]);
}
#[test]
fn save_changes_plan_ignores_foreign_keys_to_entities_outside_context() {
let plan =
save_changes_operation_plan(&[&ORDER_ITEM_METADATA, &DUMMY_ENTITY_METADATA]).unwrap();
assert_eq!(plan.added_order(), &[0, 1]);
assert_eq!(plan.modified_order(), &[0, 1]);
assert_eq!(plan.deleted_order(), &[1, 0]);
}
#[test]
fn save_changes_plan_ignores_simple_self_references() {
let plan =
save_changes_operation_plan(&[&CATEGORY_METADATA, &DUMMY_ENTITY_METADATA]).unwrap();
assert_eq!(plan.added_order(), &[0, 1]);
assert_eq!(plan.modified_order(), &[0, 1]);
assert_eq!(plan.deleted_order(), &[1, 0]);
}
#[test]
fn save_changes_plan_rejects_foreign_key_cycles() {
let error =
save_changes_operation_plan(&[&CYCLE_A_METADATA, &CYCLE_B_METADATA]).unwrap_err();
assert!(error.message().contains("foreign-key cycle"));
}
}