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: 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: 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
351#[derive(Debug)]
352enum OriginalSnapshot {
353    Materialized(EntitySnapshot),
354    Compact(teaql_core::CompactRow),
355}
356
357impl PartialEq for EntityRoot {
358    fn eq(&self, other: &Self) -> bool {
359        Arc::ptr_eq(&self.inner, &other.inner)
360    }
361}
362
363impl EntityRoot {
364    /// Make this root resolve entities from the same flat graph as `source`.
365    /// Existing snapshots and mutation ledger state remain owned by this root.
366    pub fn with_shared_graph(&self, source: &EntityRoot) -> Self {
367        Self {
368            inner: self.inner.clone(),
369            graph: source.graph.clone(),
370        }
371    }
372
373    /// Publish a completely assembled graph. It becomes immutable after this call.
374    pub fn freeze_graph(&self, builder: EntityGraphBuilder) -> Result<(), EntityGraphBuilder> {
375        self.graph
376            .set(builder.freeze())
377            .map_err(|graph| EntityGraphBuilder {
378                tables: graph.tables,
379                relation_lists: graph.relation_lists,
380            })
381    }
382
383    /// Resolve an entity by type and ID without locking or reference cloning.
384    pub fn resolve_entity<T>(&self, id: u64) -> Option<&T>
385    where
386        T: Any + Send + Sync,
387    {
388        self.graph
389            .get()?
390            .tables
391            .get(&TypeId::of::<T>())?
392            .get(&id)?
393            .downcast_ref::<T>()
394    }
395
396    pub fn resolve_relation_list<T>(
397        &self,
398        owner_entity: &str,
399        owner_id: u64,
400        relation: &str,
401    ) -> Option<&SmartList<T>>
402    where
403        T: Any + Send + Sync,
404    {
405        self.graph
406            .get()?
407            .relation_lists
408            .get(&RelationListKey {
409                owner_entity: owner_entity.to_owned(),
410                owner_id,
411                relation: relation.to_owned(),
412            })?
413            .downcast_ref::<SmartList<T>>()
414    }
415
416    pub fn resolve_relation_option<T>(
417        &self,
418        owner_entity: &str,
419        owner_id: u64,
420        relation: &str,
421    ) -> Option<&Option<T>>
422    where
423        T: Any + Send + Sync,
424    {
425        self.graph
426            .get()?
427            .relation_lists
428            .get(&RelationListKey {
429                owner_entity: owner_entity.to_owned(),
430                owner_id,
431                relation: relation.to_owned(),
432            })?
433            .downcast_ref::<Option<T>>()
434    }
435
436    pub fn has_relation_view(&self, owner_entity: &str, owner_id: u64, relation: &str) -> bool {
437        self.graph.get().is_some_and(|graph| {
438            graph.relation_lists.contains_key(&RelationListKey {
439                owner_entity: owner_entity.to_owned(),
440                owner_id,
441                relation: relation.to_owned(),
442            })
443        })
444    }
445
446    pub fn push_change_set(&self) {
447        self.inner
448            .lock()
449            .unwrap_or_else(|e| e.into_inner())
450            .change_sets
451            .push();
452    }
453
454    pub fn pop_change_set(&self) -> Option<EntityChangeSet> {
455        self.inner
456            .lock()
457            .unwrap_or_else(|e| e.into_inner())
458            .change_sets
459            .pop()
460    }
461
462    pub fn clear_current_change_set(&self) {
463        self.inner
464            .lock()
465            .unwrap_or_else(|e| e.into_inner())
466            .change_sets
467            .clear_current();
468    }
469
470    /// Clear all state consumed by a successfully committed ledger save.
471    /// Failed saves must not call this method so their pending intent remains retryable.
472    pub fn clear_committed(&self) {
473        let mut context = self.inner.lock().unwrap_or_else(|e| e.into_inner());
474        context.change_sets = ChangeSetStack::default();
475        context.deleted_keys.clear();
476        context.new_keys.clear();
477        context.original_versions.clear();
478        context.trace_chains.clear();
479        context.original_snapshot = None;
480        context.comment = None;
481        context.is_new = false;
482    }
483
484    pub fn set(&self, key: EntityKey, field: impl Into<String>, value: impl Into<Value>) {
485        self.inner
486            .lock()
487            .unwrap_or_else(|e| e.into_inner())
488            .change_sets
489            .set(key, field, value.into());
490    }
491
492    pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
493        self.inner
494            .lock()
495            .unwrap_or_else(|e| e.into_inner())
496            .change_sets
497            .get(key, field)
498    }
499
500    pub fn current_change_set(&self) -> EntityChangeSet {
501        self.inner
502            .lock()
503            .unwrap_or_else(|e| e.into_inner())
504            .change_sets
505            .current()
506            .cloned()
507            .unwrap_or_default()
508    }
509
510    /// Set an annotation comment on this entity root.
511    /// The comment propagates through the graph save process for observability.
512    pub fn set_comment(&self, comment: impl Into<String>) {
513        self.inner.lock().unwrap_or_else(|e| e.into_inner()).comment = Some(comment.into());
514    }
515
516    /// Get the annotation comment, if any.
517    pub fn get_comment(&self) -> Option<String> {
518        self.inner
519            .lock()
520            .unwrap_or_else(|e| e.into_inner())
521            .comment
522            .clone()
523    }
524
525    /// Mark this entity root as a newly created entity in memory.
526    pub fn mark_as_new(&self, key: EntityKey) {
527        self.inner
528            .lock()
529            .unwrap_or_else(|e| e.into_inner())
530            .new_keys
531            .insert(key);
532    }
533
534    /// Check if this entity root is marked as newly created.
535    pub fn is_new(&self, key: &EntityKey) -> bool {
536        self.inner
537            .lock()
538            .unwrap_or_else(|e| e.into_inner())
539            .new_keys
540            .contains(key)
541    }
542
543    /// Store an original loaded entity snapshot.
544    pub fn set_original_snapshot(&self, snapshot: EntitySnapshot) {
545        self.inner
546            .lock()
547            .unwrap_or_else(|e| e.into_inner())
548            .original_snapshot = Some(OriginalSnapshot::Materialized(snapshot));
549    }
550
551    /// Store a shared-schema snapshot without eagerly allocating a map.
552    pub fn set_original_compact_row(&self, row: teaql_core::CompactRow) {
553        self.inner
554            .lock()
555            .unwrap_or_else(|e| e.into_inner())
556            .original_snapshot = Some(OriginalSnapshot::Compact(row));
557    }
558
559    /// Retrieve the original loaded entity snapshot.
560    pub fn original_snapshot(&self) -> Option<EntitySnapshot> {
561        self.inner
562            .lock()
563            .unwrap_or_else(|e| e.into_inner())
564            .original_snapshot
565            .as_ref()
566            .map(|snapshot| match snapshot {
567                OriginalSnapshot::Materialized(snapshot) => snapshot.clone(),
568                OriginalSnapshot::Compact(row) => EntitySnapshot::from(row.clone().into_map()),
569            })
570    }
571
572    /// Mark an entity as deleted. The next `save()` call will treat this entity
573    /// as a Remove operation in the graph save pipeline.
574    /// Any pending field changes for this entity are cleared — they are irrelevant
575    /// when the entity is being deleted.
576    pub fn mark_as_delete(&self, key: EntityKey) {
577        let mut context = self.inner.lock().unwrap_or_else(|e| e.into_inner());
578        context.change_sets.clear_entity(&key);
579        context.deleted_keys.insert(key);
580    }
581
582    /// Check whether an entity has been marked for deletion.
583    pub fn is_marked_as_delete(&self, key: &EntityKey) -> bool {
584        self.inner
585            .lock()
586            .unwrap_or_else(|e| e.into_inner())
587            .deleted_keys
588            .contains(key)
589    }
590
591    /// Get the set of field names that have been modified for the given entity key.
592    /// This is the Rust equivalent of Java's `entity.getUpdatedProperties()`.
593    pub fn changed_field_names(&self, key: &EntityKey) -> BTreeSet<String> {
594        self.inner
595            .lock()
596            .unwrap_or_else(|e| e.into_inner())
597            .change_sets
598            .changed_field_names(key)
599    }
600    pub fn deleted_keys(&self) -> std::collections::BTreeSet<EntityKey> {
601        self.inner
602            .lock()
603            .unwrap_or_else(|e| e.into_inner())
604            .deleted_keys
605            .clone()
606    }
607
608    pub fn new_keys(&self) -> std::collections::BTreeSet<EntityKey> {
609        self.inner
610            .lock()
611            .unwrap_or_else(|e| e.into_inner())
612            .new_keys
613            .clone()
614    }
615
616    pub fn get_original_version(&self, key: &EntityKey) -> Option<i64> {
617        self.inner
618            .lock()
619            .unwrap_or_else(|e| e.into_inner())
620            .original_versions
621            .get(key)
622    }
623
624    pub fn get_trace_chain(&self, key: &EntityKey) -> Vec<teaql_core::TraceNode> {
625        self.inner
626            .lock()
627            .unwrap_or_else(|e| e.into_inner())
628            .trace_chains
629            .get(key)
630            .cloned()
631            .unwrap_or_default()
632    }
633
634    pub fn set_original_version(&self, key: EntityKey, version: i64) {
635        self.inner
636            .lock()
637            .unwrap_or_else(|e| e.into_inner())
638            .original_versions
639            .insert(key, version);
640    }
641}
642
643pub trait LedgerEntity: teaql_core::Entity {
644    fn entity_root(&self) -> Option<EntityRoot>;
645}