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<teaql_core::CompactRow>,
407}
408
409#[derive(Debug)]
410enum EntityGraphReference {
411 Strong(Arc<OnceLock<FrozenEntityGraph>>),
412 Weak(Weak<OnceLock<FrozenEntityGraph>>),
413}
414
415impl EntityGraphReference {
416 fn preserve(&self) -> Self {
417 match self {
418 Self::Strong(graph) => Self::Strong(graph.clone()),
419 Self::Weak(graph) => Self::Weak(graph.clone()),
420 }
421 }
422
423 fn promote(&self) -> Self {
424 match self {
425 Self::Strong(graph) => Self::Strong(graph.clone()),
426 Self::Weak(graph) => graph
427 .upgrade()
428 .map(Self::Strong)
429 .unwrap_or_else(|| Self::Strong(Arc::default())),
430 }
431 }
432
433 fn weak(&self) -> Self {
434 match self {
435 Self::Strong(graph) => Self::Weak(Arc::downgrade(graph)),
436 Self::Weak(graph) => Self::Weak(graph.clone()),
437 }
438 }
439
440 fn pointer(&self) -> *const OnceLock<FrozenEntityGraph> {
441 match self {
442 Self::Strong(graph) => Arc::as_ptr(graph),
443 Self::Weak(graph) => graph.as_ptr(),
444 }
445 }
446
447 fn strong(&self) -> Option<&Arc<OnceLock<FrozenEntityGraph>>> {
448 match self {
449 Self::Strong(graph) => Some(graph),
450 Self::Weak(_) => None,
451 }
452 }
453
454 fn frozen(&self) -> Option<&FrozenEntityGraph> {
455 match self {
456 Self::Strong(graph) => graph.get(),
457 Self::Weak(graph) => {
458 let owner = graph.upgrade()?;
459 let frozen = owner.get()? as *const FrozenEntityGraph;
460 Some(unsafe { &*frozen })
465 }
466 }
467 }
468}
469
470impl Default for EntityRuntimeState {
471 fn default() -> Self {
472 Self {
473 inner: Arc::default(),
474 graph: EntityGraphReference::Strong(Arc::default()),
475 loaded_snapshot: None,
476 }
477 }
478}
479
480impl Clone for EntityRuntimeState {
481 fn clone(&self) -> Self {
482 Self {
483 inner: self.inner.clone(),
484 graph: self.graph.promote(),
485 loaded_snapshot: self.loaded_snapshot.clone(),
486 }
487 }
488}
489
490impl std::panic::UnwindSafe for EntityRuntimeState {}
491impl std::panic::RefUnwindSafe for EntityRuntimeState {}
492
493#[derive(Debug, Clone)]
494enum OriginalSnapshot {
495 Materialized(EntitySnapshot),
496 Compact(teaql_core::CompactRow),
497}
498
499impl PartialEq for EntityRuntimeState {
500 fn eq(&self, other: &Self) -> bool {
501 if Arc::ptr_eq(&self.inner, &other.inner) {
502 return true;
503 }
504 match (self.inner.get(), other.inner.get()) {
505 (Some(left), Some(right)) => Arc::ptr_eq(left, right),
506 (None, None) => false,
507 _ => false,
508 }
509 }
510}
511
512impl EntityRuntimeState {
513 #[cfg(test)]
514 fn has_mutation_context(&self) -> bool {
515 self.inner.get().is_some()
516 }
517
518 fn context(&self) -> &Arc<Mutex<EntityMutationLedger>> {
519 self.inner
520 .get_or_init(|| Arc::new(Mutex::new(EntityMutationLedger::default())))
521 }
522
523 fn read_context<R>(&self, default: R, read: impl FnOnce(&EntityMutationLedger) -> R) -> R {
524 let Some(context) = self.inner.get() else {
525 return default;
526 };
527 let context = context.lock().unwrap_or_else(|error| error.into_inner());
528 read(&context)
529 }
530
531 fn write_context<R>(&self, write: impl FnOnce(&mut EntityMutationLedger) -> R) -> R {
532 let mut context = self
533 .context()
534 .lock()
535 .unwrap_or_else(|error| error.into_inner());
536 write(&mut context)
537 }
538
539 pub fn fresh_with_shared_graph(source: &EntityRuntimeState) -> Self {
540 Self {
541 inner: Arc::default(),
542 graph: source.graph.preserve(),
543 loaded_snapshot: None,
544 }
545 }
546
547 pub(crate) fn fresh_with_weak_graph(source: &EntityRuntimeState) -> Self {
550 Self {
551 inner: Arc::default(),
552 graph: source.graph.weak(),
553 loaded_snapshot: None,
554 }
555 }
556
557 pub fn with_shared_graph(&self, source: &EntityRuntimeState) -> Self {
560 Self {
561 inner: self.inner.clone(),
562 graph: source.graph.preserve(),
563 loaded_snapshot: self.loaded_snapshot.clone(),
564 }
565 }
566
567 #[doc(hidden)]
573 pub fn adopt_mutations_from(&self, source: &EntityRuntimeState) {
574 let Some(source_context) = source.inner.get() else {
575 return;
576 };
577 if self
578 .inner
579 .get()
580 .is_some_and(|target_context| Arc::ptr_eq(target_context, source_context))
581 {
582 return;
583 }
584 let snapshot = source_context
585 .lock()
586 .unwrap_or_else(|error| error.into_inner())
587 .clone();
588 self.write_context(|target| {
589 for change_set in snapshot.change_sets.stack {
590 for (key, values) in change_set.changes {
591 for (field, value) in values {
592 target.change_sets.set(key.clone(), field, value);
593 }
594 }
595 }
596 target.deleted_keys.extend(snapshot.deleted_keys);
597 target.new_keys.extend(snapshot.new_keys);
598 target
599 .original_versions
600 .merge_from(&snapshot.original_versions);
601 for (key, traces) in snapshot.trace_chains {
602 target.trace_chains.entry(key).or_default().extend(traces);
603 }
604 if target.original_snapshot.is_none() {
605 target.original_snapshot = snapshot.original_snapshot;
606 }
607 if target.comment.is_none() {
608 target.comment = snapshot.comment;
609 }
610 target.is_new |= snapshot.is_new;
611 });
612 }
613
614 pub fn freeze_graph(&self, builder: EntityGraphBuilder) -> Result<(), EntityGraphBuilder> {
616 let Some(graph) = self.graph.strong() else {
617 return Err(builder);
618 };
619 graph
620 .set(builder.freeze())
621 .map_err(|graph| EntityGraphBuilder {
622 tables: graph.tables,
623 relation_lists: graph.relation_lists,
624 })
625 }
626
627 pub fn resolve_entity<T>(&self, id: u64) -> Option<&T>
629 where
630 T: Any + Send + Sync,
631 {
632 self.graph
633 .frozen()?
634 .tables
635 .get(&TypeId::of::<T>())?
636 .get(&id)?
637 .downcast_ref::<T>()
638 }
639
640 pub fn resolve_relation_list<T>(
641 &self,
642 owner_entity: &str,
643 owner_id: u64,
644 relation: &str,
645 ) -> Option<&SmartList<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::<SmartList<T>>()
658 }
659
660 pub fn relation_list<T>(
662 &self,
663 owner_entity: &str,
664 owner_id: u64,
665 relation: &str,
666 ) -> RelationHandle<'_, SmartList<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 list = stored.downcast_ref::<SmartList<T>>().unwrap_or_else(|| {
682 panic!(
683 "relation view type mismatch: owner={} id={} relation={}",
684 owner_entity, owner_id, relation
685 )
686 });
687 if list.is_empty() {
688 RelationHandle::new(LoadedRelation::Empty, Some(list))
689 } else {
690 RelationHandle::new(LoadedRelation::Loaded, Some(list))
691 }
692 }
693
694 pub fn resolve_relation_option<T>(
695 &self,
696 owner_entity: &str,
697 owner_id: u64,
698 relation: &str,
699 ) -> Option<&Option<T>>
700 where
701 T: Any + Send + Sync,
702 {
703 self.graph
704 .frozen()?
705 .relation_lists
706 .get(&RelationListKey {
707 owner_entity: crate::canonical_id_space_entity(owner_entity),
708 owner_id,
709 relation: relation.to_owned(),
710 })?
711 .downcast_ref::<Option<T>>()
712 }
713
714 pub fn relation_option<T>(
716 &self,
717 owner_entity: &str,
718 owner_id: u64,
719 relation: &str,
720 ) -> RelationHandle<'_, T>
721 where
722 T: Any + Send + Sync,
723 {
724 let Some(graph) = self.graph.frozen() else {
725 return RelationHandle::new(LoadedRelation::NotLoaded, None);
726 };
727 let key = RelationListKey {
728 owner_entity: crate::canonical_id_space_entity(owner_entity),
729 owner_id,
730 relation: relation.to_owned(),
731 };
732 let Some(stored) = graph.relation_lists.get(&key) else {
733 return RelationHandle::new(LoadedRelation::NotLoaded, None);
734 };
735 let value = stored.downcast_ref::<Option<T>>().unwrap_or_else(|| {
736 panic!(
737 "relation view type mismatch: owner={} id={} relation={}",
738 owner_entity, owner_id, relation
739 )
740 });
741 match value {
742 Some(value) => RelationHandle::new(LoadedRelation::Loaded, Some(value)),
743 None => RelationHandle::new(LoadedRelation::Empty, None),
744 }
745 }
746
747 pub fn has_relation_view(&self, owner_entity: &str, owner_id: u64, relation: &str) -> bool {
748 self.graph.frozen().is_some_and(|graph| {
749 graph.relation_lists.contains_key(&RelationListKey {
750 owner_entity: crate::canonical_id_space_entity(owner_entity),
751 owner_id,
752 relation: relation.to_owned(),
753 })
754 })
755 }
756
757 pub fn push_change_set(&self) {
758 self.write_context(|context| context.change_sets.push());
759 }
760
761 pub fn pop_change_set(&self) -> Option<EntityChangeSet> {
762 self.inner.get()?;
763 self.write_context(|context| context.change_sets.pop())
764 }
765
766 pub fn clear_current_change_set(&self) {
767 if self.inner.get().is_some() {
768 self.write_context(|context| context.change_sets.clear_current());
769 }
770 }
771
772 pub fn clear_committed(&self) {
775 if self.inner.get().is_some() {
776 self.write_context(|context| {
777 context.change_sets = ChangeSetStack::default();
778 context.deleted_keys.clear();
779 context.new_keys.clear();
780 context.original_versions.clear();
781 context.trace_chains.clear();
782 context.original_snapshot = None;
783 context.comment = None;
784 context.is_new = false;
785 });
786 }
787 }
788
789 pub fn set(&self, key: EntityKey, field: impl Into<String>, value: impl Into<Value>) {
790 self.write_context(|context| context.change_sets.set(key, field, value.into()));
791 }
792
793 pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
794 self.read_context(None, |context| context.change_sets.get(key, field))
795 }
796
797 pub fn current_change_set(&self) -> EntityChangeSet {
798 self.read_context(EntityChangeSet::default(), |context| {
799 context.change_sets.current().cloned().unwrap_or_default()
800 })
801 }
802
803 pub fn set_comment(&self, comment: impl Into<String>) {
806 self.write_context(|context| context.comment = Some(comment.into()));
807 }
808
809 pub fn get_comment(&self) -> Option<String> {
811 self.read_context(None, |context| context.comment.clone())
812 }
813
814 pub fn mark_as_new(&self, key: EntityKey) {
816 self.write_context(|context| {
817 context.new_keys.insert(key);
818 });
819 }
820
821 pub fn is_new(&self, key: &EntityKey) -> bool {
823 self.read_context(false, |context| context.new_keys.contains(key))
824 }
825
826 pub fn set_original_snapshot(&self, snapshot: EntitySnapshot) {
828 self.write_context(|context| {
829 context.original_snapshot = Some(OriginalSnapshot::Materialized(snapshot));
830 });
831 }
832
833 pub fn set_original_compact_row(&mut self, row: teaql_core::CompactRow) {
835 self.loaded_snapshot = Some(row);
836 }
837
838 pub fn original_snapshot(&self) -> Option<EntitySnapshot> {
840 if let Some(row) = &self.loaded_snapshot {
841 return Some(EntitySnapshot::from(row.clone().into_map()));
842 }
843 self.read_context(None, |context| {
844 context
845 .original_snapshot
846 .as_ref()
847 .map(|snapshot| match snapshot {
848 OriginalSnapshot::Materialized(snapshot) => snapshot.clone(),
849 OriginalSnapshot::Compact(row) => EntitySnapshot::from(row.clone().into_map()),
850 })
851 })
852 }
853
854 pub fn mark_as_delete(&self, key: EntityKey) {
859 self.write_context(|context| {
860 context.change_sets.clear_entity(&key);
861 context.deleted_keys.insert(key);
862 });
863 }
864
865 pub fn is_marked_as_delete(&self, key: &EntityKey) -> bool {
867 self.read_context(false, |context| context.deleted_keys.contains(key))
868 }
869
870 pub fn changed_field_names(&self, key: &EntityKey) -> BTreeSet<String> {
873 self.read_context(BTreeSet::new(), |context| {
874 context.change_sets.changed_field_names(key)
875 })
876 }
877 pub fn deleted_keys(&self) -> std::collections::BTreeSet<EntityKey> {
878 self.read_context(BTreeSet::new(), |context| context.deleted_keys.clone())
879 }
880
881 pub fn new_keys(&self) -> std::collections::BTreeSet<EntityKey> {
882 self.read_context(BTreeSet::new(), |context| context.new_keys.clone())
883 }
884
885 pub fn get_original_version(&self, key: &EntityKey) -> Option<i64> {
886 self.read_context(None, |context| context.original_versions.get(key))
887 .or_else(|| {
888 self.loaded_snapshot
889 .as_ref()?
890 .get("id")?
891 .try_u64()
892 .filter(|id| Some(*id) == key.id.try_u64())?;
893 self.loaded_snapshot.as_ref()?.get("version")?.try_i64()
894 })
895 }
896
897 pub fn get_trace_chain(&self, key: &EntityKey) -> Vec<teaql_core::TraceNode> {
898 self.read_context(Vec::new(), |context| {
899 context.trace_chains.get(key).cloned().unwrap_or_default()
900 })
901 }
902
903 pub fn set_original_version(&self, key: EntityKey, version: i64) {
904 self.write_context(|context| context.original_versions.insert(key, version));
905 }
906}
907
908pub trait LedgerEntity: teaql_core::Entity {
909 fn entity_runtime_state(&self) -> Option<EntityRuntimeState>;
910}
911
912#[cfg(test)]
913mod lazy_root_tests {
914 use super::*;
915
916 #[derive(Clone)]
917 struct GraphChild {
918 root: EntityRuntimeState,
919 }
920
921 #[test]
922 fn loaded_snapshot_does_not_allocate_ledger_until_mutation() {
923 let mut root = EntityRuntimeState::default();
924 root.set_original_compact_row(teaql_core::CompactRow::new(
925 Arc::from(["id".to_owned(), "version".to_owned()]),
926 vec![Value::U64(7), Value::I64(3)],
927 ));
928 let key = EntityKey::new_static("Example", 7_u64);
929
930 assert!(!root.has_mutation_context());
931 assert_eq!(root.get(&key, "name"), None);
932 assert_eq!(root.get_original_version(&key), Some(3));
933 assert!(!root.has_mutation_context());
934
935 root.set(key, "name", Value::Text("updated".to_owned()));
936 assert!(root.has_mutation_context());
937 }
938
939 #[test]
940 fn clone_before_first_mutation_materializes_one_shared_ledger() {
941 let root = EntityRuntimeState::default();
942 let child = root.clone();
943 let child_key = EntityKey::new_static("Child", 2_u64);
944
945 child.set(child_key.clone(), "name", Value::Text("updated".to_owned()));
946
947 assert_eq!(
948 root.get(&child_key, "name"),
949 Some(Value::Text("updated".to_owned()))
950 );
951 assert!(root.has_mutation_context());
952 }
953
954 #[test]
955 fn explicit_graph_composition_adopts_existing_child_mutations() {
956 let parent = EntityRuntimeState::default();
957 let child = EntityRuntimeState::default();
958 let child_key = EntityKey::new_static("Child", 17_u64);
959
960 child.mark_as_new(child_key.clone());
961 child.set(child_key.clone(), "display_name", "before attach");
962 child.set_original_version(child_key.clone(), 3);
963
964 parent.adopt_mutations_from(&child);
965
966 assert!(parent.is_new(&child_key));
967 assert_eq!(
968 parent.get(&child_key, "display_name"),
969 Some(Value::Text("before attach".to_owned()))
970 );
971 assert_eq!(parent.get_original_version(&child_key), Some(3));
972 }
973
974 #[test]
975 fn graph_owned_entities_do_not_keep_the_graph_alive() {
976 let root = EntityRuntimeState::default();
977 let graph_owner = match &root.graph {
978 EntityGraphReference::Strong(graph) => Arc::downgrade(graph),
979 EntityGraphReference::Weak(_) => unreachable!(),
980 };
981 let mut builder = EntityGraphBuilder::default();
982 builder.install_relation_list(
983 "Owner",
984 1,
985 "children",
986 SmartList::from(vec![GraphChild {
987 root: EntityRuntimeState::fresh_with_weak_graph(&root),
988 }]),
989 );
990 root.freeze_graph(builder).unwrap();
991
992 drop(root);
993 assert!(graph_owner.upgrade().is_none());
994 }
995
996 #[test]
997 fn cloning_a_graph_owned_entity_promotes_its_graph_anchor() {
998 let root = EntityRuntimeState::default();
999 let graph_owner = match &root.graph {
1000 EntityGraphReference::Strong(graph) => Arc::downgrade(graph),
1001 EntityGraphReference::Weak(_) => unreachable!(),
1002 };
1003 let mut builder = EntityGraphBuilder::default();
1004 builder.install_relation_list(
1005 "Owner",
1006 1,
1007 "children",
1008 SmartList::from(vec![GraphChild {
1009 root: EntityRuntimeState::fresh_with_weak_graph(&root),
1010 }]),
1011 );
1012 root.freeze_graph(builder).unwrap();
1013 let detached = root
1014 .resolve_relation_list::<GraphChild>("Owner", 1, "children")
1015 .unwrap()[0]
1016 .clone();
1017
1018 drop(root);
1019 assert!(graph_owner.upgrade().is_some());
1020 assert!(detached.root.graph.frozen().is_some());
1021 drop(detached);
1022 assert!(graph_owner.upgrade().is_none());
1023 }
1024
1025 #[test]
1026 fn relation_handles_distinguish_loaded_empty_and_not_loaded() {
1027 let root = EntityRuntimeState::default();
1028 let mut builder = EntityGraphBuilder::default();
1029 builder.install_relation_list("Owner", 1, "loaded", SmartList::from(vec![7_u64]));
1030 builder.install_relation_list::<u64>("Owner", 1, "empty", SmartList::empty());
1031 builder.install_relation_option("Owner", 1, "present", Some(9_u64));
1032 builder.install_relation_option::<u64>("Owner", 1, "null", None);
1033 root.freeze_graph(builder).unwrap();
1034
1035 let loaded = root.relation_list::<u64>("Owner", 1, "loaded");
1036 assert_eq!(loaded.state(), LoadedRelation::Loaded);
1037 assert_eq!(loaded.value().map(|list| list.as_slice()), Some(&[7][..]));
1038
1039 let empty = root.relation_list::<u64>("Owner", 1, "empty");
1040 assert_eq!(empty.state(), LoadedRelation::Empty);
1041 assert!(empty.value().is_some_and(SmartList::is_empty));
1042
1043 let missing = root.relation_list::<u64>("Owner", 1, "missing");
1044 assert_eq!(missing.state(), LoadedRelation::NotLoaded);
1045 assert!(missing.value().is_none());
1046
1047 let present = root.relation_option::<u64>("Owner", 1, "present");
1048 assert_eq!(present.state(), LoadedRelation::Loaded);
1049 assert_eq!(present.value(), Some(&9));
1050
1051 let null = root.relation_option::<u64>("Owner", 1, "null");
1052 assert_eq!(null.state(), LoadedRelation::Empty);
1053 assert!(null.value().is_none());
1054
1055 let absent = root.relation_option::<u64>("Owner", 1, "absent");
1056 assert_eq!(absent.state(), LoadedRelation::NotLoaded);
1057 assert!(absent.value().is_none());
1058 }
1059}