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#[derive(Debug, Clone, PartialEq)]
29pub struct TraceScopeToken {
30 pub parent: Option<Arc<TraceScopeToken>>,
32 pub track: TraceNode,
34 pub node_index: u64,
36}
37
38impl TraceScopeToken {
39 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 pub item_index: u64,
63 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 pub next_item_index: u64,
83 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 pub comment: Option<String>,
182 pub dirty_fields: Option<BTreeSet<String>>,
187 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 pub fn comment(mut self, comment: impl Into<String>) -> Self {
246 self.comment = Some(comment.into());
247 self
248 }
249
250 pub fn set_comment(&mut self, comment: impl Into<String>) {
252 self.comment = Some(comment.into());
253 }
254}
255
256#[derive(Debug)]
266pub struct ScopedCommentNode<'a> {
267 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}