Skip to main content

teaql_runtime/
entity_runtime.rs

1use std::any::{Any, TypeId};
2use std::borrow::Cow;
3use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::sync::{Arc, Mutex, OnceLock, Weak};
5
6use teaql_core::{EntitySnapshot, MutationValues, SmartList, Value};
7
8#[derive(Debug, Clone)]
9pub struct EntityKey {
10    pub entity: Cow<'static, str>,
11    pub id: Value,
12    id_key: EntityIdentityKey,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
16enum EntityIdentityKey {
17    Null,
18    Bool(bool),
19    I64(i64),
20    U64(u64),
21    F64(u64),
22    Decimal(rust_decimal::Decimal),
23    Text(String),
24    Date(chrono::NaiveDate),
25    Timestamp(i64),
26    Other(String),
27}
28
29impl EntityKey {
30    pub fn new(entity: impl Into<String>, id: impl Into<Value>) -> Self {
31        let id = id.into();
32        Self {
33            entity: Cow::Owned(entity.into()),
34            id_key: entity_identity_key(&id),
35            id,
36        }
37    }
38
39    pub fn new_static(entity: &'static str, id: impl Into<Value>) -> Self {
40        let id = id.into();
41        Self {
42            entity: Cow::Borrowed(entity),
43            id_key: entity_identity_key(&id),
44            id,
45        }
46    }
47}
48
49impl PartialEq for EntityKey {
50    fn eq(&self, other: &Self) -> bool {
51        self.entity == other.entity && self.id_key == other.id_key
52    }
53}
54
55impl Eq for EntityKey {}
56
57impl PartialOrd for EntityKey {
58    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
59        Some(self.cmp(other))
60    }
61}
62
63impl Ord for EntityKey {
64    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
65        self.entity
66            .cmp(&other.entity)
67            .then_with(|| self.id_key.cmp(&other.id_key))
68    }
69}
70
71fn entity_identity_key(value: &Value) -> EntityIdentityKey {
72    match value {
73        Value::Null | Value::TypedNull(_) => EntityIdentityKey::Null,
74        Value::Bool(value) => EntityIdentityKey::Bool(*value),
75        Value::I64(value) => EntityIdentityKey::I64(*value),
76        Value::U64(value) => EntityIdentityKey::U64(*value),
77        Value::F64(value) => EntityIdentityKey::F64(value.to_bits()),
78        Value::Decimal(value) => EntityIdentityKey::Decimal(*value),
79        Value::Text(value) => EntityIdentityKey::Text(value.clone()),
80        Value::Json(value) => EntityIdentityKey::Other(format!("json:{value}")),
81        Value::Date(value) => EntityIdentityKey::Date(*value),
82        Value::Timestamp(value) => EntityIdentityKey::Timestamp(value.0),
83        Value::Object(_) => EntityIdentityKey::Other("object".to_owned()),
84        Value::List(_) => EntityIdentityKey::Other("list".to_owned()),
85    }
86}
87
88#[derive(Default)]
89pub struct EntityGraphBuilder {
90    tables: HashMap<TypeId, EntityTable>,
91    relation_lists: HashMap<RelationListKey, Box<dyn Any + Send + Sync>>,
92}
93
94type EntityTable = HashMap<u64, Box<dyn Any + Send + Sync>>;
95
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97struct RelationListKey {
98    owner_entity: String,
99    owner_id: u64,
100    relation: String,
101}
102
103impl EntityGraphBuilder {
104    pub fn install<T>(&mut self, id: u64, entity: T)
105    where
106        T: Any + Send + Sync,
107    {
108        self.tables
109            .entry(TypeId::of::<T>())
110            .or_default()
111            .insert(id, Box::new(entity));
112    }
113
114    pub fn entity_count(&self) -> usize {
115        self.tables.values().map(HashMap::len).sum()
116    }
117
118    pub fn install_relation_list<T>(
119        &mut self,
120        owner_entity: impl Into<String>,
121        owner_id: u64,
122        relation: impl Into<String>,
123        list: SmartList<T>,
124    ) where
125        T: Any + Send + Sync,
126    {
127        self.relation_lists.insert(
128            RelationListKey {
129                owner_entity: crate::canonical_id_space_entity(&owner_entity.into()),
130                owner_id,
131                relation: relation.into(),
132            },
133            Box::new(list),
134        );
135    }
136
137    pub fn install_relation_option<T>(
138        &mut self,
139        owner_entity: impl Into<String>,
140        owner_id: u64,
141        relation: impl Into<String>,
142        value: Option<T>,
143    ) where
144        T: Any + Send + Sync,
145    {
146        self.relation_lists.insert(
147            RelationListKey {
148                owner_entity: crate::canonical_id_space_entity(&owner_entity.into()),
149                owner_id,
150                relation: relation.into(),
151            },
152            Box::new(value),
153        );
154    }
155
156    pub fn relation_list_count(&self) -> usize {
157        self.relation_lists.len()
158    }
159
160    fn freeze(self) -> FrozenEntityGraph {
161        FrozenEntityGraph {
162            tables: self.tables,
163            relation_lists: self.relation_lists,
164        }
165    }
166}
167
168impl std::fmt::Debug for EntityGraphBuilder {
169    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        formatter
171            .debug_struct("EntityGraphBuilder")
172            .field("entity_types", &self.tables.len())
173            .field("entities", &self.entity_count())
174            .field("relation_lists", &self.relation_list_count())
175            .finish()
176    }
177}
178
179struct FrozenEntityGraph {
180    tables: HashMap<TypeId, EntityTable>,
181    relation_lists: HashMap<RelationListKey, Box<dyn Any + Send + Sync>>,
182}
183
184impl std::fmt::Debug for FrozenEntityGraph {
185    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        formatter
187            .debug_struct("FrozenEntityGraph")
188            .field("entity_types", &self.tables.len())
189            .field(
190                "entities",
191                &self.tables.values().map(HashMap::len).sum::<usize>(),
192            )
193            .field("relation_lists", &self.relation_lists.len())
194            .finish()
195    }
196}
197
198#[derive(Debug, Clone, Default, PartialEq)]
199pub struct EntityChangeSet {
200    changes: BTreeMap<EntityKey, MutationValues>,
201}
202
203#[derive(Debug, Default)]
204struct OriginalVersions {
205    first: Option<(EntityKey, i64)>,
206    overflow: BTreeMap<EntityKey, i64>,
207}
208
209impl OriginalVersions {
210    fn clear(&mut self) {
211        self.first = None;
212        self.overflow.clear();
213    }
214
215    fn get(&self, key: &EntityKey) -> Option<i64> {
216        self.first
217            .as_ref()
218            .and_then(|(first_key, version)| (first_key == key).then_some(*version))
219            .or_else(|| self.overflow.get(key).copied())
220    }
221
222    fn insert(&mut self, key: EntityKey, version: i64) {
223        match &mut self.first {
224            None => self.first = Some((key, version)),
225            Some((first_key, first_version)) if first_key == &key => *first_version = version,
226            Some(_) => {
227                self.overflow.insert(key, version);
228            }
229        }
230    }
231}
232
233impl EntityChangeSet {
234    pub fn is_empty(&self) -> bool {
235        self.changes.is_empty()
236    }
237
238    pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
239        self.changes
240            .entry(key)
241            .or_default()
242            .insert(field.into(), value);
243    }
244
245    pub fn get(&self, key: &EntityKey, field: &str) -> Option<&Value> {
246        self.changes.get(key).and_then(|changes| changes.get(field))
247    }
248
249    pub fn changes(&self) -> &BTreeMap<EntityKey, MutationValues> {
250        &self.changes
251    }
252
253    /// Remove all pending changes for a specific entity key.
254    pub fn clear_entity(&mut self, key: &EntityKey) {
255        self.changes.remove(key);
256    }
257
258    /// Get the set of field names that have been modified for a given entity key.
259    pub fn field_names(&self, key: &EntityKey) -> BTreeSet<String> {
260        self.changes
261            .get(key)
262            .map(|record| record.keys().cloned().collect())
263            .unwrap_or_default()
264    }
265}
266
267#[derive(Debug, Clone, Default, PartialEq)]
268pub struct ChangeSetStack {
269    stack: Vec<EntityChangeSet>,
270}
271
272impl ChangeSetStack {
273    pub fn current_mut(&mut self) -> &mut EntityChangeSet {
274        if self.stack.is_empty() {
275            self.stack.push(EntityChangeSet::default());
276        }
277        self.stack.last_mut().expect("change set stack has current")
278    }
279
280    pub fn current(&self) -> Option<&EntityChangeSet> {
281        self.stack.last()
282    }
283
284    pub fn push(&mut self) {
285        self.stack.push(EntityChangeSet::default());
286    }
287
288    pub fn pop(&mut self) -> Option<EntityChangeSet> {
289        self.stack.pop()
290    }
291
292    pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
293        self.stack
294            .iter()
295            .rev()
296            .find_map(|change_set| change_set.get(key, field).cloned())
297    }
298
299    pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
300        self.current_mut().set(key, field, value);
301    }
302
303    pub fn clear_current(&mut self) {
304        if let Some(current) = self.stack.last_mut() {
305            *current = EntityChangeSet::default();
306        }
307    }
308
309    /// Remove all pending changes for a specific entity key across all stack levels.
310    pub fn clear_entity(&mut self, key: &EntityKey) {
311        for change_set in &mut self.stack {
312            change_set.clear_entity(key);
313        }
314    }
315
316    /// Get the union of all changed field names for a given entity key across all stack levels.
317    /// This is the Rust equivalent of Java's `entity.getUpdatedProperties()`.
318    pub fn changed_field_names(&self, key: &EntityKey) -> BTreeSet<String> {
319        let mut fields = BTreeSet::new();
320        for change_set in &self.stack {
321            fields.extend(change_set.field_names(key));
322        }
323        fields
324    }
325}
326
327#[derive(Debug, Default)]
328struct EntityMutationLedger {
329    change_sets: ChangeSetStack,
330    /// Annotation comment for observability during graph save.
331    comment: Option<String>,
332    /// Entity keys that have been marked for deletion.
333    /// When the entity is saved, the graph save pipeline will treat these as Remove operations.
334    deleted_keys: std::collections::BTreeSet<EntityKey>,
335    /// Entity keys that have been marked as newly inserted.
336    new_keys: std::collections::BTreeSet<EntityKey>,
337    /// The original loaded snapshot, used to avoid redundant fetching during save.
338    original_snapshot: Option<OriginalSnapshot>,
339    /// Trace chains associated with each entity key.
340    trace_chains: std::collections::BTreeMap<EntityKey, Vec<teaql_core::TraceNode>>,
341    /// Original versions of entities to perform optimistic concurrency control.
342    original_versions: OriginalVersions,
343    /// Indicates if this entity root is entirely new.
344    is_new: bool,
345}
346
347#[derive(Debug)]
348pub struct EntityRuntimeState {
349    inner: OnceLock<Arc<Mutex<EntityMutationLedger>>>,
350    graph: EntityGraphReference,
351    loaded_snapshot: Option<teaql_core::CompactRow>,
352}
353
354#[derive(Debug)]
355enum EntityGraphReference {
356    Strong(Arc<OnceLock<FrozenEntityGraph>>),
357    Weak(Weak<OnceLock<FrozenEntityGraph>>),
358}
359
360impl EntityGraphReference {
361    fn preserve(&self) -> Self {
362        match self {
363            Self::Strong(graph) => Self::Strong(graph.clone()),
364            Self::Weak(graph) => Self::Weak(graph.clone()),
365        }
366    }
367
368    fn promote(&self) -> Self {
369        match self {
370            Self::Strong(graph) => Self::Strong(graph.clone()),
371            Self::Weak(graph) => graph
372                .upgrade()
373                .map(Self::Strong)
374                .unwrap_or_else(|| Self::Strong(Arc::default())),
375        }
376    }
377
378    fn weak(&self) -> Self {
379        match self {
380            Self::Strong(graph) => Self::Weak(Arc::downgrade(graph)),
381            Self::Weak(graph) => Self::Weak(graph.clone()),
382        }
383    }
384
385    fn pointer(&self) -> *const OnceLock<FrozenEntityGraph> {
386        match self {
387            Self::Strong(graph) => Arc::as_ptr(graph),
388            Self::Weak(graph) => graph.as_ptr(),
389        }
390    }
391
392    fn strong(&self) -> Option<&Arc<OnceLock<FrozenEntityGraph>>> {
393        match self {
394            Self::Strong(graph) => Some(graph),
395            Self::Weak(_) => None,
396        }
397    }
398
399    fn frozen(&self) -> Option<&FrozenEntityGraph> {
400        match self {
401            Self::Strong(graph) => graph.get(),
402            Self::Weak(graph) => {
403                let owner = graph.upgrade()?;
404                let frozen = owner.get()? as *const FrozenEntityGraph;
405                // SAFETY: weak graph references are only installed into entities owned by the
406                // same frozen graph. Such an entity can only be borrowed while an owning root
407                // keeps the graph alive. Cloning EntityRuntimeState promotes the weak reference to a
408                // strong owner, so an entity moved out through safe code also anchors the graph.
409                Some(unsafe { &*frozen })
410            }
411        }
412    }
413}
414
415impl Default for EntityRuntimeState {
416    fn default() -> Self {
417        Self {
418            inner: OnceLock::new(),
419            graph: EntityGraphReference::Strong(Arc::default()),
420            loaded_snapshot: None,
421        }
422    }
423}
424
425impl Clone for EntityRuntimeState {
426    fn clone(&self) -> Self {
427        let inner = OnceLock::new();
428        if let Some(context) = self.inner.get() {
429            let _ = inner.set(context.clone());
430        }
431        Self {
432            inner,
433            graph: self.graph.promote(),
434            loaded_snapshot: self.loaded_snapshot.clone(),
435        }
436    }
437}
438
439impl std::panic::UnwindSafe for EntityRuntimeState {}
440impl std::panic::RefUnwindSafe for EntityRuntimeState {}
441
442#[derive(Debug)]
443enum OriginalSnapshot {
444    Materialized(EntitySnapshot),
445    Compact(teaql_core::CompactRow),
446}
447
448impl PartialEq for EntityRuntimeState {
449    fn eq(&self, other: &Self) -> bool {
450        match (self.inner.get(), other.inner.get()) {
451            (Some(left), Some(right)) => Arc::ptr_eq(left, right),
452            (None, None) => {
453                self.graph.pointer() == other.graph.pointer()
454                    && self.loaded_snapshot == other.loaded_snapshot
455            }
456            _ => false,
457        }
458    }
459}
460
461impl EntityRuntimeState {
462    #[cfg(test)]
463    fn has_mutation_context(&self) -> bool {
464        self.inner.get().is_some()
465    }
466
467    fn context(&self) -> &Arc<Mutex<EntityMutationLedger>> {
468        self.inner
469            .get_or_init(|| Arc::new(Mutex::new(EntityMutationLedger::default())))
470    }
471
472    fn read_context<R>(&self, default: R, read: impl FnOnce(&EntityMutationLedger) -> R) -> R {
473        let Some(context) = self.inner.get() else {
474            return default;
475        };
476        let context = context.lock().unwrap_or_else(|error| error.into_inner());
477        read(&context)
478    }
479
480    fn write_context<R>(&self, write: impl FnOnce(&mut EntityMutationLedger) -> R) -> R {
481        let mut context = self
482            .context()
483            .lock()
484            .unwrap_or_else(|error| error.into_inner());
485        write(&mut context)
486    }
487
488    pub fn fresh_with_shared_graph(source: &EntityRuntimeState) -> Self {
489        Self {
490            inner: OnceLock::new(),
491            graph: source.graph.preserve(),
492            loaded_snapshot: None,
493        }
494    }
495
496    /// Create a root view for an entity stored inside the graph itself. The weak view prevents
497    /// the graph from strongly owning an entity that strongly owns the graph in return.
498    pub(crate) fn fresh_with_weak_graph(source: &EntityRuntimeState) -> Self {
499        Self {
500            inner: OnceLock::new(),
501            graph: source.graph.weak(),
502            loaded_snapshot: None,
503        }
504    }
505
506    /// Make this root resolve entities from the same flat graph as `source`.
507    /// Existing snapshots and mutation ledger state remain owned by this root.
508    pub fn with_shared_graph(&self, source: &EntityRuntimeState) -> Self {
509        Self {
510            inner: self.inner.clone(),
511            graph: source.graph.preserve(),
512            loaded_snapshot: self.loaded_snapshot.clone(),
513        }
514    }
515
516    /// Publish a completely assembled graph. It becomes immutable after this call.
517    pub fn freeze_graph(&self, builder: EntityGraphBuilder) -> Result<(), EntityGraphBuilder> {
518        let Some(graph) = self.graph.strong() else {
519            return Err(builder);
520        };
521        graph
522            .set(builder.freeze())
523            .map_err(|graph| EntityGraphBuilder {
524                tables: graph.tables,
525                relation_lists: graph.relation_lists,
526            })
527    }
528
529    /// Resolve an entity by type and ID without locking or reference cloning.
530    pub fn resolve_entity<T>(&self, id: u64) -> Option<&T>
531    where
532        T: Any + Send + Sync,
533    {
534        self.graph
535            .frozen()?
536            .tables
537            .get(&TypeId::of::<T>())?
538            .get(&id)?
539            .downcast_ref::<T>()
540    }
541
542    pub fn resolve_relation_list<T>(
543        &self,
544        owner_entity: &str,
545        owner_id: u64,
546        relation: &str,
547    ) -> Option<&SmartList<T>>
548    where
549        T: Any + Send + Sync,
550    {
551        self.graph
552            .frozen()?
553            .relation_lists
554            .get(&RelationListKey {
555                owner_entity: crate::canonical_id_space_entity(owner_entity),
556                owner_id,
557                relation: relation.to_owned(),
558            })?
559            .downcast_ref::<SmartList<T>>()
560    }
561
562    pub fn resolve_relation_option<T>(
563        &self,
564        owner_entity: &str,
565        owner_id: u64,
566        relation: &str,
567    ) -> Option<&Option<T>>
568    where
569        T: Any + Send + Sync,
570    {
571        self.graph
572            .frozen()?
573            .relation_lists
574            .get(&RelationListKey {
575                owner_entity: crate::canonical_id_space_entity(owner_entity),
576                owner_id,
577                relation: relation.to_owned(),
578            })?
579            .downcast_ref::<Option<T>>()
580    }
581
582    pub fn has_relation_view(&self, owner_entity: &str, owner_id: u64, relation: &str) -> bool {
583        self.graph.frozen().is_some_and(|graph| {
584            graph.relation_lists.contains_key(&RelationListKey {
585                owner_entity: crate::canonical_id_space_entity(owner_entity),
586                owner_id,
587                relation: relation.to_owned(),
588            })
589        })
590    }
591
592    pub fn push_change_set(&self) {
593        self.write_context(|context| context.change_sets.push());
594    }
595
596    pub fn pop_change_set(&self) -> Option<EntityChangeSet> {
597        self.inner.get()?;
598        self.write_context(|context| context.change_sets.pop())
599    }
600
601    pub fn clear_current_change_set(&self) {
602        if self.inner.get().is_some() {
603            self.write_context(|context| context.change_sets.clear_current());
604        }
605    }
606
607    /// Clear all state consumed by a successfully committed ledger save.
608    /// Failed saves must not call this method so their pending intent remains retryable.
609    pub fn clear_committed(&self) {
610        if self.inner.get().is_some() {
611            self.write_context(|context| {
612                context.change_sets = ChangeSetStack::default();
613                context.deleted_keys.clear();
614                context.new_keys.clear();
615                context.original_versions.clear();
616                context.trace_chains.clear();
617                context.original_snapshot = None;
618                context.comment = None;
619                context.is_new = false;
620            });
621        }
622    }
623
624    pub fn set(&self, key: EntityKey, field: impl Into<String>, value: impl Into<Value>) {
625        self.write_context(|context| context.change_sets.set(key, field, value.into()));
626    }
627
628    pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
629        self.read_context(None, |context| context.change_sets.get(key, field))
630    }
631
632    pub fn current_change_set(&self) -> EntityChangeSet {
633        self.read_context(EntityChangeSet::default(), |context| {
634            context.change_sets.current().cloned().unwrap_or_default()
635        })
636    }
637
638    /// Set an annotation comment on this entity root.
639    /// The comment propagates through the graph save process for observability.
640    pub fn set_comment(&self, comment: impl Into<String>) {
641        self.write_context(|context| context.comment = Some(comment.into()));
642    }
643
644    /// Get the annotation comment, if any.
645    pub fn get_comment(&self) -> Option<String> {
646        self.read_context(None, |context| context.comment.clone())
647    }
648
649    /// Mark this entity root as a newly created entity in memory.
650    pub fn mark_as_new(&self, key: EntityKey) {
651        self.write_context(|context| {
652            context.new_keys.insert(key);
653        });
654    }
655
656    /// Check if this entity root is marked as newly created.
657    pub fn is_new(&self, key: &EntityKey) -> bool {
658        self.read_context(false, |context| context.new_keys.contains(key))
659    }
660
661    /// Store an original loaded entity snapshot.
662    pub fn set_original_snapshot(&self, snapshot: EntitySnapshot) {
663        self.write_context(|context| {
664            context.original_snapshot = Some(OriginalSnapshot::Materialized(snapshot));
665        });
666    }
667
668    /// Store a shared-schema snapshot without allocating a mutation ledger.
669    pub fn set_original_compact_row(&mut self, row: teaql_core::CompactRow) {
670        self.loaded_snapshot = Some(row);
671    }
672
673    /// Retrieve the original loaded entity snapshot.
674    pub fn original_snapshot(&self) -> Option<EntitySnapshot> {
675        if let Some(row) = &self.loaded_snapshot {
676            return Some(EntitySnapshot::from(row.clone().into_map()));
677        }
678        self.read_context(None, |context| {
679            context
680                .original_snapshot
681                .as_ref()
682                .map(|snapshot| match snapshot {
683                    OriginalSnapshot::Materialized(snapshot) => snapshot.clone(),
684                    OriginalSnapshot::Compact(row) => EntitySnapshot::from(row.clone().into_map()),
685                })
686        })
687    }
688
689    /// Mark an entity as deleted. The next `save()` call will treat this entity
690    /// as a Remove operation in the graph save pipeline.
691    /// Any pending field changes for this entity are cleared — they are irrelevant
692    /// when the entity is being deleted.
693    pub fn mark_as_delete(&self, key: EntityKey) {
694        self.write_context(|context| {
695            context.change_sets.clear_entity(&key);
696            context.deleted_keys.insert(key);
697        });
698    }
699
700    /// Check whether an entity has been marked for deletion.
701    pub fn is_marked_as_delete(&self, key: &EntityKey) -> bool {
702        self.read_context(false, |context| context.deleted_keys.contains(key))
703    }
704
705    /// Get the set of field names that have been modified for the given entity key.
706    /// This is the Rust equivalent of Java's `entity.getUpdatedProperties()`.
707    pub fn changed_field_names(&self, key: &EntityKey) -> BTreeSet<String> {
708        self.read_context(BTreeSet::new(), |context| {
709            context.change_sets.changed_field_names(key)
710        })
711    }
712    pub fn deleted_keys(&self) -> std::collections::BTreeSet<EntityKey> {
713        self.read_context(BTreeSet::new(), |context| context.deleted_keys.clone())
714    }
715
716    pub fn new_keys(&self) -> std::collections::BTreeSet<EntityKey> {
717        self.read_context(BTreeSet::new(), |context| context.new_keys.clone())
718    }
719
720    pub fn get_original_version(&self, key: &EntityKey) -> Option<i64> {
721        self.read_context(None, |context| context.original_versions.get(key))
722            .or_else(|| {
723                self.loaded_snapshot
724                    .as_ref()?
725                    .get("id")?
726                    .try_u64()
727                    .filter(|id| Some(*id) == key.id.try_u64())?;
728                self.loaded_snapshot.as_ref()?.get("version")?.try_i64()
729            })
730    }
731
732    pub fn get_trace_chain(&self, key: &EntityKey) -> Vec<teaql_core::TraceNode> {
733        self.read_context(Vec::new(), |context| {
734            context.trace_chains.get(key).cloned().unwrap_or_default()
735        })
736    }
737
738    pub fn set_original_version(&self, key: EntityKey, version: i64) {
739        self.write_context(|context| context.original_versions.insert(key, version));
740    }
741}
742
743pub trait LedgerEntity: teaql_core::Entity {
744    fn entity_runtime_state(&self) -> Option<EntityRuntimeState>;
745}
746
747#[cfg(test)]
748mod lazy_root_tests {
749    use super::*;
750
751    #[derive(Clone)]
752    struct GraphChild {
753        root: EntityRuntimeState,
754    }
755
756    #[test]
757    fn loaded_snapshot_does_not_allocate_ledger_until_mutation() {
758        let mut root = EntityRuntimeState::default();
759        root.set_original_compact_row(teaql_core::CompactRow::new(
760            Arc::from(["id".to_owned(), "version".to_owned()]),
761            vec![Value::U64(7), Value::I64(3)],
762        ));
763        let key = EntityKey::new_static("Example", 7_u64);
764
765        assert!(!root.has_mutation_context());
766        assert_eq!(root.get(&key, "name"), None);
767        assert_eq!(root.get_original_version(&key), Some(3));
768        assert!(!root.has_mutation_context());
769
770        root.set(key, "name", Value::Text("updated".to_owned()));
771        assert!(root.has_mutation_context());
772    }
773
774    #[test]
775    fn graph_owned_entities_do_not_keep_the_graph_alive() {
776        let root = EntityRuntimeState::default();
777        let graph_owner = match &root.graph {
778            EntityGraphReference::Strong(graph) => Arc::downgrade(graph),
779            EntityGraphReference::Weak(_) => unreachable!(),
780        };
781        let mut builder = EntityGraphBuilder::default();
782        builder.install_relation_list(
783            "Owner",
784            1,
785            "children",
786            SmartList::from(vec![GraphChild {
787                root: EntityRuntimeState::fresh_with_weak_graph(&root),
788            }]),
789        );
790        root.freeze_graph(builder).unwrap();
791
792        drop(root);
793        assert!(graph_owner.upgrade().is_none());
794    }
795
796    #[test]
797    fn cloning_a_graph_owned_entity_promotes_its_graph_anchor() {
798        let root = EntityRuntimeState::default();
799        let graph_owner = match &root.graph {
800            EntityGraphReference::Strong(graph) => Arc::downgrade(graph),
801            EntityGraphReference::Weak(_) => unreachable!(),
802        };
803        let mut builder = EntityGraphBuilder::default();
804        builder.install_relation_list(
805            "Owner",
806            1,
807            "children",
808            SmartList::from(vec![GraphChild {
809                root: EntityRuntimeState::fresh_with_weak_graph(&root),
810            }]),
811        );
812        root.freeze_graph(builder).unwrap();
813        let detached = root
814            .resolve_relation_list::<GraphChild>("Owner", 1, "children")
815            .unwrap()[0]
816            .clone();
817
818        drop(root);
819        assert!(graph_owner.upgrade().is_some());
820        assert!(detached.root.graph.frozen().is_some());
821        drop(detached);
822        assert!(graph_owner.upgrade().is_none());
823    }
824}