1use std::collections::{BTreeMap, BTreeSet};
2use std::ops::{Deref, DerefMut};
3use std::sync::Arc;
4
5use teaql_core::{EntitySnapshot, MutationValues, TraceNode, Value};
6
7#[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#[derive(Debug, Clone, PartialEq)]
114pub struct TraceScopeToken {
115 pub parent: Option<Arc<TraceScopeToken>>,
117 pub track: TraceNode,
119 pub node_index: u64,
121}
122
123impl TraceScopeToken {
124 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 pub item_index: u64,
148 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 pub next_item_index: u64,
168 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 let mut grouped: BTreeMap<
197 (String, GraphMutationKind, Vec<String>),
198 Vec<GraphMutationPlanItem>,
199 > = BTreeMap::new();
200 for item in &self.items {
201 let update_fields = match item.kind {
202 GraphMutationKind::Update => item.update_fields.clone(),
203 _ => Vec::new(),
204 };
205 grouped
206 .entry((item.entity.clone(), item.kind, update_fields))
207 .or_default()
208 .push(item.clone());
209 }
210 self.batches = grouped
211 .into_iter()
212 .map(
213 |((entity, kind, update_fields), items)| GraphMutationBatch {
214 entity,
215 kind,
216 update_fields,
217 items,
218 },
219 )
220 .collect();
221 }
222
223 pub fn grouped_counts(&self) -> BTreeMap<(String, GraphMutationKind), usize> {
224 let mut counts = BTreeMap::new();
225 for batch in &self.batches {
226 *counts
227 .entry((batch.entity.clone(), batch.kind))
228 .or_insert(0) += batch.items.len();
229 }
230 counts
231 }
232
233 pub fn batch_count(&self) -> usize {
234 self.batches.len()
235 }
236
237 pub fn len(&self) -> usize {
238 self.items.len()
239 }
240
241 pub fn is_empty(&self) -> bool {
242 self.items.is_empty()
243 }
244}
245
246pub fn sorted_update_fields(
247 values: &EntityValues,
248 excluded: impl IntoIterator<Item = String>,
249) -> Vec<String> {
250 let excluded = excluded.into_iter().collect::<BTreeSet<_>>();
251 values
252 .keys()
253 .filter(|field| !excluded.contains(*field))
254 .cloned()
255 .collect()
256}
257
258#[derive(Debug, Clone, PartialEq)]
259pub struct GraphNode {
260 pub entity: String,
261 pub values: EntityValues,
262 pub relations: BTreeMap<String, Vec<GraphNode>>,
263 pub operation: GraphOperation,
264 pub comment: Option<String>,
267 pub dirty_fields: Option<BTreeSet<String>>,
272 pub original_values: Option<EntitySnapshot>,
275}
276
277impl GraphNode {
278 pub fn new(entity: impl Into<String>) -> Self {
279 Self {
280 entity: entity.into(),
281 values: EntityValues::new(),
282 relations: BTreeMap::new(),
283 operation: GraphOperation::Upsert,
284 comment: None,
285 dirty_fields: None,
286 original_values: None,
287 }
288 }
289
290 pub fn operation(mut self, operation: GraphOperation) -> Self {
291 self.operation = operation;
292 self
293 }
294
295 pub fn reference(mut self) -> Self {
296 self.operation = GraphOperation::Reference;
297 self
298 }
299
300 pub fn remove(mut self) -> Self {
301 self.operation = GraphOperation::Remove;
302 self
303 }
304
305 pub fn value(mut self, field: impl Into<String>, value: impl Into<Value>) -> Self {
306 self.values.insert(field.into(), value.into());
307 self
308 }
309
310 pub fn relation(mut self, name: impl Into<String>, node: GraphNode) -> Self {
311 self.relations.entry(name.into()).or_default().push(node);
312 self
313 }
314
315 pub fn relations(
316 mut self,
317 name: impl Into<String>,
318 nodes: impl IntoIterator<Item = GraphNode>,
319 ) -> Self {
320 self.relations.entry(name.into()).or_default().extend(nodes);
321 self
322 }
323
324 pub fn id(&self) -> Option<&Value> {
325 self.values.get("id")
326 }
327
328 pub fn comment(mut self, comment: impl Into<String>) -> Self {
331 self.comment = Some(comment.into());
332 self
333 }
334
335 pub fn set_comment(&mut self, comment: impl Into<String>) {
337 self.comment = Some(comment.into());
338 }
339}
340
341#[derive(Debug)]
351pub struct ScopedCommentNode<'a> {
352 pub parent: Option<&'a ScopedCommentNode<'a>>,
354 pub track: teaql_core::TraceNode,
355}
356
357impl<'a> ScopedCommentNode<'a> {
358 pub fn to_trace_chain(&self) -> Vec<teaql_core::TraceNode> {
359 let mut chain = Vec::new();
360 let mut current: Option<&ScopedCommentNode<'_>> = Some(self);
361
362 while let Some(node) = current {
363 if !node.track.comment.is_empty() {
364 chain.push(node.track.clone());
365 }
366 current = node.parent;
367 }
368
369 chain.reverse();
370 chain
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 #[test]
379 fn test_hierarchical_trace_chain_recovery() {
380 let root_trace = TraceNode {
381 kind: teaql_core::TraceKind::Entity,
382 entity_type: "User".to_string(),
383 entity_id: Some(1),
384 comment: "Create User".to_string(),
385 };
386
387 let child_trace = TraceNode {
388 kind: teaql_core::TraceKind::Entity,
389 entity_type: "Profile".to_string(),
390 entity_id: None,
391 comment: "Create Profile".to_string(),
392 };
393
394 let empty_comment_trace = TraceNode {
395 kind: teaql_core::TraceKind::Entity,
396 entity_type: "AuditLog".to_string(),
397 entity_id: None,
398 comment: "".to_string(),
399 };
400
401 let root_scope = ScopedCommentNode {
403 parent: None,
404 track: root_trace.clone(),
405 };
406 let child_scope = ScopedCommentNode {
407 parent: Some(&root_scope),
408 track: child_trace.clone(),
409 };
410 let empty_scope = ScopedCommentNode {
411 parent: Some(&child_scope),
412 track: empty_comment_trace.clone(),
413 };
414
415 let chain = empty_scope.to_trace_chain();
416 assert_eq!(chain.len(), 2);
417 assert_eq!(chain[0], root_trace);
418 assert_eq!(chain[1], child_trace);
419
420 let root_token = Arc::new(TraceScopeToken {
422 parent: None,
423 track: root_trace.clone(),
424 node_index: 0,
425 });
426 let child_token = Arc::new(TraceScopeToken {
427 parent: Some(root_token),
428 track: child_trace.clone(),
429 node_index: 1,
430 });
431 let empty_token = Arc::new(TraceScopeToken {
432 parent: Some(child_token),
433 track: empty_comment_trace,
434 node_index: 2,
435 });
436
437 let chain = empty_token.recover_trace_chain();
438 assert_eq!(chain.len(), 2);
439 assert_eq!(chain[0], root_trace);
440 assert_eq!(chain[1], child_trace);
441 }
442
443 #[test]
444 fn test_graph_mutation_plan_batching_keys_and_counts() {
445 let mut plan = GraphMutationPlan::default();
446
447 plan.push(
449 "User",
450 GraphMutationKind::Create,
451 MutationValues::new(),
452 vec![],
453 None,
454 None,
455 );
456 plan.push(
457 "User",
458 GraphMutationKind::Create,
459 MutationValues::new(),
460 vec![],
461 None,
462 None,
463 );
464
465 plan.push(
467 "User",
468 GraphMutationKind::Update,
469 MutationValues::new(),
470 vec!["name".to_string()],
471 None,
472 None,
473 );
474 plan.push(
475 "User",
476 GraphMutationKind::Update,
477 MutationValues::new(),
478 vec!["name".to_string()],
479 None,
480 None,
481 );
482
483 plan.push(
485 "User",
486 GraphMutationKind::Update,
487 MutationValues::new(),
488 vec!["email".to_string()],
489 None,
490 None,
491 );
492
493 plan.push(
495 "Profile",
496 GraphMutationKind::Create,
497 MutationValues::new(),
498 vec![],
499 None,
500 None,
501 );
502
503 assert_eq!(plan.len(), 6);
504
505 plan.rebuild_batches();
507
508 assert_eq!(plan.batch_count(), 4);
514
515 let counts = plan.grouped_counts();
516 assert_eq!(counts.len(), 3);
517 assert_eq!(
518 counts.get(&("User".to_string(), GraphMutationKind::Create)),
519 Some(&2)
520 );
521 assert_eq!(
522 counts.get(&("User".to_string(), GraphMutationKind::Update)),
523 Some(&3)
524 );
525 assert_eq!(
526 counts.get(&("Profile".to_string(), GraphMutationKind::Create)),
527 Some(&1)
528 );
529 }
530}