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