Skip to main content

teaql_runtime/
entity_runtime.rs

1use std::any::{Any, TypeId};
2use std::borrow::Cow;
3use std::collections::{BTreeMap, BTreeSet, HashMap};
4use std::sync::{Arc, Mutex, OnceLock};
5
6use teaql_core::{EntitySnapshot, MutationValues, SmartList, Value};
7
8#[derive(Debug, Clone)]
9pub struct EntityKey {
10    pub entity: Cow<'static, str>,
11    pub id: Value,
12    id_key: EntityIdentityKey,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
16enum EntityIdentityKey {
17    Null,
18    Bool(bool),
19    I64(i64),
20    U64(u64),
21    F64(u64),
22    Decimal(rust_decimal::Decimal),
23    Text(String),
24    Date(chrono::NaiveDate),
25    Timestamp(i64),
26    Other(String),
27}
28
29impl EntityKey {
30    pub fn new(entity: impl Into<String>, id: impl Into<Value>) -> Self {
31        let id = id.into();
32        Self {
33            entity: Cow::Owned(entity.into()),
34            id_key: entity_identity_key(&id),
35            id,
36        }
37    }
38
39    pub fn new_static(entity: &'static str, id: impl Into<Value>) -> Self {
40        let id = id.into();
41        Self {
42            entity: Cow::Borrowed(entity),
43            id_key: entity_identity_key(&id),
44            id,
45        }
46    }
47}
48
49impl PartialEq for EntityKey {
50    fn eq(&self, other: &Self) -> bool {
51        self.entity == other.entity && self.id_key == other.id_key
52    }
53}
54
55impl Eq for EntityKey {}
56
57impl PartialOrd for EntityKey {
58    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
59        Some(self.cmp(other))
60    }
61}
62
63impl Ord for EntityKey {
64    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
65        self.entity
66            .cmp(&other.entity)
67            .then_with(|| self.id_key.cmp(&other.id_key))
68    }
69}
70
71fn entity_identity_key(value: &Value) -> EntityIdentityKey {
72    match value {
73        Value::Null | Value::TypedNull(_) => EntityIdentityKey::Null,
74        Value::Bool(value) => EntityIdentityKey::Bool(*value),
75        Value::I64(value) => EntityIdentityKey::I64(*value),
76        Value::U64(value) => EntityIdentityKey::U64(*value),
77        Value::F64(value) => EntityIdentityKey::F64(value.to_bits()),
78        Value::Decimal(value) => EntityIdentityKey::Decimal(*value),
79        Value::Text(value) => EntityIdentityKey::Text(value.clone()),
80        Value::Json(value) => EntityIdentityKey::Other(format!("json:{value}")),
81        Value::Date(value) => EntityIdentityKey::Date(*value),
82        Value::Timestamp(value) => EntityIdentityKey::Timestamp(value.0),
83        Value::Object(_) => EntityIdentityKey::Other("object".to_owned()),
84        Value::List(_) => EntityIdentityKey::Other("list".to_owned()),
85    }
86}
87
88#[derive(Default)]
89pub struct EntityGraphBuilder {
90    tables: HashMap<TypeId, HashMap<u64, Box<dyn Any + Send + Sync>>>,
91    relation_lists: HashMap<RelationListKey, Box<dyn Any + Send + Sync>>,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq, Hash)]
95struct RelationListKey {
96    owner_entity: String,
97    owner_id: u64,
98    relation: String,
99}
100
101impl EntityGraphBuilder {
102    pub fn install<T>(&mut self, id: u64, entity: T)
103    where
104        T: Any + Send + Sync,
105    {
106        self.tables
107            .entry(TypeId::of::<T>())
108            .or_default()
109            .insert(id, Box::new(entity));
110    }
111
112    pub fn entity_count(&self) -> usize {
113        self.tables.values().map(HashMap::len).sum()
114    }
115
116    pub fn install_relation_list<T>(
117        &mut self,
118        owner_entity: impl Into<String>,
119        owner_id: u64,
120        relation: impl Into<String>,
121        list: SmartList<T>,
122    ) where
123        T: Any + Send + Sync,
124    {
125        self.relation_lists.insert(
126            RelationListKey {
127                owner_entity: crate::canonical_id_space_entity(&owner_entity.into()),
128                owner_id,
129                relation: relation.into(),
130            },
131            Box::new(list),
132        );
133    }
134
135    pub fn install_relation_option<T>(
136        &mut self,
137        owner_entity: impl Into<String>,
138        owner_id: u64,
139        relation: impl Into<String>,
140        value: Option<T>,
141    ) where
142        T: Any + Send + Sync,
143    {
144        self.relation_lists.insert(
145            RelationListKey {
146                owner_entity: crate::canonical_id_space_entity(&owner_entity.into()),
147                owner_id,
148                relation: relation.into(),
149            },
150            Box::new(value),
151        );
152    }
153
154    pub fn relation_list_count(&self) -> usize {
155        self.relation_lists.len()
156    }
157
158    fn freeze(self) -> FrozenEntityGraph {
159        FrozenEntityGraph {
160            tables: self.tables,
161            relation_lists: self.relation_lists,
162        }
163    }
164}
165
166impl std::fmt::Debug for EntityGraphBuilder {
167    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        formatter
169            .debug_struct("EntityGraphBuilder")
170            .field("entity_types", &self.tables.len())
171            .field("entities", &self.entity_count())
172            .field("relation_lists", &self.relation_list_count())
173            .finish()
174    }
175}
176
177struct FrozenEntityGraph {
178    tables: HashMap<TypeId, HashMap<u64, Box<dyn Any + Send + Sync>>>,
179    relation_lists: HashMap<RelationListKey, Box<dyn Any + Send + Sync>>,
180}
181
182impl std::fmt::Debug for FrozenEntityGraph {
183    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        formatter
185            .debug_struct("FrozenEntityGraph")
186            .field("entity_types", &self.tables.len())
187            .field(
188                "entities",
189                &self.tables.values().map(HashMap::len).sum::<usize>(),
190            )
191            .field("relation_lists", &self.relation_lists.len())
192            .finish()
193    }
194}
195
196#[derive(Debug, Clone, Default, PartialEq)]
197pub struct EntityChangeSet {
198    changes: BTreeMap<EntityKey, MutationValues>,
199}
200
201#[derive(Debug, Default)]
202struct OriginalVersions {
203    first: Option<(EntityKey, i64)>,
204    overflow: BTreeMap<EntityKey, i64>,
205}
206
207impl OriginalVersions {
208    fn clear(&mut self) {
209        self.first = None;
210        self.overflow.clear();
211    }
212
213    fn get(&self, key: &EntityKey) -> Option<i64> {
214        self.first
215            .as_ref()
216            .and_then(|(first_key, version)| (first_key == key).then_some(*version))
217            .or_else(|| self.overflow.get(key).copied())
218    }
219
220    fn insert(&mut self, key: EntityKey, version: i64) {
221        match &mut self.first {
222            None => self.first = Some((key, version)),
223            Some((first_key, first_version)) if first_key == &key => *first_version = version,
224            Some(_) => {
225                self.overflow.insert(key, version);
226            }
227        }
228    }
229}
230
231impl EntityChangeSet {
232    pub fn is_empty(&self) -> bool {
233        self.changes.is_empty()
234    }
235
236    pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
237        self.changes
238            .entry(key)
239            .or_default()
240            .insert(field.into(), value);
241    }
242
243    pub fn get(&self, key: &EntityKey, field: &str) -> Option<&Value> {
244        self.changes.get(key).and_then(|changes| changes.get(field))
245    }
246
247    pub fn changes(&self) -> &BTreeMap<EntityKey, MutationValues> {
248        &self.changes
249    }
250
251    /// Remove all pending changes for a specific entity key.
252    pub fn clear_entity(&mut self, key: &EntityKey) {
253        self.changes.remove(key);
254    }
255
256    /// Get the set of field names that have been modified for a given entity key.
257    pub fn field_names(&self, key: &EntityKey) -> BTreeSet<String> {
258        self.changes
259            .get(key)
260            .map(|record| record.keys().cloned().collect())
261            .unwrap_or_default()
262    }
263}
264
265#[derive(Debug, Clone, Default, PartialEq)]
266pub struct ChangeSetStack {
267    stack: Vec<EntityChangeSet>,
268}
269
270impl ChangeSetStack {
271    pub fn current_mut(&mut self) -> &mut EntityChangeSet {
272        if self.stack.is_empty() {
273            self.stack.push(EntityChangeSet::default());
274        }
275        self.stack.last_mut().expect("change set stack has current")
276    }
277
278    pub fn current(&self) -> Option<&EntityChangeSet> {
279        self.stack.last()
280    }
281
282    pub fn push(&mut self) {
283        self.stack.push(EntityChangeSet::default());
284    }
285
286    pub fn pop(&mut self) -> Option<EntityChangeSet> {
287        self.stack.pop()
288    }
289
290    pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
291        self.stack
292            .iter()
293            .rev()
294            .find_map(|change_set| change_set.get(key, field).cloned())
295    }
296
297    pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
298        self.current_mut().set(key, field, value);
299    }
300
301    pub fn clear_current(&mut self) {
302        if let Some(current) = self.stack.last_mut() {
303            *current = EntityChangeSet::default();
304        }
305    }
306
307    /// Remove all pending changes for a specific entity key across all stack levels.
308    pub fn clear_entity(&mut self, key: &EntityKey) {
309        for change_set in &mut self.stack {
310            change_set.clear_entity(key);
311        }
312    }
313
314    /// Get the union of all changed field names for a given entity key across all stack levels.
315    /// This is the Rust equivalent of Java's `entity.getUpdatedProperties()`.
316    pub fn changed_field_names(&self, key: &EntityKey) -> BTreeSet<String> {
317        let mut fields = BTreeSet::new();
318        for change_set in &self.stack {
319            fields.extend(change_set.field_names(key));
320        }
321        fields
322    }
323}
324
325#[derive(Debug, Default)]
326pub struct RootContext {
327    change_sets: ChangeSetStack,
328    /// Annotation comment for observability during graph save.
329    comment: Option<String>,
330    /// Entity keys that have been marked for deletion.
331    /// When the entity is saved, the graph save pipeline will treat these as Remove operations.
332    deleted_keys: std::collections::BTreeSet<EntityKey>,
333    /// Entity keys that have been marked as newly inserted.
334    new_keys: std::collections::BTreeSet<EntityKey>,
335    /// The original loaded snapshot, used to avoid redundant fetching during save.
336    original_snapshot: Option<OriginalSnapshot>,
337    /// Trace chains associated with each entity key.
338    trace_chains: std::collections::BTreeMap<EntityKey, Vec<teaql_core::TraceNode>>,
339    /// Original versions of entities to perform optimistic concurrency control.
340    original_versions: OriginalVersions,
341    /// Indicates if this entity root is entirely new.
342    is_new: bool,
343}
344
345#[derive(Debug, Clone, Default)]
346pub struct EntityRoot {
347    inner: Arc<Mutex<RootContext>>,
348    graph: Arc<OnceLock<FrozenEntityGraph>>,
349}
350
351impl std::panic::UnwindSafe for EntityRoot {}
352impl std::panic::RefUnwindSafe for EntityRoot {}
353
354#[derive(Debug)]
355enum OriginalSnapshot {
356    Materialized(EntitySnapshot),
357    Compact(teaql_core::CompactRow),
358}
359
360impl PartialEq for EntityRoot {
361    fn eq(&self, other: &Self) -> bool {
362        Arc::ptr_eq(&self.inner, &other.inner)
363    }
364}
365
366impl EntityRoot {
367    /// Make this root resolve entities from the same flat graph as `source`.
368    /// Existing snapshots and mutation ledger state remain owned by this root.
369    pub fn with_shared_graph(&self, source: &EntityRoot) -> Self {
370        Self {
371            inner: self.inner.clone(),
372            graph: source.graph.clone(),
373        }
374    }
375
376    /// Publish a completely assembled graph. It becomes immutable after this call.
377    pub fn freeze_graph(&self, builder: EntityGraphBuilder) -> Result<(), EntityGraphBuilder> {
378        self.graph
379            .set(builder.freeze())
380            .map_err(|graph| EntityGraphBuilder {
381                tables: graph.tables,
382                relation_lists: graph.relation_lists,
383            })
384    }
385
386    /// Resolve an entity by type and ID without locking or reference cloning.
387    pub fn resolve_entity<T>(&self, id: u64) -> Option<&T>
388    where
389        T: Any + Send + Sync,
390    {
391        self.graph
392            .get()?
393            .tables
394            .get(&TypeId::of::<T>())?
395            .get(&id)?
396            .downcast_ref::<T>()
397    }
398
399    pub fn resolve_relation_list<T>(
400        &self,
401        owner_entity: &str,
402        owner_id: u64,
403        relation: &str,
404    ) -> Option<&SmartList<T>>
405    where
406        T: Any + Send + Sync,
407    {
408        self.graph
409            .get()?
410            .relation_lists
411            .get(&RelationListKey {
412                owner_entity: crate::canonical_id_space_entity(owner_entity),
413                owner_id,
414                relation: relation.to_owned(),
415            })?
416            .downcast_ref::<SmartList<T>>()
417    }
418
419    pub fn resolve_relation_option<T>(
420        &self,
421        owner_entity: &str,
422        owner_id: u64,
423        relation: &str,
424    ) -> Option<&Option<T>>
425    where
426        T: Any + Send + Sync,
427    {
428        self.graph
429            .get()?
430            .relation_lists
431            .get(&RelationListKey {
432                owner_entity: crate::canonical_id_space_entity(owner_entity),
433                owner_id,
434                relation: relation.to_owned(),
435            })?
436            .downcast_ref::<Option<T>>()
437    }
438
439    pub fn has_relation_view(&self, owner_entity: &str, owner_id: u64, relation: &str) -> bool {
440        self.graph.get().is_some_and(|graph| {
441            graph.relation_lists.contains_key(&RelationListKey {
442                owner_entity: crate::canonical_id_space_entity(owner_entity),
443                owner_id,
444                relation: relation.to_owned(),
445            })
446        })
447    }
448
449    pub fn push_change_set(&self) {
450        self.inner
451            .lock()
452            .unwrap_or_else(|e| e.into_inner())
453            .change_sets
454            .push();
455    }
456
457    pub fn pop_change_set(&self) -> Option<EntityChangeSet> {
458        self.inner
459            .lock()
460            .unwrap_or_else(|e| e.into_inner())
461            .change_sets
462            .pop()
463    }
464
465    pub fn clear_current_change_set(&self) {
466        self.inner
467            .lock()
468            .unwrap_or_else(|e| e.into_inner())
469            .change_sets
470            .clear_current();
471    }
472
473    /// Clear all state consumed by a successfully committed ledger save.
474    /// Failed saves must not call this method so their pending intent remains retryable.
475    pub fn clear_committed(&self) {
476        let mut context = self.inner.lock().unwrap_or_else(|e| e.into_inner());
477        context.change_sets = ChangeSetStack::default();
478        context.deleted_keys.clear();
479        context.new_keys.clear();
480        context.original_versions.clear();
481        context.trace_chains.clear();
482        context.original_snapshot = None;
483        context.comment = None;
484        context.is_new = false;
485    }
486
487    pub fn set(&self, key: EntityKey, field: impl Into<String>, value: impl Into<Value>) {
488        self.inner
489            .lock()
490            .unwrap_or_else(|e| e.into_inner())
491            .change_sets
492            .set(key, field, value.into());
493    }
494
495    pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
496        self.inner
497            .lock()
498            .unwrap_or_else(|e| e.into_inner())
499            .change_sets
500            .get(key, field)
501    }
502
503    pub fn current_change_set(&self) -> EntityChangeSet {
504        self.inner
505            .lock()
506            .unwrap_or_else(|e| e.into_inner())
507            .change_sets
508            .current()
509            .cloned()
510            .unwrap_or_default()
511    }
512
513    /// Set an annotation comment on this entity root.
514    /// The comment propagates through the graph save process for observability.
515    pub fn set_comment(&self, comment: impl Into<String>) {
516        self.inner.lock().unwrap_or_else(|e| e.into_inner()).comment = Some(comment.into());
517    }
518
519    /// Get the annotation comment, if any.
520    pub fn get_comment(&self) -> Option<String> {
521        self.inner
522            .lock()
523            .unwrap_or_else(|e| e.into_inner())
524            .comment
525            .clone()
526    }
527
528    /// Mark this entity root as a newly created entity in memory.
529    pub fn mark_as_new(&self, key: EntityKey) {
530        self.inner
531            .lock()
532            .unwrap_or_else(|e| e.into_inner())
533            .new_keys
534            .insert(key);
535    }
536
537    /// Check if this entity root is marked as newly created.
538    pub fn is_new(&self, key: &EntityKey) -> bool {
539        self.inner
540            .lock()
541            .unwrap_or_else(|e| e.into_inner())
542            .new_keys
543            .contains(key)
544    }
545
546    /// Store an original loaded entity snapshot.
547    pub fn set_original_snapshot(&self, snapshot: EntitySnapshot) {
548        self.inner
549            .lock()
550            .unwrap_or_else(|e| e.into_inner())
551            .original_snapshot = Some(OriginalSnapshot::Materialized(snapshot));
552    }
553
554    /// Store a shared-schema snapshot without eagerly allocating a map.
555    pub fn set_original_compact_row(&self, row: teaql_core::CompactRow) {
556        self.inner
557            .lock()
558            .unwrap_or_else(|e| e.into_inner())
559            .original_snapshot = Some(OriginalSnapshot::Compact(row));
560    }
561
562    /// Retrieve the original loaded entity snapshot.
563    pub fn original_snapshot(&self) -> Option<EntitySnapshot> {
564        self.inner
565            .lock()
566            .unwrap_or_else(|e| e.into_inner())
567            .original_snapshot
568            .as_ref()
569            .map(|snapshot| match snapshot {
570                OriginalSnapshot::Materialized(snapshot) => snapshot.clone(),
571                OriginalSnapshot::Compact(row) => EntitySnapshot::from(row.clone().into_map()),
572            })
573    }
574
575    /// Mark an entity as deleted. The next `save()` call will treat this entity
576    /// as a Remove operation in the graph save pipeline.
577    /// Any pending field changes for this entity are cleared — they are irrelevant
578    /// when the entity is being deleted.
579    pub fn mark_as_delete(&self, key: EntityKey) {
580        let mut context = self.inner.lock().unwrap_or_else(|e| e.into_inner());
581        context.change_sets.clear_entity(&key);
582        context.deleted_keys.insert(key);
583    }
584
585    /// Check whether an entity has been marked for deletion.
586    pub fn is_marked_as_delete(&self, key: &EntityKey) -> bool {
587        self.inner
588            .lock()
589            .unwrap_or_else(|e| e.into_inner())
590            .deleted_keys
591            .contains(key)
592    }
593
594    /// Get the set of field names that have been modified for the given entity key.
595    /// This is the Rust equivalent of Java's `entity.getUpdatedProperties()`.
596    pub fn changed_field_names(&self, key: &EntityKey) -> BTreeSet<String> {
597        self.inner
598            .lock()
599            .unwrap_or_else(|e| e.into_inner())
600            .change_sets
601            .changed_field_names(key)
602    }
603    pub fn deleted_keys(&self) -> std::collections::BTreeSet<EntityKey> {
604        self.inner
605            .lock()
606            .unwrap_or_else(|e| e.into_inner())
607            .deleted_keys
608            .clone()
609    }
610
611    pub fn new_keys(&self) -> std::collections::BTreeSet<EntityKey> {
612        self.inner
613            .lock()
614            .unwrap_or_else(|e| e.into_inner())
615            .new_keys
616            .clone()
617    }
618
619    pub fn get_original_version(&self, key: &EntityKey) -> Option<i64> {
620        self.inner
621            .lock()
622            .unwrap_or_else(|e| e.into_inner())
623            .original_versions
624            .get(key)
625    }
626
627    pub fn get_trace_chain(&self, key: &EntityKey) -> Vec<teaql_core::TraceNode> {
628        self.inner
629            .lock()
630            .unwrap_or_else(|e| e.into_inner())
631            .trace_chains
632            .get(key)
633            .cloned()
634            .unwrap_or_default()
635    }
636
637    pub fn set_original_version(&self, key: EntityKey, version: i64) {
638        self.inner
639            .lock()
640            .unwrap_or_else(|e| e.into_inner())
641            .original_versions
642            .insert(key, version);
643    }
644}
645
646pub trait LedgerEntity: teaql_core::Entity {
647    fn entity_root(&self) -> Option<EntityRoot>;
648}