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.to_rfc3339()),
58        Value::Object(_) => "object".to_owned(),
59        Value::List(_) => "list".to_owned(),
60    }
61}
62
63#[derive(Debug, Clone, Default, PartialEq)]
64pub struct EntityChangeSet {
65    changes: BTreeMap<EntityKey, Record>,
66}
67
68impl EntityChangeSet {
69    pub fn is_empty(&self) -> bool {
70        self.changes.is_empty()
71    }
72
73    pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
74        self.changes
75            .entry(key)
76            .or_default()
77            .insert(field.into(), value);
78    }
79
80    pub fn get(&self, key: &EntityKey, field: &str) -> Option<&Value> {
81        self.changes.get(key).and_then(|changes| changes.get(field))
82    }
83
84    pub fn changes(&self) -> &BTreeMap<EntityKey, Record> {
85        &self.changes
86    }
87
88    /// Remove all pending changes for a specific entity key.
89    pub fn clear_entity(&mut self, key: &EntityKey) {
90        self.changes.remove(key);
91    }
92}
93
94#[derive(Debug, Clone, Default, PartialEq)]
95pub struct ChangeSetStack {
96    stack: Vec<EntityChangeSet>,
97}
98
99impl ChangeSetStack {
100    pub fn current_mut(&mut self) -> &mut EntityChangeSet {
101        if self.stack.is_empty() {
102            self.stack.push(EntityChangeSet::default());
103        }
104        self.stack.last_mut().expect("change set stack has current")
105    }
106
107    pub fn current(&self) -> Option<&EntityChangeSet> {
108        self.stack.last()
109    }
110
111    pub fn push(&mut self) {
112        self.stack.push(EntityChangeSet::default());
113    }
114
115    pub fn pop(&mut self) -> Option<EntityChangeSet> {
116        self.stack.pop()
117    }
118
119    pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
120        self.stack
121            .iter()
122            .rev()
123            .find_map(|change_set| change_set.get(key, field).cloned())
124    }
125
126    pub fn set(&mut self, key: EntityKey, field: impl Into<String>, value: Value) {
127        self.current_mut().set(key, field, value);
128    }
129
130    pub fn clear_current(&mut self) {
131        if let Some(current) = self.stack.last_mut() {
132            *current = EntityChangeSet::default();
133        }
134    }
135
136    /// Remove all pending changes for a specific entity key across all stack levels.
137    pub fn clear_entity(&mut self, key: &EntityKey) {
138        for change_set in &mut self.stack {
139            change_set.clear_entity(key);
140        }
141    }
142}
143
144#[derive(Debug, Default)]
145pub struct RootContext {
146    change_sets: ChangeSetStack,
147    /// Annotation comment for observability during graph save.
148    comment: Option<String>,
149    /// Entity keys that have been marked for deletion.
150    /// When the entity is saved, the graph save pipeline will treat these as Remove operations.
151    deleted_keys: BTreeSet<EntityKey>,
152}
153
154#[derive(Debug, Clone, Default)]
155pub struct EntityRoot {
156    inner: Arc<Mutex<RootContext>>,
157}
158
159impl PartialEq for EntityRoot {
160    fn eq(&self, other: &Self) -> bool {
161        Arc::ptr_eq(&self.inner, &other.inner)
162    }
163}
164
165impl EntityRoot {
166    pub fn push_change_set(&self) {
167        self.inner
168            .lock()
169            .expect("entity root mutex")
170            .change_sets
171            .push();
172    }
173
174    pub fn pop_change_set(&self) -> Option<EntityChangeSet> {
175        self.inner
176            .lock()
177            .expect("entity root mutex")
178            .change_sets
179            .pop()
180    }
181
182    pub fn clear_current_change_set(&self) {
183        self.inner
184            .lock()
185            .expect("entity root mutex")
186            .change_sets
187            .clear_current();
188    }
189
190    pub fn set(&self, key: EntityKey, field: impl Into<String>, value: impl Into<Value>) {
191        self.inner
192            .lock()
193            .expect("entity root mutex")
194            .change_sets
195            .set(key, field, value.into());
196    }
197
198    pub fn get(&self, key: &EntityKey, field: &str) -> Option<Value> {
199        self.inner
200            .lock()
201            .expect("entity root mutex")
202            .change_sets
203            .get(key, field)
204    }
205
206    pub fn current_change_set(&self) -> EntityChangeSet {
207        self.inner
208            .lock()
209            .expect("entity root mutex")
210            .change_sets
211            .current()
212            .cloned()
213            .unwrap_or_default()
214    }
215
216    /// Set an annotation comment on this entity root.
217    /// The comment propagates through the graph save process for observability.
218    pub fn set_comment(&self, comment: impl Into<String>) {
219        self.inner
220            .lock()
221            .expect("entity root mutex")
222            .comment = Some(comment.into());
223    }
224
225    /// Get the annotation comment, if any.
226    pub fn get_comment(&self) -> Option<String> {
227        self.inner
228            .lock()
229            .expect("entity root mutex")
230            .comment
231            .clone()
232    }
233
234    /// Mark an entity as deleted. The next `save()` call will treat this entity
235    /// as a Remove operation in the graph save pipeline.
236    /// Any pending field changes for this entity are cleared — they are irrelevant
237    /// when the entity is being deleted.
238    pub fn mark_as_delete(&self, key: EntityKey) {
239        let mut ctx = self.inner.lock().expect("entity root mutex");
240        ctx.change_sets.clear_entity(&key);
241        ctx.deleted_keys.insert(key);
242    }
243
244    /// Check whether an entity has been marked for deletion.
245    pub fn is_marked_as_delete(&self, key: &EntityKey) -> bool {
246        self.inner
247            .lock()
248            .expect("entity root mutex")
249            .deleted_keys
250            .contains(key)
251    }
252}