Skip to main content

teaql_runtime/
graph.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::sync::Arc;
3
4use teaql_core::{Record, TraceNode, Value};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum GraphOperation {
8    Upsert,
9    Create,
10    Reference,
11    Remove,
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
15pub enum GraphMutationKind {
16    Create,
17    Update,
18    Delete,
19    Reference,
20}
21
22/// A persistent linked-list token for hierarchical trace context.
23///
24/// Each token holds the trace info for one graph node and an `Arc` pointer
25/// to its parent's token. The full trace chain is only materialized when
26/// explicitly requested via [`recover_trace_chain()`], giving us zero-cost
27/// propagation during the flatten phase.
28#[derive(Debug, Clone, PartialEq)]
29pub struct TraceScopeToken {
30    /// Shared pointer to the parent scope (zero-copy link).
31    pub parent: Option<Arc<TraceScopeToken>>,
32    /// The trace metadata for this scope level.
33    pub track: TraceNode,
34    /// The item_index of the PlanItem that created this scope (for debugging).
35    pub node_index: u64,
36}
37
38impl TraceScopeToken {
39    /// Lazily recover the full trace chain by walking the parent pointers.
40    /// Only called when an event consumer actually needs the chain.
41    pub fn recover_trace_chain(&self) -> Vec<TraceNode> {
42        let mut chain = Vec::new();
43        let mut current: Option<&TraceScopeToken> = Some(self);
44        while let Some(token) = current {
45            if !token.track.comment.is_empty() {
46                chain.push(token.track.clone());
47            }
48            current = token.parent.as_deref();
49        }
50        chain.reverse();
51        chain
52    }
53}
54
55#[derive(Debug, Clone, PartialEq)]
56pub struct GraphMutationPlanItem {
57    pub entity: String,
58    pub kind: GraphMutationKind,
59    pub values: Record,
60    pub update_fields: Vec<String>,
61    /// Monotonically increasing index assigned at push time (for debugging).
62    pub item_index: u64,
63    /// Lazy trace context — only materialized into a Vec<TraceNode> on demand.
64    pub scope_token: Option<Arc<TraceScopeToken>>,
65    pub old_values: Option<Record>,
66}
67
68#[derive(Debug, Clone, PartialEq)]
69pub struct GraphMutationBatch {
70    pub entity: String,
71    pub kind: GraphMutationKind,
72    pub update_fields: Vec<String>,
73    pub items: Vec<GraphMutationPlanItem>,
74}
75
76#[derive(Debug, Clone, PartialEq, Default)]
77pub struct GraphMutationPlan {
78    pub planned_root: Option<GraphNode>,
79    pub items: Vec<GraphMutationPlanItem>,
80    pub batches: Vec<GraphMutationBatch>,
81    /// Auto-incrementing counter for item_index assignment.
82    pub next_item_index: u64,
83    /// Keep track of visited nodes to avoid infinite loops and redundant updates
84    pub visited_nodes: std::collections::HashSet<(String, String)>,
85}
86
87impl GraphMutationPlan {
88    pub fn push(
89        &mut self,
90        entity: impl Into<String>,
91        kind: GraphMutationKind,
92        values: Record,
93        update_fields: Vec<String>,
94        scope_token: Option<Arc<TraceScopeToken>>,
95        old_values: Option<Record>,
96    ) {
97        let index = self.next_item_index;
98        self.next_item_index += 1;
99        self.items.push(GraphMutationPlanItem {
100            entity: entity.into(),
101            kind,
102            values,
103            update_fields,
104            item_index: index,
105            scope_token,
106            old_values,
107        });
108    }
109
110    pub fn rebuild_batches(&mut self) {
111        let mut grouped: BTreeMap<
112            (String, GraphMutationKind, Vec<String>),
113            Vec<GraphMutationPlanItem>,
114        > = BTreeMap::new();
115        for item in &self.items {
116            let update_fields = match item.kind {
117                GraphMutationKind::Update => item.update_fields.clone(),
118                _ => Vec::new(),
119            };
120            grouped
121                .entry((item.entity.clone(), item.kind, update_fields))
122                .or_default()
123                .push(item.clone());
124        }
125        self.batches = grouped
126            .into_iter()
127            .map(
128                |((entity, kind, update_fields), items)| GraphMutationBatch {
129                    entity,
130                    kind,
131                    update_fields,
132                    items,
133                },
134            )
135            .collect();
136    }
137
138    pub fn grouped_counts(&self) -> BTreeMap<(String, GraphMutationKind), usize> {
139        let mut counts = BTreeMap::new();
140        for batch in &self.batches {
141            *counts
142                .entry((batch.entity.clone(), batch.kind))
143                .or_insert(0) += batch.items.len();
144        }
145        counts
146    }
147
148    pub fn batch_count(&self) -> usize {
149        self.batches.len()
150    }
151
152    pub fn len(&self) -> usize {
153        self.items.len()
154    }
155
156    pub fn is_empty(&self) -> bool {
157        self.items.is_empty()
158    }
159}
160
161pub fn sorted_update_fields(
162    values: &Record,
163    excluded: impl IntoIterator<Item = String>,
164) -> Vec<String> {
165    let excluded = excluded.into_iter().collect::<BTreeSet<_>>();
166    values
167        .keys()
168        .filter(|field| !excluded.contains(*field))
169        .cloned()
170        .collect()
171}
172
173#[derive(Debug, Clone, PartialEq)]
174pub struct GraphNode {
175    pub entity: String,
176    pub values: Record,
177    pub relations: BTreeMap<String, Vec<GraphNode>>,
178    pub operation: GraphOperation,
179    /// Annotation comment: carries business intent metadata through graph save.
180    /// Not persisted to the database — used for observability (SQL logs, audit trails).
181    pub comment: Option<String>,
182    /// Fields modified via `update_*()` methods (dirty tracking).
183    /// `None` = all fields (new entity or no tracking available).
184    /// `Some(set)` = only these fields were modified — UPDATE should only include them.
185    /// This is the Rust equivalent of Java's `entity.getUpdatedProperties()`.
186    pub dirty_fields: Option<BTreeSet<String>>,
187    /// L1 Cache snapshot of the entity values exactly as they were loaded from the database.
188    /// Used by the Event Engine to eliminate redundant old_value queries during auditing.
189    pub original_values: Option<Record>,
190}
191
192impl GraphNode {
193    pub fn new(entity: impl Into<String>) -> Self {
194        Self {
195            entity: entity.into(),
196            values: Record::new(),
197            relations: BTreeMap::new(),
198            operation: GraphOperation::Upsert,
199            comment: None,
200            dirty_fields: None,
201            original_values: None,
202        }
203    }
204
205    pub fn operation(mut self, operation: GraphOperation) -> Self {
206        self.operation = operation;
207        self
208    }
209
210    pub fn reference(mut self) -> Self {
211        self.operation = GraphOperation::Reference;
212        self
213    }
214
215    pub fn remove(mut self) -> Self {
216        self.operation = GraphOperation::Remove;
217        self
218    }
219
220    pub fn value(mut self, field: impl Into<String>, value: impl Into<Value>) -> Self {
221        self.values.insert(field.into(), value.into());
222        self
223    }
224
225    pub fn relation(mut self, name: impl Into<String>, node: GraphNode) -> Self {
226        self.relations.entry(name.into()).or_default().push(node);
227        self
228    }
229
230    pub fn relations(
231        mut self,
232        name: impl Into<String>,
233        nodes: impl IntoIterator<Item = GraphNode>,
234    ) -> Self {
235        self.relations.entry(name.into()).or_default().extend(nodes);
236        self
237    }
238
239    pub fn id(&self) -> Option<&Value> {
240        self.values.get("id")
241    }
242
243    /// Set an annotation comment on this graph node.
244    /// The comment propagates through the graph save process for observability.
245    pub fn comment(mut self, comment: impl Into<String>) -> Self {
246        self.comment = Some(comment.into());
247        self
248    }
249
250    /// Set an annotation comment by mutable reference.
251    pub fn set_comment(&mut self, comment: impl Into<String>) {
252        self.comment = Some(comment.into());
253    }
254}
255
256// ---------------------------------------------------------------------------
257// Hierarchical Comment Propagation (Scoped Cons List)
258// ---------------------------------------------------------------------------
259
260/// A stack-allocated scope node forming a parent-pointer cons list.
261///
262/// Each node lives on the call stack of the recursive graph save function.
263/// Child nodes hold a `&'a` reference to their parent's stack frame,
264/// giving us thread-safe, lock-free, zero-overhead hierarchical comment tracking.
265#[derive(Debug)]
266pub struct ScopedCommentNode<'a> {
267    /// Reference to the parent scope (lives on the caller's stack frame)
268    pub parent: Option<&'a ScopedCommentNode<'a>>,
269    pub track: teaql_core::TraceNode,
270}
271
272impl<'a> ScopedCommentNode<'a> {
273    pub fn to_trace_chain(&self) -> Vec<teaql_core::TraceNode> {
274        let mut chain = Vec::new();
275        let mut current: Option<&ScopedCommentNode<'_>> = Some(self);
276
277        while let Some(node) = current {
278            if !node.track.comment.is_empty() {
279                chain.push(node.track.clone());
280            }
281            current = node.parent;
282        }
283
284        chain.reverse();
285        chain
286    }
287}