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 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 pub comment: Option<String>,
263 pub dirty_fields: Option<BTreeSet<String>>,
268 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 pub fn comment(mut self, comment: impl Into<String>) -> Self {
327 self.comment = Some(comment.into());
328 self
329 }
330
331 pub fn set_comment(&mut self, comment: impl Into<String>) {
333 self.comment = Some(comment.into());
334 }
335}
336
337#[derive(Debug)]
347pub struct ScopedCommentNode<'a> {
348 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 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 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 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 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 plan.push(
481 "User",
482 GraphMutationKind::Update,
483 MutationValues::new(),
484 vec!["email".to_string()],
485 None,
486 None,
487 );
488
489 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 plan.rebuild_batches();
503
504 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}