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, Copy, PartialEq, Eq)]
14pub enum LoadedRelation {
15 Loaded,
16 Empty,
17 NotLoaded,
18}
19
20#[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, Clone, 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 fn merge_from(&mut self, source: &Self) {
277 if let Some((key, version)) = &source.first {
278 self.insert(key.clone(), *version);
279 }
280 for (key, version) in &source.overflow {
281 self.insert(key.clone(), *version);
282 }
283 }
284}
285
286impl EntityChangeSet {
287 pub fn is_empty(&self) -> bool {
288 self.changes.is_empty()
289 }
290
291 pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
292 self.changes
293 .entry(key)
294 .or_default()
295 .insert(field.into(), value);
296 }
297
298 pub fn get(&self, key: &EntityKey, field: &str) -> Option<&Value> {
299 self.changes.get(key).and_then(|changes| changes.get(field))
300 }
301
302 pub fn changes(&self) -> &BTreeMap<EntityKey, MutationValues> {
303 &self.changes
304 }
305
306 pub fn clear_entity(&mut self, key: &EntityKey) {
308 self.changes.remove(key);
309 }
310
311 pub fn field_names(&self, key: &EntityKey) -> BTreeSet<String> {
313 self.changes
314 .get(key)
315 .map(|record| record.keys().cloned().collect())
316 .unwrap_or_default()
317 }
318}
319
320#[derive(Debug, Clone, Default, PartialEq)]
321pub struct ChangeSetStack {
322 stack: Vec<EntityChangeSet>,
323}
324
325impl ChangeSetStack {
326 pub fn current_mut(&mut self) -> &mut EntityChangeSet {
327 if self.stack.is_empty() {
328 self.stack.push(EntityChangeSet::default());
329 }
330 self.stack.last_mut().expect("change set stack has current")
331 }
332
333 pub fn current(&self) -> Option<&EntityChangeSet> {
334 self.stack.last()
335 }
336
337 pub fn push(&mut self) {
338 self.stack.push(EntityChangeSet::default());
339 }
340
341 pub fn pop(&mut self) -> Option<EntityChangeSet> {
342 self.stack.pop()
343 }
344
345 pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
346 self.stack
347 .iter()
348 .rev()
349 .find_map(|change_set| change_set.get(key, field).cloned())
350 }
351
352 pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
353 self.current_mut().set(key, field, value);
354 }
355
356 pub fn clear_current(&mut self) {
357 if let Some(current) = self.stack.last_mut() {
358 *current = EntityChangeSet::default();
359 }
360 }
361
362 pub fn clear_entity(&mut self, key: &EntityKey) {
364 for change_set in &mut self.stack {
365 change_set.clear_entity(key);
366 }
367 }
368
369 pub fn changed_field_names(&self, key: &EntityKey) -> BTreeSet<String> {
372 let mut fields = BTreeSet::new();
373 for change_set in &self.stack {
374 fields.extend(change_set.field_names(key));
375 }
376 fields
377 }
378}
379
380#[derive(Debug, Clone, Default)]
381struct EntityMutationLedger {
382 change_sets: ChangeSetStack,
383 comment: Option<String>,
385 deleted_keys: std::collections::BTreeSet<EntityKey>,
388 new_keys: std::collections::BTreeSet<EntityKey>,
390 original_snapshot: Option<OriginalSnapshot>,
392 trace_chains: std::collections::BTreeMap<EntityKey, Vec<teaql_core::TraceNode>>,
394 original_versions: OriginalVersions,
396 is_new: bool,
398}
399
400#[derive(Debug)]
401pub struct EntityRuntimeState {
402 inner: Arc<OnceLock<Arc<Mutex<EntityMutationLedger>>>>,
405 graph: EntityGraphReference,
406 loaded_snapshot: Option<LoadedEntitySnapshot>,
407}
408
409#[derive(Debug, Clone)]
410struct LoadedEntitySnapshot {
411 entity: Arc<str>,
412 row: teaql_core::CompactRow,
413}
414
415#[derive(Debug)]
416enum EntityGraphReference {
417 Strong(Arc<OnceLock<FrozenEntityGraph>>),
418 Weak(Weak<OnceLock<FrozenEntityGraph>>),
419}
420
421impl EntityGraphReference {
422 fn preserve(&self) -> Self {
423 match self {
424 Self::Strong(graph) => Self::Strong(graph.clone()),
425 Self::Weak(graph) => Self::Weak(graph.clone()),
426 }
427 }
428
429 fn promote(&self) -> Self {
430 match self {
431 Self::Strong(graph) => Self::Strong(graph.clone()),
432 Self::Weak(graph) => graph
433 .upgrade()
434 .map(Self::Strong)
435 .unwrap_or_else(|| Self::Strong(Arc::default())),
436 }
437 }
438
439 fn weak(&self) -> Self {
440 match self {
441 Self::Strong(graph) => Self::Weak(Arc::downgrade(graph)),
442 Self::Weak(graph) => Self::Weak(graph.clone()),
443 }
444 }
445
446 fn pointer(&self) -> *const OnceLock<FrozenEntityGraph> {
447 match self {
448 Self::Strong(graph) => Arc::as_ptr(graph),
449 Self::Weak(graph) => graph.as_ptr(),
450 }
451 }
452
453 fn strong(&self) -> Option<&Arc<OnceLock<FrozenEntityGraph>>> {
454 match self {
455 Self::Strong(graph) => Some(graph),
456 Self::Weak(_) => None,
457 }
458 }
459
460 fn frozen(&self) -> Option<&FrozenEntityGraph> {
461 match self {
462 Self::Strong(graph) => graph.get(),
463 Self::Weak(graph) => {
464 let owner = graph.upgrade()?;
465 let frozen = owner.get()? as *const FrozenEntityGraph;
466 Some(unsafe { &*frozen })
471 }
472 }
473 }
474}
475
476impl Default for EntityRuntimeState {
477 fn default() -> Self {
478 Self {
479 inner: Arc::default(),
480 graph: EntityGraphReference::Strong(Arc::default()),
481 loaded_snapshot: None,
482 }
483 }
484}
485
486impl Clone for EntityRuntimeState {
487 fn clone(&self) -> Self {
488 Self {
489 inner: self.inner.clone(),
490 graph: self.graph.promote(),
491 loaded_snapshot: self.loaded_snapshot.clone(),
492 }
493 }
494}
495
496impl std::panic::UnwindSafe for EntityRuntimeState {}
497impl std::panic::RefUnwindSafe for EntityRuntimeState {}
498
499#[derive(Debug, Clone)]
500enum OriginalSnapshot {
501 Materialized(EntitySnapshot),
502 Compact(teaql_core::CompactRow),
503}
504
505impl PartialEq for EntityRuntimeState {
506 fn eq(&self, other: &Self) -> bool {
507 if Arc::ptr_eq(&self.inner, &other.inner) {
508 return true;
509 }
510 match (self.inner.get(), other.inner.get()) {
511 (Some(left), Some(right)) => Arc::ptr_eq(left, right),
512 (None, None) => false,
513 _ => false,
514 }
515 }
516}
517
518impl EntityRuntimeState {
519 #[cfg(test)]
520 fn has_mutation_context(&self) -> bool {
521 self.inner.get().is_some()
522 }
523
524 fn context(&self) -> &Arc<Mutex<EntityMutationLedger>> {
525 self.inner
526 .get_or_init(|| Arc::new(Mutex::new(EntityMutationLedger::default())))
527 }
528
529 fn read_context<R>(&self, default: R, read: impl FnOnce(&EntityMutationLedger) -> R) -> R {
530 let Some(context) = self.inner.get() else {
531 return default;
532 };
533 let context = context.lock().unwrap_or_else(|error| error.into_inner());
534 read(&context)
535 }
536
537 fn write_context<R>(&self, write: impl FnOnce(&mut EntityMutationLedger) -> R) -> R {
538 let mut context = self
539 .context()
540 .lock()
541 .unwrap_or_else(|error| error.into_inner());
542 write(&mut context)
543 }
544
545 pub fn fresh_with_shared_graph(source: &EntityRuntimeState) -> Self {
546 Self {
547 inner: Arc::default(),
548 graph: source.graph.preserve(),
549 loaded_snapshot: None,
550 }
551 }
552
553 pub(crate) fn fresh_with_weak_graph(source: &EntityRuntimeState) -> Self {
556 Self {
557 inner: Arc::default(),
558 graph: source.graph.weak(),
559 loaded_snapshot: None,
560 }
561 }
562
563 pub fn with_shared_graph(&self, source: &EntityRuntimeState) -> Self {
566 Self {
567 inner: self.inner.clone(),
568 graph: source.graph.preserve(),
569 loaded_snapshot: self.loaded_snapshot.clone(),
570 }
571 }
572
573 #[doc(hidden)]
579 pub fn adopt_mutations_from(&self, source: &EntityRuntimeState) {
580 let Some(source_context) = source.inner.get() else {
581 return;
582 };
583 if self
584 .inner
585 .get()
586 .is_some_and(|target_context| Arc::ptr_eq(target_context, source_context))
587 {
588 return;
589 }
590 let snapshot = source_context
591 .lock()
592 .unwrap_or_else(|error| error.into_inner())
593 .clone();
594 let loaded_version = source.loaded_snapshot.as_ref().and_then(|loaded| {
595 let id = loaded.row.get("id")?.clone();
596 let version = loaded.row.get("version")?.try_i64()?;
597 Some((EntityKey::new(loaded.entity.as_ref(), id), version))
598 });
599 self.write_context(|target| {
600 for change_set in snapshot.change_sets.stack {
601 for (key, values) in change_set.changes {
602 for (field, value) in values {
603 target.change_sets.set(key.clone(), field, value);
604 }
605 }
606 }
607 target.deleted_keys.extend(snapshot.deleted_keys);
608 target.new_keys.extend(snapshot.new_keys);
609 target
610 .original_versions
611 .merge_from(&snapshot.original_versions);
612 if let Some((key, version)) = loaded_version {
613 target.original_versions.insert(key, version);
614 }
615 for (key, traces) in snapshot.trace_chains {
616 target.trace_chains.entry(key).or_default().extend(traces);
617 }
618 if target.original_snapshot.is_none() {
619 target.original_snapshot = snapshot.original_snapshot;
620 }
621 if target.comment.is_none() {
622 target.comment = snapshot.comment;
623 }
624 target.is_new |= snapshot.is_new;
625 });
626 }
627
628 pub fn freeze_graph(&self, builder: EntityGraphBuilder) -> Result<(), EntityGraphBuilder> {
630 let Some(graph) = self.graph.strong() else {
631 return Err(builder);
632 };
633 graph
634 .set(builder.freeze())
635 .map_err(|graph| EntityGraphBuilder {
636 tables: graph.tables,
637 relation_lists: graph.relation_lists,
638 })
639 }
640
641 pub fn resolve_entity<T>(&self, id: u64) -> Option<&T>
643 where
644 T: Any + Send + Sync,
645 {
646 self.graph
647 .frozen()?
648 .tables
649 .get(&TypeId::of::<T>())?
650 .get(&id)?
651 .downcast_ref::<T>()
652 }
653
654 pub fn resolve_relation_list<T>(
655 &self,
656 owner_entity: &str,
657 owner_id: u64,
658 relation: &str,
659 ) -> Option<&SmartList<T>>
660 where
661 T: Any + Send + Sync,
662 {
663 self.graph
664 .frozen()?
665 .relation_lists
666 .get(&RelationListKey {
667 owner_entity: crate::canonical_id_space_entity(owner_entity),
668 owner_id,
669 relation: relation.to_owned(),
670 })?
671 .downcast_ref::<SmartList<T>>()
672 }
673
674 pub fn relation_list<T>(
676 &self,
677 owner_entity: &str,
678 owner_id: u64,
679 relation: &str,
680 ) -> RelationHandle<'_, SmartList<T>>
681 where
682 T: Any + Send + Sync,
683 {
684 let Some(graph) = self.graph.frozen() else {
685 return RelationHandle::new(LoadedRelation::NotLoaded, None);
686 };
687 let key = RelationListKey {
688 owner_entity: crate::canonical_id_space_entity(owner_entity),
689 owner_id,
690 relation: relation.to_owned(),
691 };
692 let Some(stored) = graph.relation_lists.get(&key) else {
693 return RelationHandle::new(LoadedRelation::NotLoaded, None);
694 };
695 let list = stored.downcast_ref::<SmartList<T>>().unwrap_or_else(|| {
696 panic!(
697 "relation view type mismatch: owner={} id={} relation={}",
698 owner_entity, owner_id, relation
699 )
700 });
701 if list.is_empty() {
702 RelationHandle::new(LoadedRelation::Empty, Some(list))
703 } else {
704 RelationHandle::new(LoadedRelation::Loaded, Some(list))
705 }
706 }
707
708 pub fn resolve_relation_option<T>(
709 &self,
710 owner_entity: &str,
711 owner_id: u64,
712 relation: &str,
713 ) -> Option<&Option<T>>
714 where
715 T: Any + Send + Sync,
716 {
717 self.graph
718 .frozen()?
719 .relation_lists
720 .get(&RelationListKey {
721 owner_entity: crate::canonical_id_space_entity(owner_entity),
722 owner_id,
723 relation: relation.to_owned(),
724 })?
725 .downcast_ref::<Option<T>>()
726 }
727
728 pub fn relation_option<T>(
730 &self,
731 owner_entity: &str,
732 owner_id: u64,
733 relation: &str,
734 ) -> RelationHandle<'_, T>
735 where
736 T: Any + Send + Sync,
737 {
738 let Some(graph) = self.graph.frozen() else {
739 return RelationHandle::new(LoadedRelation::NotLoaded, None);
740 };
741 let key = RelationListKey {
742 owner_entity: crate::canonical_id_space_entity(owner_entity),
743 owner_id,
744 relation: relation.to_owned(),
745 };
746 let Some(stored) = graph.relation_lists.get(&key) else {
747 return RelationHandle::new(LoadedRelation::NotLoaded, None);
748 };
749 let value = stored.downcast_ref::<Option<T>>().unwrap_or_else(|| {
750 panic!(
751 "relation view type mismatch: owner={} id={} relation={}",
752 owner_entity, owner_id, relation
753 )
754 });
755 match value {
756 Some(value) => RelationHandle::new(LoadedRelation::Loaded, Some(value)),
757 None => RelationHandle::new(LoadedRelation::Empty, None),
758 }
759 }
760
761 pub fn has_relation_view(&self, owner_entity: &str, owner_id: u64, relation: &str) -> bool {
762 self.graph.frozen().is_some_and(|graph| {
763 graph.relation_lists.contains_key(&RelationListKey {
764 owner_entity: crate::canonical_id_space_entity(owner_entity),
765 owner_id,
766 relation: relation.to_owned(),
767 })
768 })
769 }
770
771 pub fn push_change_set(&self) {
772 self.write_context(|context| context.change_sets.push());
773 }
774
775 pub fn pop_change_set(&self) -> Option<EntityChangeSet> {
776 self.inner.get()?;
777 self.write_context(|context| context.change_sets.pop())
778 }
779
780 pub fn clear_current_change_set(&self) {
781 if self.inner.get().is_some() {
782 self.write_context(|context| context.change_sets.clear_current());
783 }
784 }
785
786 pub fn clear_committed(&self) {
789 if self.inner.get().is_some() {
790 self.write_context(|context| {
791 context.change_sets = ChangeSetStack::default();
792 context.deleted_keys.clear();
793 context.new_keys.clear();
794 context.original_versions.clear();
795 context.trace_chains.clear();
796 context.original_snapshot = None;
797 context.comment = None;
798 context.is_new = false;
799 });
800 }
801 }
802
803 pub fn set(&self, key: EntityKey, field: impl Into<String>, value: impl Into<Value>) {
804 self.write_context(|context| context.change_sets.set(key, field, value.into()));
805 }
806
807 pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
808 self.read_context(None, |context| context.change_sets.get(key, field))
809 }
810
811 pub fn current_change_set(&self) -> EntityChangeSet {
812 self.read_context(EntityChangeSet::default(), |context| {
813 context.change_sets.current().cloned().unwrap_or_default()
814 })
815 }
816
817 pub fn set_comment(&self, comment: impl Into<String>) {
820 self.write_context(|context| context.comment = Some(comment.into()));
821 }
822
823 pub fn get_comment(&self) -> Option<String> {
825 self.read_context(None, |context| context.comment.clone())
826 }
827
828 pub fn mark_as_new(&self, key: EntityKey) {
830 self.write_context(|context| {
831 context.new_keys.insert(key);
832 });
833 }
834
835 pub fn is_new(&self, key: &EntityKey) -> bool {
837 self.read_context(false, |context| context.new_keys.contains(key))
838 }
839
840 pub fn set_original_snapshot(&self, snapshot: EntitySnapshot) {
842 self.write_context(|context| {
843 context.original_snapshot = Some(OriginalSnapshot::Materialized(snapshot));
844 });
845 }
846
847 pub fn set_original_compact_row(
849 &mut self,
850 entity: impl Into<Arc<str>>,
851 row: teaql_core::CompactRow,
852 ) {
853 self.loaded_snapshot = Some(LoadedEntitySnapshot {
854 entity: entity.into(),
855 row,
856 });
857 }
858
859 pub fn original_snapshot(&self) -> Option<EntitySnapshot> {
861 if let Some(snapshot) = &self.loaded_snapshot {
862 return Some(EntitySnapshot::from(snapshot.row.clone().into_map()));
863 }
864 self.read_context(None, |context| {
865 context
866 .original_snapshot
867 .as_ref()
868 .map(|snapshot| match snapshot {
869 OriginalSnapshot::Materialized(snapshot) => snapshot.clone(),
870 OriginalSnapshot::Compact(row) => EntitySnapshot::from(row.clone().into_map()),
871 })
872 })
873 }
874
875 pub fn mark_as_delete(&self, key: EntityKey) {
880 self.write_context(|context| {
881 context.change_sets.clear_entity(&key);
882 context.deleted_keys.insert(key);
883 });
884 }
885
886 pub fn is_marked_as_delete(&self, key: &EntityKey) -> bool {
888 self.read_context(false, |context| context.deleted_keys.contains(key))
889 }
890
891 pub fn changed_field_names(&self, key: &EntityKey) -> BTreeSet<String> {
894 self.read_context(BTreeSet::new(), |context| {
895 context.change_sets.changed_field_names(key)
896 })
897 }
898 pub fn deleted_keys(&self) -> std::collections::BTreeSet<EntityKey> {
899 self.read_context(BTreeSet::new(), |context| context.deleted_keys.clone())
900 }
901
902 pub fn new_keys(&self) -> std::collections::BTreeSet<EntityKey> {
903 self.read_context(BTreeSet::new(), |context| context.new_keys.clone())
904 }
905
906 pub fn get_original_version(&self, key: &EntityKey) -> Option<i64> {
907 self.read_context(None, |context| context.original_versions.get(key))
908 .or_else(|| {
909 let snapshot = self.loaded_snapshot.as_ref()?;
910 if snapshot.entity.as_ref() != key.entity.as_ref() {
911 return None;
912 }
913 snapshot
914 .row
915 .get("id")?
916 .try_u64()
917 .filter(|id| Some(*id) == key.id.try_u64())?;
918 snapshot.row.get("version")?.try_i64()
919 })
920 }
921
922 pub fn get_trace_chain(&self, key: &EntityKey) -> Vec<teaql_core::TraceNode> {
923 self.read_context(Vec::new(), |context| {
924 context.trace_chains.get(key).cloned().unwrap_or_default()
925 })
926 }
927
928 pub fn set_original_version(&self, key: EntityKey, version: i64) {
929 self.write_context(|context| context.original_versions.insert(key, version));
930 }
931}
932
933pub trait LedgerEntity: teaql_core::Entity {
934 fn entity_runtime_state(&self) -> Option<EntityRuntimeState>;
935}
936
937#[cfg(test)]
938mod lazy_root_tests {
939 use super::*;
940
941 #[derive(Clone)]
942 struct GraphChild {
943 root: EntityRuntimeState,
944 }
945
946 #[test]
947 fn loaded_snapshot_does_not_allocate_ledger_until_mutation() {
948 let mut root = EntityRuntimeState::default();
949 root.set_original_compact_row(
950 "Example",
951 teaql_core::CompactRow::new(
952 Arc::from(["id".to_owned(), "version".to_owned()]),
953 vec![Value::U64(7), Value::I64(3)],
954 ),
955 );
956 let key = EntityKey::new_static("Example", 7_u64);
957
958 assert!(!root.has_mutation_context());
959 assert_eq!(root.get(&key, "name"), None);
960 assert_eq!(root.get_original_version(&key), Some(3));
961 assert!(!root.has_mutation_context());
962
963 root.set(key, "name", Value::Text("updated".to_owned()));
964 assert!(root.has_mutation_context());
965 }
966
967 #[test]
968 fn graph_composition_retains_version_for_same_id_across_entity_types() {
969 let mut parent = EntityRuntimeState::default();
970 parent.set_original_compact_row(
971 "Order",
972 teaql_core::CompactRow::new(
973 Arc::from(["id".to_owned(), "version".to_owned()]),
974 vec![Value::U64(1), Value::I64(1)],
975 ),
976 );
977 let mut execution = EntityRuntimeState::default();
978 execution.set_original_compact_row(
979 "InferenceExecution",
980 teaql_core::CompactRow::new(
981 Arc::from(["id".to_owned(), "version".to_owned()]),
982 vec![Value::U64(1), Value::I64(2)],
983 ),
984 );
985 let order_key = EntityKey::new_static("Order", 1_u64);
986 let execution_key = EntityKey::new_static("InferenceExecution", 1_u64);
987 execution.set(
988 execution_key.clone(),
989 "execution_status",
990 Value::Text("COMPLETED".to_owned()),
991 );
992
993 assert_eq!(parent.get_original_version(&execution_key), None);
994 parent.adopt_mutations_from(&execution);
995
996 assert_eq!(parent.get_original_version(&order_key), Some(1));
997 assert_eq!(parent.get_original_version(&execution_key), Some(2));
998 }
999
1000 #[test]
1001 fn clone_before_first_mutation_materializes_one_shared_ledger() {
1002 let root = EntityRuntimeState::default();
1003 let child = root.clone();
1004 let child_key = EntityKey::new_static("Child", 2_u64);
1005
1006 child.set(child_key.clone(), "name", Value::Text("updated".to_owned()));
1007
1008 assert_eq!(
1009 root.get(&child_key, "name"),
1010 Some(Value::Text("updated".to_owned()))
1011 );
1012 assert!(root.has_mutation_context());
1013 }
1014
1015 #[test]
1016 fn explicit_graph_composition_adopts_existing_child_mutations() {
1017 let parent = EntityRuntimeState::default();
1018 let child = EntityRuntimeState::default();
1019 let child_key = EntityKey::new_static("Child", 17_u64);
1020
1021 child.mark_as_new(child_key.clone());
1022 child.set(child_key.clone(), "display_name", "before attach");
1023 child.set_original_version(child_key.clone(), 3);
1024
1025 parent.adopt_mutations_from(&child);
1026
1027 assert!(parent.is_new(&child_key));
1028 assert_eq!(
1029 parent.get(&child_key, "display_name"),
1030 Some(Value::Text("before attach".to_owned()))
1031 );
1032 assert_eq!(parent.get_original_version(&child_key), Some(3));
1033 }
1034
1035 #[test]
1036 fn graph_owned_entities_do_not_keep_the_graph_alive() {
1037 let root = EntityRuntimeState::default();
1038 let graph_owner = match &root.graph {
1039 EntityGraphReference::Strong(graph) => Arc::downgrade(graph),
1040 EntityGraphReference::Weak(_) => unreachable!(),
1041 };
1042 let mut builder = EntityGraphBuilder::default();
1043 builder.install_relation_list(
1044 "Owner",
1045 1,
1046 "children",
1047 SmartList::from(vec![GraphChild {
1048 root: EntityRuntimeState::fresh_with_weak_graph(&root),
1049 }]),
1050 );
1051 root.freeze_graph(builder).unwrap();
1052
1053 drop(root);
1054 assert!(graph_owner.upgrade().is_none());
1055 }
1056
1057 #[test]
1058 fn cloning_a_graph_owned_entity_promotes_its_graph_anchor() {
1059 let root = EntityRuntimeState::default();
1060 let graph_owner = match &root.graph {
1061 EntityGraphReference::Strong(graph) => Arc::downgrade(graph),
1062 EntityGraphReference::Weak(_) => unreachable!(),
1063 };
1064 let mut builder = EntityGraphBuilder::default();
1065 builder.install_relation_list(
1066 "Owner",
1067 1,
1068 "children",
1069 SmartList::from(vec![GraphChild {
1070 root: EntityRuntimeState::fresh_with_weak_graph(&root),
1071 }]),
1072 );
1073 root.freeze_graph(builder).unwrap();
1074 let detached = root
1075 .resolve_relation_list::<GraphChild>("Owner", 1, "children")
1076 .unwrap()[0]
1077 .clone();
1078
1079 drop(root);
1080 assert!(graph_owner.upgrade().is_some());
1081 assert!(detached.root.graph.frozen().is_some());
1082 drop(detached);
1083 assert!(graph_owner.upgrade().is_none());
1084 }
1085
1086 #[test]
1087 fn relation_handles_distinguish_loaded_empty_and_not_loaded() {
1088 let root = EntityRuntimeState::default();
1089 let mut builder = EntityGraphBuilder::default();
1090 builder.install_relation_list("Owner", 1, "loaded", SmartList::from(vec![7_u64]));
1091 builder.install_relation_list::<u64>("Owner", 1, "empty", SmartList::empty());
1092 builder.install_relation_option("Owner", 1, "present", Some(9_u64));
1093 builder.install_relation_option::<u64>("Owner", 1, "null", None);
1094 root.freeze_graph(builder).unwrap();
1095
1096 let loaded = root.relation_list::<u64>("Owner", 1, "loaded");
1097 assert_eq!(loaded.state(), LoadedRelation::Loaded);
1098 assert_eq!(loaded.value().map(|list| list.as_slice()), Some(&[7][..]));
1099
1100 let empty = root.relation_list::<u64>("Owner", 1, "empty");
1101 assert_eq!(empty.state(), LoadedRelation::Empty);
1102 assert!(empty.value().is_some_and(SmartList::is_empty));
1103
1104 let missing = root.relation_list::<u64>("Owner", 1, "missing");
1105 assert_eq!(missing.state(), LoadedRelation::NotLoaded);
1106 assert!(missing.value().is_none());
1107
1108 let present = root.relation_option::<u64>("Owner", 1, "present");
1109 assert_eq!(present.state(), LoadedRelation::Loaded);
1110 assert_eq!(present.value(), Some(&9));
1111
1112 let null = root.relation_option::<u64>("Owner", 1, "null");
1113 assert_eq!(null.state(), LoadedRelation::Empty);
1114 assert!(null.value().is_none());
1115
1116 let absent = root.relation_option::<u64>("Owner", 1, "absent");
1117 assert_eq!(absent.state(), LoadedRelation::NotLoaded);
1118 assert!(absent.value().is_none());
1119 }
1120}