Skip to main content

teaql_runtime/
graph.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use teaql_core::{Record, Value};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum GraphOperation {
7    Upsert,
8    Create,
9    Reference,
10    Remove,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
14pub enum GraphMutationKind {
15    Create,
16    Update,
17    Delete,
18    Reference,
19}
20
21#[derive(Debug, Clone, PartialEq)]
22pub struct GraphMutationPlanItem {
23    pub entity: String,
24    pub kind: GraphMutationKind,
25    pub values: Record,
26    pub update_fields: Vec<String>,
27}
28
29#[derive(Debug, Clone, PartialEq)]
30pub struct GraphMutationBatch {
31    pub entity: String,
32    pub kind: GraphMutationKind,
33    pub update_fields: Vec<String>,
34    pub items: Vec<GraphMutationPlanItem>,
35}
36
37#[derive(Debug, Clone, PartialEq, Default)]
38pub struct GraphMutationPlan {
39    pub planned_root: Option<GraphNode>,
40    pub items: Vec<GraphMutationPlanItem>,
41    pub batches: Vec<GraphMutationBatch>,
42}
43
44impl GraphMutationPlan {
45    pub fn push(
46        &mut self,
47        entity: impl Into<String>,
48        kind: GraphMutationKind,
49        values: Record,
50        update_fields: Vec<String>,
51    ) {
52        self.items.push(GraphMutationPlanItem {
53            entity: entity.into(),
54            kind,
55            values,
56            update_fields,
57        });
58    }
59
60    pub fn rebuild_batches(&mut self) {
61        let mut grouped: BTreeMap<
62            (String, GraphMutationKind, Vec<String>),
63            Vec<GraphMutationPlanItem>,
64        > = BTreeMap::new();
65        for item in &self.items {
66            let update_fields = match item.kind {
67                GraphMutationKind::Update => item.update_fields.clone(),
68                _ => Vec::new(),
69            };
70            grouped
71                .entry((item.entity.clone(), item.kind, update_fields))
72                .or_default()
73                .push(item.clone());
74        }
75        self.batches = grouped
76            .into_iter()
77            .map(
78                |((entity, kind, update_fields), items)| GraphMutationBatch {
79                    entity,
80                    kind,
81                    update_fields,
82                    items,
83                },
84            )
85            .collect();
86    }
87
88    pub fn grouped_counts(&self) -> BTreeMap<(String, GraphMutationKind), usize> {
89        let mut counts = BTreeMap::new();
90        for batch in &self.batches {
91            *counts
92                .entry((batch.entity.clone(), batch.kind))
93                .or_insert(0) += batch.items.len();
94        }
95        counts
96    }
97
98    pub fn batch_count(&self) -> usize {
99        self.batches.len()
100    }
101
102    pub fn len(&self) -> usize {
103        self.items.len()
104    }
105
106    pub fn is_empty(&self) -> bool {
107        self.items.is_empty()
108    }
109}
110
111pub fn sorted_update_fields(
112    values: &Record,
113    excluded: impl IntoIterator<Item = String>,
114) -> Vec<String> {
115    let excluded = excluded.into_iter().collect::<BTreeSet<_>>();
116    values
117        .keys()
118        .filter(|field| !excluded.contains(*field))
119        .cloned()
120        .collect()
121}
122
123#[derive(Debug, Clone, PartialEq)]
124pub struct GraphNode {
125    pub entity: String,
126    pub values: Record,
127    pub relations: BTreeMap<String, Vec<GraphNode>>,
128    pub operation: GraphOperation,
129    /// Annotation comment: carries business intent metadata through graph save.
130    /// Not persisted to the database — used for observability (SQL logs, audit trails).
131    pub comment: Option<String>,
132}
133
134impl GraphNode {
135    pub fn new(entity: impl Into<String>) -> Self {
136        Self {
137            entity: entity.into(),
138            values: Record::new(),
139            relations: BTreeMap::new(),
140            operation: GraphOperation::Upsert,
141            comment: None,
142        }
143    }
144
145    pub fn operation(mut self, operation: GraphOperation) -> Self {
146        self.operation = operation;
147        self
148    }
149
150    pub fn reference(mut self) -> Self {
151        self.operation = GraphOperation::Reference;
152        self
153    }
154
155    pub fn remove(mut self) -> Self {
156        self.operation = GraphOperation::Remove;
157        self
158    }
159
160    pub fn value(mut self, field: impl Into<String>, value: impl Into<Value>) -> Self {
161        self.values.insert(field.into(), value.into());
162        self
163    }
164
165    pub fn relation(mut self, name: impl Into<String>, node: GraphNode) -> Self {
166        self.relations.entry(name.into()).or_default().push(node);
167        self
168    }
169
170    pub fn relations(
171        mut self,
172        name: impl Into<String>,
173        nodes: impl IntoIterator<Item = GraphNode>,
174    ) -> Self {
175        self.relations.entry(name.into()).or_default().extend(nodes);
176        self
177    }
178
179    pub fn id(&self) -> Option<&Value> {
180        self.values.get("id")
181    }
182
183    /// Set an annotation comment on this graph node.
184    /// The comment propagates through the graph save process for observability.
185    pub fn comment(mut self, comment: impl Into<String>) -> Self {
186        self.comment = Some(comment.into());
187        self
188    }
189
190    /// Set an annotation comment by mutable reference.
191    pub fn set_comment(&mut self, comment: impl Into<String>) {
192        self.comment = Some(comment.into());
193    }
194}
195
196// ---------------------------------------------------------------------------
197// Hierarchical Comment Propagation (Scoped Cons List)
198// ---------------------------------------------------------------------------
199
200/// Structured metadata attached to each scope node in the comment propagation chain.
201#[derive(Debug, Clone)]
202pub struct CommentTrack {
203    /// Entity type name (e.g. "Task")
204    pub entity_type: String,
205    /// Entity primary key (e.g. "23"), may be "(pending)" before INSERT
206    pub entity_id: String,
207    /// Business intent annotation (e.g. "Create lift #3")
208    pub comment: String,
209}
210
211/// A stack-allocated scope node forming a parent-pointer cons list.
212///
213/// Each node lives on the call stack of the recursive graph save function.
214/// Child nodes hold a `&'a` reference to their parent's stack frame,
215/// giving us thread-safe, lock-free, zero-overhead hierarchical comment tracking.
216#[derive(Debug)]
217pub struct ScopedCommentNode<'a> {
218    /// Reference to the parent scope (lives on the caller's stack frame)
219    pub parent: Option<&'a ScopedCommentNode<'a>>,
220    /// Structured metadata for this scope level
221    pub track: CommentTrack,
222}
223
224impl<'a> ScopedCommentNode<'a> {
225    /// Walk up the parent-pointer chain and format the full lineage string.
226    /// Output: `"Task(2): Create task '2' -> TaskExecutionLog(3): CREATED"`
227    pub fn to_lineage_string(&self) -> String {
228        let mut chain = Vec::new();
229        let mut current: Option<&ScopedCommentNode<'_>> = Some(self);
230
231        while let Some(node) = current {
232            if !node.track.comment.is_empty() {
233                chain.push(format!(
234                    "{}({}): {}",
235                    node.track.entity_type, node.track.entity_id, node.track.comment
236                ));
237            }
238            current = node.parent;
239        }
240
241        chain.reverse();
242        chain.join(" -> ")
243    }
244}