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, 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 pub fn clear_entity(&mut self, key: &EntityKey) {
299 self.changes.remove(key);
300 }
301
302 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 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 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 comment: Option<String>,
376 deleted_keys: std::collections::BTreeSet<EntityKey>,
379 new_keys: std::collections::BTreeSet<EntityKey>,
381 original_snapshot: Option<OriginalSnapshot>,
383 trace_chains: std::collections::BTreeMap<EntityKey, Vec<teaql_core::TraceNode>>,
385 original_versions: OriginalVersions,
387 is_new: bool,
389}
390
391#[derive(Debug)]
392pub struct EntityRuntimeState {
393 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 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 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 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 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 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 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 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 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 pub fn set_comment(&self, comment: impl Into<String>) {
750 self.write_context(|context| context.comment = Some(comment.into()));
751 }
752
753 pub fn get_comment(&self) -> Option<String> {
755 self.read_context(None, |context| context.comment.clone())
756 }
757
758 pub fn mark_as_new(&self, key: EntityKey) {
760 self.write_context(|context| {
761 context.new_keys.insert(key);
762 });
763 }
764
765 pub fn is_new(&self, key: &EntityKey) -> bool {
767 self.read_context(false, |context| context.new_keys.contains(key))
768 }
769
770 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 pub fn set_original_compact_row(&mut self, row: teaql_core::CompactRow) {
779 self.loaded_snapshot = Some(row);
780 }
781
782 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 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 pub fn is_marked_as_delete(&self, key: &EntityKey) -> bool {
811 self.read_context(false, |context| context.deleted_keys.contains(key))
812 }
813
814 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}