Skip to main content

teaql_runtime/
entity_runtime.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::sync::{Arc, Mutex};
3
4use teaql_core::{Record, Value};
5
6#[derive(Debug, Clone)]
7pub struct EntityKey {
8    pub entity: String,
9    pub id: Value,
10    id_key: String,
11}
12
13impl EntityKey {
14    pub fn new(entity: impl Into<String>, id: impl Into<Value>) -> Self {
15        let id = id.into();
16        Self {
17            entity: entity.into(),
18            id_key: value_key(&id),
19            id,
20        }
21    }
22}
23
24impl PartialEq for EntityKey {
25    fn eq(&self, other: &Self) -> bool {
26        self.entity == other.entity && self.id_key == other.id_key
27    }
28}
29
30impl Eq for EntityKey {}
31
32impl PartialOrd for EntityKey {
33    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
34        Some(self.cmp(other))
35    }
36}
37
38impl Ord for EntityKey {
39    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
40        self.entity
41            .cmp(&other.entity)
42            .then_with(|| self.id_key.cmp(&other.id_key))
43    }
44}
45
46fn value_key(value: &Value) -> String {
47    match value {
48        Value::Null => "null".to_owned(),
49        Value::Bool(value) => format!("bool:{value}"),
50        Value::I64(value) => format!("i64:{value}"),
51        Value::U64(value) => format!("u64:{value}"),
52        Value::F64(value) => format!("f64:{value}"),
53        Value::Decimal(value) => format!("decimal:{value}"),
54        Value::Text(value) => format!("text:{value}"),
55        Value::Json(value) => format!("json:{value}"),
56        Value::Date(value) => format!("date:{value}"),
57        Value::Timestamp(value) => format!("timestamp:{}", value.0),
58        Value::Object(_) => "object".to_owned(),
59        Value::List(_) => "list".to_owned(),
60        Value::TypedNull(_) => "null".to_owned(),
61    }
62}
63
64#[derive(Debug, Clone, Default, PartialEq)]
65pub struct EntityChangeSet {
66    changes: BTreeMap<EntityKey, Record>,
67}
68
69impl EntityChangeSet {
70    pub fn is_empty(&self) -> bool {
71        self.changes.is_empty()
72    }
73
74    pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
75        self.changes
76            .entry(key)
77            .or_default()
78            .insert(field.into(), value);
79    }
80
81    pub fn get(&self, key: &EntityKey, field: &str) -> Option<&Value> {
82        self.changes.get(key).and_then(|changes| changes.get(field))
83    }
84
85    pub fn changes(&self) -> &BTreeMap<EntityKey, Record> {
86        &self.changes
87    }
88
89    /// Remove all pending changes for a specific entity key.
90    pub fn clear_entity(&mut self, key: &EntityKey) {
91        self.changes.remove(key);
92    }
93
94    /// Get the set of field names that have been modified for a given entity key.
95    pub fn field_names(&self, key: &EntityKey) -> BTreeSet<String> {
96        self.changes
97            .get(key)
98            .map(|record| record.keys().cloned().collect())
99            .unwrap_or_default()
100    }
101}
102
103#[derive(Debug, Clone, Default, PartialEq)]
104pub struct ChangeSetStack {
105    stack: Vec<EntityChangeSet>,
106}
107
108impl ChangeSetStack {
109    pub fn current_mut(&mut self) -> &mut EntityChangeSet {
110        if self.stack.is_empty() {
111            self.stack.push(EntityChangeSet::default());
112        }
113        self.stack.last_mut().expect("change set stack has current")
114    }
115
116    pub fn current(&self) -> Option<&EntityChangeSet> {
117        self.stack.last()
118    }
119
120    pub fn push(&mut self) {
121        self.stack.push(EntityChangeSet::default());
122    }
123
124    pub fn pop(&mut self) -> Option<EntityChangeSet> {
125        self.stack.pop()
126    }
127
128    pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
129        self.stack
130            .iter()
131            .rev()
132            .find_map(|change_set| change_set.get(key, field).cloned())
133    }
134
135    pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
136        self.current_mut().set(key, field, value);
137    }
138
139    pub fn clear_current(&mut self) {
140        if let Some(current) = self.stack.last_mut() {
141            *current = EntityChangeSet::default();
142        }
143    }
144
145    /// Remove all pending changes for a specific entity key across all stack levels.
146    pub fn clear_entity(&mut self, key: &EntityKey) {
147        for change_set in &mut self.stack {
148            change_set.clear_entity(key);
149        }
150    }
151
152    /// Get the union of all changed field names for a given entity key across all stack levels.
153    /// This is the Rust equivalent of Java's `entity.getUpdatedProperties()`.
154    pub fn changed_field_names(&self, key: &EntityKey) -> BTreeSet<String> {
155        let mut fields = BTreeSet::new();
156        for change_set in &self.stack {
157            fields.extend(change_set.field_names(key));
158        }
159        fields
160    }
161}
162
163#[derive(Debug, Default)]
164pub struct RootContext {
165    change_sets: ChangeSetStack,
166    /// Annotation comment for observability during graph save.
167    comment: Option<String>,
168    /// Entity keys that have been marked for deletion.
169    /// When the entity is saved, the graph save pipeline will treat these as Remove operations.
170    deleted_keys: std::collections::BTreeSet<EntityKey>,
171    /// Entity keys that have been marked as newly inserted.
172    new_keys: std::collections::BTreeSet<EntityKey>,
173    /// The original loaded snapshot record, used to avoid redundant fetching during save.
174    original_record: Option<Record>,
175    /// Trace chains associated with each entity key.
176    trace_chains: std::collections::BTreeMap<EntityKey, Vec<teaql_core::TraceNode>>,
177    /// Original versions of entities to perform optimistic concurrency control.
178    original_versions: std::collections::BTreeMap<EntityKey, i64>,
179    /// Indicates if this entity root is entirely new.
180    is_new: bool,
181}
182
183#[derive(Debug, Clone, Default)]
184pub struct EntityRoot {
185    inner: Arc<Mutex<RootContext>>,
186}
187
188impl PartialEq for EntityRoot {
189    fn eq(&self, other: &Self) -> bool {
190        Arc::ptr_eq(&self.inner, &other.inner)
191    }
192}
193
194impl EntityRoot {
195    pub fn push_change_set(&self) {
196        self.inner
197            .lock()
198            .unwrap_or_else(|e| e.into_inner())
199            .change_sets
200            .push();
201    }
202
203    pub fn pop_change_set(&self) -> Option<EntityChangeSet> {
204        self.inner
205            .lock()
206            .unwrap_or_else(|e| e.into_inner())
207            .change_sets
208            .pop()
209    }
210
211    pub fn clear_current_change_set(&self) {
212        self.inner
213            .lock()
214            .unwrap_or_else(|e| e.into_inner())
215            .change_sets
216            .clear_current();
217    }
218
219    /// Clear all state consumed by a successfully committed ledger save.
220    /// Failed saves must not call this method so their pending intent remains retryable.
221    pub fn clear_committed(&self) {
222        let mut context = self.inner.lock().unwrap_or_else(|e| e.into_inner());
223        context.change_sets = ChangeSetStack::default();
224        context.deleted_keys.clear();
225        context.new_keys.clear();
226        context.original_versions.clear();
227        context.trace_chains.clear();
228        context.original_record = None;
229        context.comment = None;
230        context.is_new = false;
231    }
232
233    pub fn set(&self, key: EntityKey, field: impl Into<String>, value: impl Into<Value>) {
234        self.inner
235            .lock()
236            .unwrap_or_else(|e| e.into_inner())
237            .change_sets
238            .set(key, field, value.into());
239    }
240
241    pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
242        self.inner
243            .lock()
244            .unwrap_or_else(|e| e.into_inner())
245            .change_sets
246            .get(key, field)
247    }
248
249    pub fn current_change_set(&self) -> EntityChangeSet {
250        self.inner
251            .lock()
252            .unwrap_or_else(|e| e.into_inner())
253            .change_sets
254            .current()
255            .cloned()
256            .unwrap_or_default()
257    }
258
259    /// Set an annotation comment on this entity root.
260    /// The comment propagates through the graph save process for observability.
261    pub fn set_comment(&self, comment: impl Into<String>) {
262        self.inner.lock().unwrap_or_else(|e| e.into_inner()).comment = Some(comment.into());
263    }
264
265    /// Get the annotation comment, if any.
266    pub fn get_comment(&self) -> Option<String> {
267        self.inner
268            .lock()
269            .unwrap_or_else(|e| e.into_inner())
270            .comment
271            .clone()
272    }
273
274    /// Mark this entity root as a newly created entity in memory.
275    pub fn mark_as_new(&self, key: EntityKey) {
276        self.inner
277            .lock()
278            .unwrap_or_else(|e| e.into_inner())
279            .new_keys
280            .insert(key);
281    }
282
283    /// Check if this entity root is marked as newly created.
284    pub fn is_new(&self, key: &EntityKey) -> bool {
285        self.inner
286            .lock()
287            .unwrap_or_else(|e| e.into_inner())
288            .new_keys
289            .contains(key)
290    }
291
292    /// Store the original record when loaded from DB.
293    pub fn set_original_record(&self, record: Record) {
294        self.inner
295            .lock()
296            .unwrap_or_else(|e| e.into_inner())
297            .original_record = Some(record);
298    }
299
300    /// Retrieve the original record.
301    pub fn original_record(&self) -> Option<Record> {
302        self.inner
303            .lock()
304            .unwrap_or_else(|e| e.into_inner())
305            .original_record
306            .clone()
307    }
308
309    /// Mark an entity as deleted. The next `save()` call will treat this entity
310    /// as a Remove operation in the graph save pipeline.
311    /// Any pending field changes for this entity are cleared — they are irrelevant
312    /// when the entity is being deleted.
313    pub fn mark_as_delete(&self, key: EntityKey) {
314        let mut context = self.inner.lock().unwrap_or_else(|e| e.into_inner());
315        context.change_sets.clear_entity(&key);
316        context.deleted_keys.insert(key);
317    }
318
319    /// Check whether an entity has been marked for deletion.
320    pub fn is_marked_as_delete(&self, key: &EntityKey) -> bool {
321        self.inner
322            .lock()
323            .unwrap_or_else(|e| e.into_inner())
324            .deleted_keys
325            .contains(key)
326    }
327
328    /// Get the set of field names that have been modified for the given entity key.
329    /// This is the Rust equivalent of Java's `entity.getUpdatedProperties()`.
330    pub fn changed_field_names(&self, key: &EntityKey) -> BTreeSet<String> {
331        self.inner
332            .lock()
333            .unwrap_or_else(|e| e.into_inner())
334            .change_sets
335            .changed_field_names(key)
336    }
337    pub fn deleted_keys(&self) -> std::collections::BTreeSet<EntityKey> {
338        self.inner
339            .lock()
340            .unwrap_or_else(|e| e.into_inner())
341            .deleted_keys
342            .clone()
343    }
344
345    pub fn new_keys(&self) -> std::collections::BTreeSet<EntityKey> {
346        self.inner
347            .lock()
348            .unwrap_or_else(|e| e.into_inner())
349            .new_keys
350            .clone()
351    }
352
353    pub fn get_original_version(&self, key: &EntityKey) -> Option<i64> {
354        self.inner
355            .lock()
356            .unwrap_or_else(|e| e.into_inner())
357            .original_versions
358            .get(key)
359            .cloned()
360    }
361
362    pub fn get_trace_chain(&self, key: &EntityKey) -> Vec<teaql_core::TraceNode> {
363        self.inner
364            .lock()
365            .unwrap_or_else(|e| e.into_inner())
366            .trace_chains
367            .get(key)
368            .cloned()
369            .unwrap_or_default()
370    }
371
372    pub fn set_original_version(&self, key: EntityKey, version: i64) {
373        self.inner
374            .lock()
375            .unwrap_or_else(|e| e.into_inner())
376            .original_versions
377            .insert(key, version);
378    }
379}
380
381pub trait LedgerEntity: teaql_core::Entity {
382    fn entity_root(&self) -> Option<EntityRoot>;
383}