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}
84
85impl GraphMutationPlan {
86 pub fn push(
87 &mut self,
88 entity: impl Into<String>,
89 kind: GraphMutationKind,
90 values: Record,
91 update_fields: Vec<String>,
92 scope_token: Option<Arc<TraceScopeToken>>,
93 old_values: Option<Record>,
94 ) {
95 let index = self.next_item_index;
96 self.next_item_index += 1;
97 self.items.push(GraphMutationPlanItem {
98 entity: entity.into(),
99 kind,
100 values,
101 update_fields,
102 item_index: index,
103 scope_token,
104 old_values,
105 });
106 }
107
108 pub fn rebuild_batches(&mut self) {
109 let mut grouped: BTreeMap<
110 (String, GraphMutationKind, Vec<String>),
111 Vec<GraphMutationPlanItem>,
112 > = BTreeMap::new();
113 for item in &self.items {
114 let update_fields = match item.kind {
115 GraphMutationKind::Update => item.update_fields.clone(),
116 _ => Vec::new(),
117 };
118 grouped
119 .entry((item.entity.clone(), item.kind, update_fields))
120 .or_default()
121 .push(item.clone());
122 }
123 self.batches = grouped
124 .into_iter()
125 .map(
126 |((entity, kind, update_fields), items)| GraphMutationBatch {
127 entity,
128 kind,
129 update_fields,
130 items,
131 },
132 )
133 .collect();
134 }
135
136 pub fn grouped_counts(&self) -> BTreeMap<(String, GraphMutationKind), usize> {
137 let mut counts = BTreeMap::new();
138 for batch in &self.batches {
139 *counts
140 .entry((batch.entity.clone(), batch.kind))
141 .or_insert(0) += batch.items.len();
142 }
143 counts
144 }
145
146 pub fn batch_count(&self) -> usize {
147 self.batches.len()
148 }
149
150 pub fn len(&self) -> usize {
151 self.items.len()
152 }
153
154 pub fn is_empty(&self) -> bool {
155 self.items.is_empty()
156 }
157}
158
159pub fn sorted_update_fields(
160 values: &Record,
161 excluded: impl IntoIterator<Item = String>,
162) -> Vec<String> {
163 let excluded = excluded.into_iter().collect::<BTreeSet<_>>();
164 values
165 .keys()
166 .filter(|field| !excluded.contains(*field))
167 .cloned()
168 .collect()
169}
170
171#[derive(Debug, Clone, PartialEq)]
172pub struct GraphNode {
173 pub entity: String,
174 pub values: Record,
175 pub relations: BTreeMap<String, Vec<GraphNode>>,
176 pub operation: GraphOperation,
177 pub comment: Option<String>,
180 pub dirty_fields: Option<BTreeSet<String>>,
185 pub original_values: Option<Record>,
188}
189
190impl GraphNode {
191 pub fn new(entity: impl Into<String>) -> Self {
192 Self {
193 entity: entity.into(),
194 values: Record::new(),
195 relations: BTreeMap::new(),
196 operation: GraphOperation::Upsert,
197 comment: None,
198 dirty_fields: None,
199 original_values: None,
200 }
201 }
202
203 pub fn operation(mut self, operation: GraphOperation) -> Self {
204 self.operation = operation;
205 self
206 }
207
208 pub fn reference(mut self) -> Self {
209 self.operation = GraphOperation::Reference;
210 self
211 }
212
213 pub fn remove(mut self) -> Self {
214 self.operation = GraphOperation::Remove;
215 self
216 }
217
218 pub fn value(mut self, field: impl Into<String>, value: impl Into<Value>) -> Self {
219 self.values.insert(field.into(), value.into());
220 self
221 }
222
223 pub fn relation(mut self, name: impl Into<String>, node: GraphNode) -> Self {
224 self.relations.entry(name.into()).or_default().push(node);
225 self
226 }
227
228 pub fn relations(
229 mut self,
230 name: impl Into<String>,
231 nodes: impl IntoIterator<Item = GraphNode>,
232 ) -> Self {
233 self.relations.entry(name.into()).or_default().extend(nodes);
234 self
235 }
236
237 pub fn id(&self) -> Option<&Value> {
238 self.values.get("id")
239 }
240
241 pub fn comment(mut self, comment: impl Into<String>) -> Self {
244 self.comment = Some(comment.into());
245 self
246 }
247
248 pub fn set_comment(&mut self, comment: impl Into<String>) {
250 self.comment = Some(comment.into());
251 }
252}
253
254#[derive(Debug)]
264pub struct ScopedCommentNode<'a> {
265 pub parent: Option<&'a ScopedCommentNode<'a>>,
267 pub track: teaql_core::TraceNode,
268}
269
270impl<'a> ScopedCommentNode<'a> {
271 pub fn to_trace_chain(&self) -> Vec<teaql_core::TraceNode> {
272 let mut chain = Vec::new();
273 let mut current: Option<&ScopedCommentNode<'_>> = Some(self);
274
275 while let Some(node) = current {
276 if !node.track.comment.is_empty() {
277 chain.push(node.track.clone());
278 }
279 current = node.parent;
280 }
281
282 chain.reverse();
283 chain
284 }
285}