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: OnceLock<Arc<Mutex<EntityMutationLedger>>>,
394 graph: EntityGraphReference,
395 loaded_snapshot: Option<teaql_core::CompactRow>,
396}
397
398#[derive(Debug)]
399enum EntityGraphReference {
400 Strong(Arc<OnceLock<FrozenEntityGraph>>),
401 Weak(Weak<OnceLock<FrozenEntityGraph>>),
402}
403
404impl EntityGraphReference {
405 fn preserve(&self) -> Self {
406 match self {
407 Self::Strong(graph) => Self::Strong(graph.clone()),
408 Self::Weak(graph) => Self::Weak(graph.clone()),
409 }
410 }
411
412 fn promote(&self) -> Self {
413 match self {
414 Self::Strong(graph) => Self::Strong(graph.clone()),
415 Self::Weak(graph) => graph
416 .upgrade()
417 .map(Self::Strong)
418 .unwrap_or_else(|| Self::Strong(Arc::default())),
419 }
420 }
421
422 fn weak(&self) -> Self {
423 match self {
424 Self::Strong(graph) => Self::Weak(Arc::downgrade(graph)),
425 Self::Weak(graph) => Self::Weak(graph.clone()),
426 }
427 }
428
429 fn pointer(&self) -> *const OnceLock<FrozenEntityGraph> {
430 match self {
431 Self::Strong(graph) => Arc::as_ptr(graph),
432 Self::Weak(graph) => graph.as_ptr(),
433 }
434 }
435
436 fn strong(&self) -> Option<&Arc<OnceLock<FrozenEntityGraph>>> {
437 match self {
438 Self::Strong(graph) => Some(graph),
439 Self::Weak(_) => None,
440 }
441 }
442
443 fn frozen(&self) -> Option<&FrozenEntityGraph> {
444 match self {
445 Self::Strong(graph) => graph.get(),
446 Self::Weak(graph) => {
447 let owner = graph.upgrade()?;
448 let frozen = owner.get()? as *const FrozenEntityGraph;
449 Some(unsafe { &*frozen })
454 }
455 }
456 }
457}
458
459impl Default for EntityRuntimeState {
460 fn default() -> Self {
461 Self {
462 inner: OnceLock::new(),
463 graph: EntityGraphReference::Strong(Arc::default()),
464 loaded_snapshot: None,
465 }
466 }
467}
468
469impl Clone for EntityRuntimeState {
470 fn clone(&self) -> Self {
471 let inner = OnceLock::new();
472 if let Some(context) = self.inner.get() {
473 let _ = inner.set(context.clone());
474 }
475 Self {
476 inner,
477 graph: self.graph.promote(),
478 loaded_snapshot: self.loaded_snapshot.clone(),
479 }
480 }
481}
482
483impl std::panic::UnwindSafe for EntityRuntimeState {}
484impl std::panic::RefUnwindSafe for EntityRuntimeState {}
485
486#[derive(Debug)]
487enum OriginalSnapshot {
488 Materialized(EntitySnapshot),
489 Compact(teaql_core::CompactRow),
490}
491
492impl PartialEq for EntityRuntimeState {
493 fn eq(&self, other: &Self) -> bool {
494 match (self.inner.get(), other.inner.get()) {
495 (Some(left), Some(right)) => Arc::ptr_eq(left, right),
496 (None, None) => {
497 self.graph.pointer() == other.graph.pointer()
498 && self.loaded_snapshot == other.loaded_snapshot
499 }
500 _ => false,
501 }
502 }
503}
504
505impl EntityRuntimeState {
506 #[cfg(test)]
507 fn has_mutation_context(&self) -> bool {
508 self.inner.get().is_some()
509 }
510
511 fn context(&self) -> &Arc<Mutex<EntityMutationLedger>> {
512 self.inner
513 .get_or_init(|| Arc::new(Mutex::new(EntityMutationLedger::default())))
514 }
515
516 fn read_context<R>(&self, default: R, read: impl FnOnce(&EntityMutationLedger) -> R) -> R {
517 let Some(context) = self.inner.get() else {
518 return default;
519 };
520 let context = context.lock().unwrap_or_else(|error| error.into_inner());
521 read(&context)
522 }
523
524 fn write_context<R>(&self, write: impl FnOnce(&mut EntityMutationLedger) -> R) -> R {
525 let mut context = self
526 .context()
527 .lock()
528 .unwrap_or_else(|error| error.into_inner());
529 write(&mut context)
530 }
531
532 pub fn fresh_with_shared_graph(source: &EntityRuntimeState) -> Self {
533 Self {
534 inner: OnceLock::new(),
535 graph: source.graph.preserve(),
536 loaded_snapshot: None,
537 }
538 }
539
540 pub(crate) fn fresh_with_weak_graph(source: &EntityRuntimeState) -> Self {
543 Self {
544 inner: OnceLock::new(),
545 graph: source.graph.weak(),
546 loaded_snapshot: None,
547 }
548 }
549
550 pub fn with_shared_graph(&self, source: &EntityRuntimeState) -> Self {
553 Self {
554 inner: self.inner.clone(),
555 graph: source.graph.preserve(),
556 loaded_snapshot: self.loaded_snapshot.clone(),
557 }
558 }
559
560 pub fn freeze_graph(&self, builder: EntityGraphBuilder) -> Result<(), EntityGraphBuilder> {
562 let Some(graph) = self.graph.strong() else {
563 return Err(builder);
564 };
565 graph
566 .set(builder.freeze())
567 .map_err(|graph| EntityGraphBuilder {
568 tables: graph.tables,
569 relation_lists: graph.relation_lists,
570 })
571 }
572
573 pub fn resolve_entity<T>(&self, id: u64) -> Option<&T>
575 where
576 T: Any + Send + Sync,
577 {
578 self.graph
579 .frozen()?
580 .tables
581 .get(&TypeId::of::<T>())?
582 .get(&id)?
583 .downcast_ref::<T>()
584 }
585
586 pub fn resolve_relation_list<T>(
587 &self,
588 owner_entity: &str,
589 owner_id: u64,
590 relation: &str,
591 ) -> Option<&SmartList<T>>
592 where
593 T: Any + Send + Sync,
594 {
595 self.graph
596 .frozen()?
597 .relation_lists
598 .get(&RelationListKey {
599 owner_entity: crate::canonical_id_space_entity(owner_entity),
600 owner_id,
601 relation: relation.to_owned(),
602 })?
603 .downcast_ref::<SmartList<T>>()
604 }
605
606 pub fn relation_list<T>(
608 &self,
609 owner_entity: &str,
610 owner_id: u64,
611 relation: &str,
612 ) -> RelationHandle<'_, SmartList<T>>
613 where
614 T: Any + Send + Sync,
615 {
616 let Some(graph) = self.graph.frozen() else {
617 return RelationHandle::new(LoadedRelation::NotLoaded, None);
618 };
619 let key = RelationListKey {
620 owner_entity: crate::canonical_id_space_entity(owner_entity),
621 owner_id,
622 relation: relation.to_owned(),
623 };
624 let Some(stored) = graph.relation_lists.get(&key) else {
625 return RelationHandle::new(LoadedRelation::NotLoaded, None);
626 };
627 let list = stored.downcast_ref::<SmartList<T>>().unwrap_or_else(|| {
628 panic!(
629 "relation view type mismatch: owner={} id={} relation={}",
630 owner_entity, owner_id, relation
631 )
632 });
633 if list.is_empty() {
634 RelationHandle::new(LoadedRelation::Empty, Some(list))
635 } else {
636 RelationHandle::new(LoadedRelation::Loaded, Some(list))
637 }
638 }
639
640 pub fn resolve_relation_option<T>(
641 &self,
642 owner_entity: &str,
643 owner_id: u64,
644 relation: &str,
645 ) -> Option<&Option<T>>
646 where
647 T: Any + Send + Sync,
648 {
649 self.graph
650 .frozen()?
651 .relation_lists
652 .get(&RelationListKey {
653 owner_entity: crate::canonical_id_space_entity(owner_entity),
654 owner_id,
655 relation: relation.to_owned(),
656 })?
657 .downcast_ref::<Option<T>>()
658 }
659
660 pub fn relation_option<T>(
662 &self,
663 owner_entity: &str,
664 owner_id: u64,
665 relation: &str,
666 ) -> RelationHandle<'_, T>
667 where
668 T: Any + Send + Sync,
669 {
670 let Some(graph) = self.graph.frozen() else {
671 return RelationHandle::new(LoadedRelation::NotLoaded, None);
672 };
673 let key = RelationListKey {
674 owner_entity: crate::canonical_id_space_entity(owner_entity),
675 owner_id,
676 relation: relation.to_owned(),
677 };
678 let Some(stored) = graph.relation_lists.get(&key) else {
679 return RelationHandle::new(LoadedRelation::NotLoaded, None);
680 };
681 let value = stored.downcast_ref::<Option<T>>().unwrap_or_else(|| {
682 panic!(
683 "relation view type mismatch: owner={} id={} relation={}",
684 owner_entity, owner_id, relation
685 )
686 });
687 match value {
688 Some(value) => RelationHandle::new(LoadedRelation::Loaded, Some(value)),
689 None => RelationHandle::new(LoadedRelation::Empty, None),
690 }
691 }
692
693 pub fn has_relation_view(&self, owner_entity: &str, owner_id: u64, relation: &str) -> bool {
694 self.graph.frozen().is_some_and(|graph| {
695 graph.relation_lists.contains_key(&RelationListKey {
696 owner_entity: crate::canonical_id_space_entity(owner_entity),
697 owner_id,
698 relation: relation.to_owned(),
699 })
700 })
701 }
702
703 pub fn push_change_set(&self) {
704 self.write_context(|context| context.change_sets.push());
705 }
706
707 pub fn pop_change_set(&self) -> Option<EntityChangeSet> {
708 self.inner.get()?;
709 self.write_context(|context| context.change_sets.pop())
710 }
711
712 pub fn clear_current_change_set(&self) {
713 if self.inner.get().is_some() {
714 self.write_context(|context| context.change_sets.clear_current());
715 }
716 }
717
718 pub fn clear_committed(&self) {
721 if self.inner.get().is_some() {
722 self.write_context(|context| {
723 context.change_sets = ChangeSetStack::default();
724 context.deleted_keys.clear();
725 context.new_keys.clear();
726 context.original_versions.clear();
727 context.trace_chains.clear();
728 context.original_snapshot = None;
729 context.comment = None;
730 context.is_new = false;
731 });
732 }
733 }
734
735 pub fn set(&self, key: EntityKey, field: impl Into<String>, value: impl Into<Value>) {
736 self.write_context(|context| context.change_sets.set(key, field, value.into()));
737 }
738
739 pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
740 self.read_context(None, |context| context.change_sets.get(key, field))
741 }
742
743 pub fn current_change_set(&self) -> EntityChangeSet {
744 self.read_context(EntityChangeSet::default(), |context| {
745 context.change_sets.current().cloned().unwrap_or_default()
746 })
747 }
748
749 pub fn set_comment(&self, comment: impl Into<String>) {
752 self.write_context(|context| context.comment = Some(comment.into()));
753 }
754
755 pub fn get_comment(&self) -> Option<String> {
757 self.read_context(None, |context| context.comment.clone())
758 }
759
760 pub fn mark_as_new(&self, key: EntityKey) {
762 self.write_context(|context| {
763 context.new_keys.insert(key);
764 });
765 }
766
767 pub fn is_new(&self, key: &EntityKey) -> bool {
769 self.read_context(false, |context| context.new_keys.contains(key))
770 }
771
772 pub fn set_original_snapshot(&self, snapshot: EntitySnapshot) {
774 self.write_context(|context| {
775 context.original_snapshot = Some(OriginalSnapshot::Materialized(snapshot));
776 });
777 }
778
779 pub fn set_original_compact_row(&mut self, row: teaql_core::CompactRow) {
781 self.loaded_snapshot = Some(row);
782 }
783
784 pub fn original_snapshot(&self) -> Option<EntitySnapshot> {
786 if let Some(row) = &self.loaded_snapshot {
787 return Some(EntitySnapshot::from(row.clone().into_map()));
788 }
789 self.read_context(None, |context| {
790 context
791 .original_snapshot
792 .as_ref()
793 .map(|snapshot| match snapshot {
794 OriginalSnapshot::Materialized(snapshot) => snapshot.clone(),
795 OriginalSnapshot::Compact(row) => EntitySnapshot::from(row.clone().into_map()),
796 })
797 })
798 }
799
800 pub fn mark_as_delete(&self, key: EntityKey) {
805 self.write_context(|context| {
806 context.change_sets.clear_entity(&key);
807 context.deleted_keys.insert(key);
808 });
809 }
810
811 pub fn is_marked_as_delete(&self, key: &EntityKey) -> bool {
813 self.read_context(false, |context| context.deleted_keys.contains(key))
814 }
815
816 pub fn changed_field_names(&self, key: &EntityKey) -> BTreeSet<String> {
819 self.read_context(BTreeSet::new(), |context| {
820 context.change_sets.changed_field_names(key)
821 })
822 }
823 pub fn deleted_keys(&self) -> std::collections::BTreeSet<EntityKey> {
824 self.read_context(BTreeSet::new(), |context| context.deleted_keys.clone())
825 }
826
827 pub fn new_keys(&self) -> std::collections::BTreeSet<EntityKey> {
828 self.read_context(BTreeSet::new(), |context| context.new_keys.clone())
829 }
830
831 pub fn get_original_version(&self, key: &EntityKey) -> Option<i64> {
832 self.read_context(None, |context| context.original_versions.get(key))
833 .or_else(|| {
834 self.loaded_snapshot
835 .as_ref()?
836 .get("id")?
837 .try_u64()
838 .filter(|id| Some(*id) == key.id.try_u64())?;
839 self.loaded_snapshot.as_ref()?.get("version")?.try_i64()
840 })
841 }
842
843 pub fn get_trace_chain(&self, key: &EntityKey) -> Vec<teaql_core::TraceNode> {
844 self.read_context(Vec::new(), |context| {
845 context.trace_chains.get(key).cloned().unwrap_or_default()
846 })
847 }
848
849 pub fn set_original_version(&self, key: EntityKey, version: i64) {
850 self.write_context(|context| context.original_versions.insert(key, version));
851 }
852}
853
854pub trait LedgerEntity: teaql_core::Entity {
855 fn entity_runtime_state(&self) -> Option<EntityRuntimeState>;
856}
857
858#[cfg(test)]
859mod lazy_root_tests {
860 use super::*;
861
862 #[derive(Clone)]
863 struct GraphChild {
864 root: EntityRuntimeState,
865 }
866
867 #[test]
868 fn loaded_snapshot_does_not_allocate_ledger_until_mutation() {
869 let mut root = EntityRuntimeState::default();
870 root.set_original_compact_row(teaql_core::CompactRow::new(
871 Arc::from(["id".to_owned(), "version".to_owned()]),
872 vec![Value::U64(7), Value::I64(3)],
873 ));
874 let key = EntityKey::new_static("Example", 7_u64);
875
876 assert!(!root.has_mutation_context());
877 assert_eq!(root.get(&key, "name"), None);
878 assert_eq!(root.get_original_version(&key), Some(3));
879 assert!(!root.has_mutation_context());
880
881 root.set(key, "name", Value::Text("updated".to_owned()));
882 assert!(root.has_mutation_context());
883 }
884
885 #[test]
886 fn graph_owned_entities_do_not_keep_the_graph_alive() {
887 let root = EntityRuntimeState::default();
888 let graph_owner = match &root.graph {
889 EntityGraphReference::Strong(graph) => Arc::downgrade(graph),
890 EntityGraphReference::Weak(_) => unreachable!(),
891 };
892 let mut builder = EntityGraphBuilder::default();
893 builder.install_relation_list(
894 "Owner",
895 1,
896 "children",
897 SmartList::from(vec![GraphChild {
898 root: EntityRuntimeState::fresh_with_weak_graph(&root),
899 }]),
900 );
901 root.freeze_graph(builder).unwrap();
902
903 drop(root);
904 assert!(graph_owner.upgrade().is_none());
905 }
906
907 #[test]
908 fn cloning_a_graph_owned_entity_promotes_its_graph_anchor() {
909 let root = EntityRuntimeState::default();
910 let graph_owner = match &root.graph {
911 EntityGraphReference::Strong(graph) => Arc::downgrade(graph),
912 EntityGraphReference::Weak(_) => unreachable!(),
913 };
914 let mut builder = EntityGraphBuilder::default();
915 builder.install_relation_list(
916 "Owner",
917 1,
918 "children",
919 SmartList::from(vec![GraphChild {
920 root: EntityRuntimeState::fresh_with_weak_graph(&root),
921 }]),
922 );
923 root.freeze_graph(builder).unwrap();
924 let detached = root
925 .resolve_relation_list::<GraphChild>("Owner", 1, "children")
926 .unwrap()[0]
927 .clone();
928
929 drop(root);
930 assert!(graph_owner.upgrade().is_some());
931 assert!(detached.root.graph.frozen().is_some());
932 drop(detached);
933 assert!(graph_owner.upgrade().is_none());
934 }
935
936 #[test]
937 fn relation_handles_distinguish_loaded_empty_and_not_loaded() {
938 let root = EntityRuntimeState::default();
939 let mut builder = EntityGraphBuilder::default();
940 builder.install_relation_list("Owner", 1, "loaded", SmartList::from(vec![7_u64]));
941 builder.install_relation_list::<u64>("Owner", 1, "empty", SmartList::empty());
942 builder.install_relation_option("Owner", 1, "present", Some(9_u64));
943 builder.install_relation_option::<u64>("Owner", 1, "null", None);
944 root.freeze_graph(builder).unwrap();
945
946 let loaded = root.relation_list::<u64>("Owner", 1, "loaded");
947 assert_eq!(loaded.state(), LoadedRelation::Loaded);
948 assert_eq!(loaded.value().map(|list| list.as_slice()), Some(&[7][..]));
949
950 let empty = root.relation_list::<u64>("Owner", 1, "empty");
951 assert_eq!(empty.state(), LoadedRelation::Empty);
952 assert!(empty.value().is_some_and(SmartList::is_empty));
953
954 let missing = root.relation_list::<u64>("Owner", 1, "missing");
955 assert_eq!(missing.state(), LoadedRelation::NotLoaded);
956 assert!(missing.value().is_none());
957
958 let present = root.relation_option::<u64>("Owner", 1, "present");
959 assert_eq!(present.state(), LoadedRelation::Loaded);
960 assert_eq!(present.value(), Some(&9));
961
962 let null = root.relation_option::<u64>("Owner", 1, "null");
963 assert_eq!(null.state(), LoadedRelation::Empty);
964 assert!(null.value().is_none());
965
966 let absent = root.relation_option::<u64>("Owner", 1, "absent");
967 assert_eq!(absent.state(), LoadedRelation::NotLoaded);
968 assert!(absent.value().is_none());
969 }
970}