Skip to main content

teaql_runtime/
graph.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::ops::{Deref, DerefMut};
3use std::sync::Arc;
4
5use teaql_core::{EntitySnapshot, MutationValues, TraceNode, Value};
6
7/// Mutable field state for one entity while checker/fix and graph planning run.
8/// It is deliberately distinct from query rows, mutation commands, and loaded
9/// snapshots.
10#[derive(Debug, Clone, Default, PartialEq)]
11pub struct EntityValues(BTreeMap<String, Value>);
12
13impl EntityValues {
14    pub fn new() -> Self {
15        Self::default()
16    }
17}
18
19impl Deref for EntityValues {
20    type Target = BTreeMap<String, Value>;
21
22    fn deref(&self) -> &Self::Target {
23        &self.0
24    }
25}
26
27impl DerefMut for EntityValues {
28    fn deref_mut(&mut self) -> &mut Self::Target {
29        &mut self.0
30    }
31}
32
33impl From<BTreeMap<String, Value>> for EntityValues {
34    fn from(values: BTreeMap<String, Value>) -> Self {
35        Self(values)
36    }
37}
38
39impl From<EntityValues> for BTreeMap<String, Value> {
40    fn from(values: EntityValues) -> Self {
41        values.0
42    }
43}
44
45impl From<EntityValues> for MutationValues {
46    fn from(values: EntityValues) -> Self {
47        BTreeMap::from(values).into()
48    }
49}
50
51impl From<MutationValues> for EntityValues {
52    fn from(values: MutationValues) -> Self {
53        let values: BTreeMap<String, Value> = values.into();
54        values.into()
55    }
56}
57
58impl From<teaql_core::CompactRow> for EntityValues {
59    fn from(row: teaql_core::CompactRow) -> Self {
60        row.into_map().into()
61    }
62}
63
64impl IntoIterator for EntityValues {
65    type Item = (String, Value);
66    type IntoIter = std::collections::btree_map::IntoIter<String, Value>;
67
68    fn into_iter(self) -> Self::IntoIter {
69        self.0.into_iter()
70    }
71}
72
73impl<'a> IntoIterator for &'a EntityValues {
74    type Item = (&'a String, &'a Value);
75    type IntoIter = std::collections::btree_map::Iter<'a, String, Value>;
76
77    fn into_iter(self) -> Self::IntoIter {
78        self.0.iter()
79    }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum GraphOperation {
84    Upsert,
85    Create,
86    Reference,
87    Remove,
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
91pub enum GraphMutationKind {
92    Create,
93    Update,
94    Delete,
95    Reference,
96}
97
98impl GraphMutationKind {
99    pub fn for_update(is_update: bool) -> Self {
100        match is_update {
101            true => Self::Update,
102            false => Self::Create,
103        }
104    }
105}
106
107/// A persistent linked-list token for hierarchical trace context.
108///
109/// Each token holds the trace info for one graph node and an `Arc` pointer
110/// to its parent's token. The full trace chain is only materialized when
111/// explicitly requested via [`recover_trace_chain()`], giving us zero-cost
112/// propagation during the flatten phase.
113#[derive(Debug, Clone, PartialEq)]
114pub struct TraceScopeToken {
115    /// Shared pointer to the parent scope (zero-copy link).
116    pub parent: Option<Arc<TraceScopeToken>>,
117    /// The trace metadata for this scope level.
118    pub track: TraceNode,
119    /// The item_index of the PlanItem that created this scope (for debugging).
120    pub node_index: u64,
121}
122
123impl TraceScopeToken {
124    /// Lazily recover the full trace chain by walking the parent pointers.
125    /// Only called when an event consumer actually needs the chain.
126    pub fn recover_trace_chain(&self) -> Vec<TraceNode> {
127        let mut chain = Vec::new();
128        let mut current: Option<&TraceScopeToken> = Some(self);
129        while let Some(token) = current {
130            if !token.track.comment.is_empty() {
131                chain.push(token.track.clone());
132            }
133            current = token.parent.as_deref();
134        }
135        chain.reverse();
136        chain
137    }
138}
139
140#[derive(Debug, Clone, PartialEq)]
141pub struct GraphMutationPlanItem {
142    pub entity: String,
143    pub kind: GraphMutationKind,
144    pub values: MutationValues,
145    pub update_fields: Vec<String>,
146    /// Monotonically increasing index assigned at push time (for debugging).
147    pub item_index: u64,
148    /// Lazy trace context — only materialized into a Vec<TraceNode> on demand.
149    pub scope_token: Option<Arc<TraceScopeToken>>,
150    pub old_values: Option<EntitySnapshot>,
151}
152
153#[derive(Debug, Clone, PartialEq)]
154pub struct GraphMutationBatch {
155    pub entity: String,
156    pub kind: GraphMutationKind,
157    pub update_fields: Vec<String>,
158    pub items: Vec<GraphMutationPlanItem>,
159}
160
161#[derive(Debug, Clone, PartialEq, Default)]
162pub struct GraphMutationPlan {
163    pub planned_root: Option<GraphNode>,
164    pub items: Vec<GraphMutationPlanItem>,
165    pub batches: Vec<GraphMutationBatch>,
166    /// Auto-incrementing counter for item_index assignment.
167    pub next_item_index: u64,
168    /// Keep track of visited nodes to avoid infinite loops and redundant updates
169    pub visited_nodes: std::collections::HashSet<(String, String)>,
170}
171
172impl GraphMutationPlan {
173    pub fn push(
174        &mut self,
175        entity: impl Into<String>,
176        kind: GraphMutationKind,
177        values: MutationValues,
178        update_fields: Vec<String>,
179        scope_token: Option<Arc<TraceScopeToken>>,
180        old_values: Option<EntitySnapshot>,
181    ) {
182        let index = self.next_item_index;
183        self.next_item_index += 1;
184        self.items.push(GraphMutationPlanItem {
185            entity: entity.into(),
186            kind,
187            values,
188            update_fields,
189            item_index: index,
190            scope_token,
191            old_values,
192        });
193    }
194
195    pub fn rebuild_batches(&mut self) {
196        self.batches.clear();
197        for item in &self.items {
198            let update_fields = match item.kind {
199                GraphMutationKind::Update => item.update_fields.clone(),
200                _ => Vec::new(),
201            };
202            if let Some(batch) = self.batches.last_mut()
203                && batch.entity == item.entity
204                && batch.kind == item.kind
205                && batch.update_fields == update_fields
206            {
207                batch.items.push(item.clone());
208                continue;
209            }
210            self.batches.push(GraphMutationBatch {
211                entity: item.entity.clone(),
212                kind: item.kind,
213                update_fields,
214                items: vec![item.clone()],
215            });
216        }
217    }
218
219    pub fn grouped_counts(&self) -> BTreeMap<(String, GraphMutationKind), usize> {
220        let mut counts = BTreeMap::new();
221        for batch in &self.batches {
222            *counts
223                .entry((batch.entity.clone(), batch.kind))
224                .or_insert(0) += batch.items.len();
225        }
226        counts
227    }
228
229    pub fn batch_count(&self) -> usize {
230        self.batches.len()
231    }
232
233    pub fn len(&self) -> usize {
234        self.items.len()
235    }
236
237    pub fn is_empty(&self) -> bool {
238        self.items.is_empty()
239    }
240}
241
242pub fn sorted_update_fields(
243    values: &EntityValues,
244    excluded: impl IntoIterator<Item = String>,
245) -> Vec<String> {
246    let excluded = excluded.into_iter().collect::<BTreeSet<_>>();
247    values
248        .keys()
249        .filter(|field| !excluded.contains(*field))
250        .cloned()
251        .collect()
252}
253
254#[derive(Debug, Clone, PartialEq)]
255pub struct GraphNode {
256    pub entity: String,
257    pub values: EntityValues,
258    pub relations: BTreeMap<String, Vec<GraphNode>>,
259    pub operation: GraphOperation,
260    /// Annotation comment: carries business intent metadata through graph save.
261    /// Not persisted to the database — used for observability (SQL logs, audit trails).
262    pub comment: Option<String>,
263    /// Fields modified via `update_*()` methods (dirty tracking).
264    /// `None` = all fields (new entity or no tracking available).
265    /// `Some(set)` = only these fields were modified — UPDATE should only include them.
266    /// This is the Rust equivalent of Java's `entity.getUpdatedProperties()`.
267    pub dirty_fields: Option<BTreeSet<String>>,
268    /// L1 Cache snapshot of the entity values exactly as they were loaded from the database.
269    /// Used by the Event Engine to eliminate redundant old_value queries during auditing.
270    pub original_values: Option<EntitySnapshot>,
271}
272
273impl GraphNode {
274    pub fn new(entity: impl Into<String>) -> Self {
275        Self {
276            entity: entity.into(),
277            values: EntityValues::new(),
278            relations: BTreeMap::new(),
279            operation: GraphOperation::Upsert,
280            comment: None,
281            dirty_fields: None,
282            original_values: None,
283        }
284    }
285
286    pub fn operation(mut self, operation: GraphOperation) -> Self {
287        self.operation = operation;
288        self
289    }
290
291    pub fn reference(mut self) -> Self {
292        self.operation = GraphOperation::Reference;
293        self
294    }
295
296    pub fn remove(mut self) -> Self {
297        self.operation = GraphOperation::Remove;
298        self
299    }
300
301    pub fn value(mut self, field: impl Into<String>, value: impl Into<Value>) -> Self {
302        self.values.insert(field.into(), value.into());
303        self
304    }
305
306    pub fn relation(mut self, name: impl Into<String>, node: GraphNode) -> Self {
307        self.relations.entry(name.into()).or_default().push(node);
308        self
309    }
310
311    pub fn relations(
312        mut self,
313        name: impl Into<String>,
314        nodes: impl IntoIterator<Item = GraphNode>,
315    ) -> Self {
316        self.relations.entry(name.into()).or_default().extend(nodes);
317        self
318    }
319
320    pub fn id(&self) -> Option<&Value> {
321        self.values.get("id")
322    }
323
324    /// Set an annotation comment on this graph node.
325    /// The comment propagates through the graph save process for observability.
326    pub fn comment(mut self, comment: impl Into<String>) -> Self {
327        self.comment = Some(comment.into());
328        self
329    }
330
331    /// Set an annotation comment by mutable reference.
332    pub fn set_comment(&mut self, comment: impl Into<String>) {
333        self.comment = Some(comment.into());
334    }
335}
336
337// ---------------------------------------------------------------------------
338// Hierarchical Comment Propagation (Scoped Cons List)
339// ---------------------------------------------------------------------------
340
341/// A stack-allocated scope node forming a parent-pointer cons list.
342///
343/// Each node lives on the call stack of the recursive graph save function.
344/// Child nodes hold a `&'a` reference to their parent's stack frame,
345/// giving us thread-safe, lock-free, zero-overhead hierarchical comment tracking.
346#[derive(Debug)]
347pub struct ScopedCommentNode<'a> {
348    /// Reference to the parent scope (lives on the caller's stack frame)
349    pub parent: Option<&'a ScopedCommentNode<'a>>,
350    pub track: teaql_core::TraceNode,
351}
352
353impl<'a> ScopedCommentNode<'a> {
354    pub fn to_trace_chain(&self) -> Vec<teaql_core::TraceNode> {
355        let mut chain = Vec::new();
356        let mut current: Option<&ScopedCommentNode<'_>> = Some(self);
357
358        while let Some(node) = current {
359            if !node.track.comment.is_empty() {
360                chain.push(node.track.clone());
361            }
362            current = node.parent;
363        }
364
365        chain.reverse();
366        chain
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    #[test]
375    fn test_hierarchical_trace_chain_recovery() {
376        let root_trace = TraceNode {
377            kind: teaql_core::TraceKind::Entity,
378            entity_type: "User".to_string(),
379            entity_id: Some(1),
380            comment: "Create User".to_string(),
381        };
382
383        let child_trace = TraceNode {
384            kind: teaql_core::TraceKind::Entity,
385            entity_type: "Profile".to_string(),
386            entity_id: None,
387            comment: "Create Profile".to_string(),
388        };
389
390        let empty_comment_trace = TraceNode {
391            kind: teaql_core::TraceKind::Entity,
392            entity_type: "AuditLog".to_string(),
393            entity_id: None,
394            comment: "".to_string(),
395        };
396
397        // Test ScopedCommentNode
398        let root_scope = ScopedCommentNode {
399            parent: None,
400            track: root_trace.clone(),
401        };
402        let child_scope = ScopedCommentNode {
403            parent: Some(&root_scope),
404            track: child_trace.clone(),
405        };
406        let empty_scope = ScopedCommentNode {
407            parent: Some(&child_scope),
408            track: empty_comment_trace.clone(),
409        };
410
411        let chain = empty_scope.to_trace_chain();
412        assert_eq!(chain.len(), 2);
413        assert_eq!(chain[0], root_trace);
414        assert_eq!(chain[1], child_trace);
415
416        // Test TraceScopeToken
417        let root_token = Arc::new(TraceScopeToken {
418            parent: None,
419            track: root_trace.clone(),
420            node_index: 0,
421        });
422        let child_token = Arc::new(TraceScopeToken {
423            parent: Some(root_token),
424            track: child_trace.clone(),
425            node_index: 1,
426        });
427        let empty_token = Arc::new(TraceScopeToken {
428            parent: Some(child_token),
429            track: empty_comment_trace,
430            node_index: 2,
431        });
432
433        let chain = empty_token.recover_trace_chain();
434        assert_eq!(chain.len(), 2);
435        assert_eq!(chain[0], root_trace);
436        assert_eq!(chain[1], child_trace);
437    }
438
439    #[test]
440    fn test_graph_mutation_plan_batching_keys_and_counts() {
441        let mut plan = GraphMutationPlan::default();
442
443        // Push 2 creates for User
444        plan.push(
445            "User",
446            GraphMutationKind::Create,
447            MutationValues::new(),
448            vec![],
449            None,
450            None,
451        );
452        plan.push(
453            "User",
454            GraphMutationKind::Create,
455            MutationValues::new(),
456            vec![],
457            None,
458            None,
459        );
460
461        // Push 2 updates for User with same fields
462        plan.push(
463            "User",
464            GraphMutationKind::Update,
465            MutationValues::new(),
466            vec!["name".to_string()],
467            None,
468            None,
469        );
470        plan.push(
471            "User",
472            GraphMutationKind::Update,
473            MutationValues::new(),
474            vec!["name".to_string()],
475            None,
476            None,
477        );
478
479        // Push 1 update for User with different fields (should be separate batch)
480        plan.push(
481            "User",
482            GraphMutationKind::Update,
483            MutationValues::new(),
484            vec!["email".to_string()],
485            None,
486            None,
487        );
488
489        // Push 1 create for Profile
490        plan.push(
491            "Profile",
492            GraphMutationKind::Create,
493            MutationValues::new(),
494            vec![],
495            None,
496            None,
497        );
498
499        assert_eq!(plan.len(), 6);
500
501        // Rebuild batches
502        plan.rebuild_batches();
503
504        // We expect 4 batches:
505        // 1. User Create (2 items)
506        // 2. User Update ["name"] (2 items)
507        // 3. User Update ["email"] (1 item)
508        // 4. Profile Create (1 item)
509        assert_eq!(plan.batch_count(), 4);
510        assert_eq!(plan.batches[0].entity, "User");
511        assert_eq!(plan.batches[0].kind, GraphMutationKind::Create);
512        assert_eq!(plan.batches[1].update_fields, vec!["name"]);
513        assert_eq!(plan.batches[2].update_fields, vec!["email"]);
514        assert_eq!(plan.batches[3].entity, "Profile");
515
516        let counts = plan.grouped_counts();
517        assert_eq!(counts.len(), 3);
518        assert_eq!(
519            counts.get(&("User".to_string(), GraphMutationKind::Create)),
520            Some(&2)
521        );
522        assert_eq!(
523            counts.get(&("User".to_string(), GraphMutationKind::Update)),
524            Some(&3)
525        );
526        assert_eq!(
527            counts.get(&("Profile".to_string(), GraphMutationKind::Create)),
528            Some(&1)
529        );
530    }
531}