1use std::collections::BTreeMap;
2
3use teaql_core::{
4 DeleteCommand, Entity, EntityDescriptor, Expr, InsertCommand, PropertyDescriptor, Record,
5 SelectQuery, UpdateCommand, Value,
6};
7use teaql_sql::SqlDialect;
8
9use crate::{
10 GraphMutationKind, GraphMutationPlan, GraphNode, GraphOperation, RepositoryError,
11 RuntimeError, ScopedCommentNode, sorted_update_fields,
12};
13use crate::entity_status::EntityStatus;
14
15use super::{GraphTransactionBoundary, QueryExecutor, ResolvedRepository, helpers::*};
16
17impl<'a, D, E> ResolvedRepository<'a, D, E>
18where
19 D: SqlDialect,
20 E: QueryExecutor,
21{
22 pub fn save_graph(&self, node: GraphNode) -> Result<GraphNode, RepositoryError<E::Error>> {
23 if node.entity != self.entity {
24 return Err(RepositoryError::Runtime(RuntimeError::Graph(format!(
25 "resolved repository {} cannot save graph root {}",
26 self.entity, node.entity
27 ))));
28 }
29 let boundary = self
30 .repository
31 .executor
32 .begin_transaction()
33 .map_err(RepositoryError::Executor)?;
34 if matches!(boundary, GraphTransactionBoundary::Unsupported) {
35 return Err(RepositoryError::Runtime(RuntimeError::Graph(
36 "save_graph requires a transactional executor".to_owned(),
37 )));
38 }
39 let result = self.upsert_graph_node_scoped(node, None);
40 match result {
41 Ok(saved) => {
42 if matches!(boundary, GraphTransactionBoundary::Started) {
43 self.repository
44 .executor
45 .commit_transaction()
46 .map_err(RepositoryError::Executor)?;
47 }
48 Ok(saved)
49 }
50 Err(err) => {
51 if !matches!(boundary, GraphTransactionBoundary::Unsupported) {
52 self.repository
53 .executor
54 .rollback_transaction()
55 .map_err(RepositoryError::Executor)?;
56 }
57 Err(err)
58 }
59 }
60 }
61
62 pub fn save_entity_graph_from(&self, graph: teaql_core::EntityGraph) -> Result<GraphNode, RepositoryError<E::Error>> {
63 fn convert(node: teaql_core::EntityGraphNode) -> GraphNode {
64 let mut relations = BTreeMap::new();
65 for (rel_name, child) in node.children {
66 relations.entry(rel_name).or_insert_with(Vec::new).push(convert(child));
67 }
68 GraphNode {
69 entity: node.entity_type,
70 values: node.record,
71 relations,
72 operation: match node.operation {
73 teaql_core::EntityGraphOperation::Save => crate::GraphOperation::Upsert,
74 teaql_core::EntityGraphOperation::Delete => crate::GraphOperation::Remove,
75 },
76 comment: node.comment,
77 }
78 }
79 self.save_graph(convert(graph.root))
80 }
81
82 pub fn save_entity_graph<T>(&self, entity: T) -> Result<GraphNode, RepositoryError<E::Error>>
83 where
84 T: Entity,
85 {
86 let node = self
87 .graph_node_from_entity(entity)
88 .map_err(RepositoryError::Runtime)?;
89 self.save_graph(node)
90 }
91
92 pub fn save_entity<T>(&self, entity: T, status: EntityStatus) -> Result<GraphNode, RepositoryError<E::Error>>
93 where
94 T: Entity,
95 {
96 if !status.need_persist() {
97 return Ok(GraphNode::new(&self.entity));
98 }
99 if status.is_deleted() {
100 let mut node = self.graph_node_from_entity(entity)
101 .map_err(RepositoryError::Runtime)?;
102 node.operation = GraphOperation::Remove;
103 node.relations.clear();
104 self.save_graph(node)
105 } else {
106 self.save_entity_graph(entity)
107 }
108 }
109 pub fn save_entity_with_comment<T>(&self, entity: T, status: EntityStatus, comment: impl Into<String>) -> Result<GraphNode, RepositoryError<E::Error>>
110 where
111 T: Entity,
112 {
113 if status.is_deleted() {
114 let mut node = self.graph_node_from_entity(entity)
115 .map_err(RepositoryError::Runtime)?;
116 node.operation = GraphOperation::Remove;
117 node.relations.clear();
118 node.set_comment(comment);
119 self.save_graph(node)
120 } else {
121 self.save_entity_graph_with_comment(entity, comment)
122 }
123 }
124 pub fn save_entity_graph_with_comment<T>(
125 &self,
126 entity: T,
127 comment: impl Into<String>,
128 ) -> Result<GraphNode, RepositoryError<E::Error>>
129 where
130 T: Entity,
131 {
132 let mut node = self
133 .graph_node_from_entity(entity)
134 .map_err(RepositoryError::Runtime)?;
135 node.set_comment(comment);
136 self.save_graph(node)
137 }
138
139 pub fn create_entity_graph_with_comment<T>(
143 &self,
144 entity: T,
145 comment: impl Into<String>,
146 ) -> Result<GraphNode, RepositoryError<E::Error>>
147 where
148 T: Entity,
149 {
150 let mut node = self
151 .graph_node_from_entity(entity)
152 .map_err(RepositoryError::Runtime)?;
153 node.operation = GraphOperation::Create;
154 node.set_comment(comment);
155 self.save_graph(node)
156 }
157
158 pub fn plan_graph(
159 &self,
160 node: GraphNode,
161 ) -> Result<GraphMutationPlan, RepositoryError<E::Error>> {
162 if node.entity != self.entity {
163 return Err(RepositoryError::Runtime(RuntimeError::Graph(format!(
164 "resolved repository {} cannot plan graph root {}",
165 self.entity, node.entity
166 ))));
167 }
168 let mut node = node;
169 let mut plan = GraphMutationPlan::default();
170 self.collect_graph_plan(&mut node, &mut plan, None, false)?;
171 plan.planned_root = Some(node);
172 plan.rebuild_batches();
173 Ok(plan)
174 }
175
176 pub fn execute_graph_plan(
177 &self,
178 plan: GraphMutationPlan,
179 ) -> Result<GraphNode, RepositoryError<E::Error>> {
180 let Some(root) = plan.planned_root else {
181 return Err(RepositoryError::Runtime(RuntimeError::Graph(
182 "graph mutation plan has no planned root".to_owned(),
183 )));
184 };
185
186 self.upsert_graph_node_scoped(root, None)
187 }
188
189 pub fn graph_node_from_entity<T>(&self, entity: T) -> Result<GraphNode, RuntimeError>
190 where
191 T: Entity,
192 {
193 let descriptor = T::entity_descriptor();
194 if descriptor.name != self.entity {
195 return Err(RuntimeError::Graph(format!(
196 "resolved repository {} cannot extract graph root {}",
197 self.entity, descriptor.name
198 )));
199 }
200 self.graph_node_from_record(&descriptor.name, entity.into_record())
201 }
202
203 fn collect_graph_plan<'s>(
204 &self,
205 node: &mut GraphNode,
206 plan: &mut GraphMutationPlan,
207 parent_scope: Option<&'s ScopedCommentNode<'s>>,
208 parent_is_create: bool,
209 ) -> Result<(), RepositoryError<E::Error>> {
210 match node.operation {
211 GraphOperation::Reference => {
212 plan.push(
213 node.entity.clone(),
214 GraphMutationKind::Reference,
215 node.values.clone(),
216 Vec::new(),
217 );
218 return Ok(());
219 }
220 GraphOperation::Remove => {
221 plan.push(
222 node.entity.clone(),
223 GraphMutationKind::Delete,
224 node.values.clone(),
225 Vec::new(),
226 );
227 return Ok(());
228 }
229 GraphOperation::Upsert | GraphOperation::Create => {}
230 }
231
232 let descriptor = self
233 .repository
234 .metadata
235 .context
236 .require_entity(&node.entity)
237 .map_err(RepositoryError::Runtime)?;
238
239 let current_scope = node.comment.as_ref().map(|c| ScopedCommentNode {
241 parent: parent_scope,
242 track: teaql_core::TraceNode {
243 entity_type: node.entity.clone(),
244 entity_id: node.id().and_then(|v| match v {
245 Value::U64(n) => Some(*n),
246 Value::I64(n) => Some(*n as u64),
247 _ => None,
248 }),
249 comment: c.clone(),
250 },
251 });
252 let active_scope = current_scope.as_ref().or(parent_scope);
253
254 let id_property = descriptor.id_property().cloned();
255 let id = id_property.as_ref().and_then(|property| {
256 node.values
257 .get(&property.name)
258 .filter(|value| !is_unassigned_id_value(value))
259 .cloned()
260 });
261
262 let is_create_op = node.operation == GraphOperation::Create || (parent_is_create && node.operation == GraphOperation::Upsert);
263
264 let is_update = if is_create_op {
265 false
266 } else {
267 match (id_property.as_ref(), id.as_ref()) {
268 (Some(id_property), Some(id)) => self
269 .fetch_graph_current_row(&node.entity, &id_property.name, id, active_scope.map(|s| s.to_trace_chain()).unwrap_or_default())?
270 .is_some(),
271 _ => false,
272 }
273 };
274 if !is_update {
275 if let Some(id_property) = id_property.as_ref() {
276 let needs_id = !node.values.contains_key(&id_property.name)
277 || node
278 .values
279 .get(&id_property.name)
280 .is_some_and(is_unassigned_id_value);
281 if needs_id {
282 let id = self
283 .repository
284 .metadata
285 .context
286 .next_id(&node.entity)
287 .map_err(RepositoryError::Runtime)?;
288 node.values.insert(id_property.name.clone(), Value::U64(id));
289 }
290 }
291 ensure_initial_version(&mut node.values, descriptor);
292 }
293 let update_fields = if is_update {
294 let mut excluded = Vec::new();
295 if let Some(id_property) = id_property.as_ref() {
296 excluded.push(id_property.name.clone());
297 }
298 if let Some(version_property) = descriptor.version_property() {
299 excluded.push(version_property.name.clone());
300 }
301 sorted_update_fields(&node.values, excluded)
302 } else {
303 Vec::new()
304 };
305 plan.push(
306 node.entity.clone(),
307 if is_update {
308 GraphMutationKind::Update
309 } else {
310 GraphMutationKind::Create
311 },
312 node.values.clone(),
313 update_fields,
314 );
315
316 for (name, children) in &mut node.relations {
317 let relation = descriptor.relation_by_name(name).ok_or_else(|| {
318 RepositoryError::Runtime(RuntimeError::MissingRelation {
319 entity: node.entity.clone(),
320 relation: name.clone(),
321 })
322 })?;
323 let child_repo = self.scoped_repository(relation.target_entity.clone());
324 for child in children {
325 ensure_relation_target(&node.entity, name, &relation.target_entity, child)?;
326 child_repo.collect_graph_plan(child, plan, active_scope, is_create_op)?;
327 }
328 }
329 Ok(())
330 }
331
332 fn insert_graph_node_scoped<'s>(
333 &self,
334 mut node: GraphNode,
335 parent_scope: Option<&'s ScopedCommentNode<'s>>,
336 ) -> Result<GraphNode, RepositoryError<E::Error>> {
337 match node.operation {
338 GraphOperation::Upsert | GraphOperation::Create => {}
339 GraphOperation::Reference => return self.validate_reference_node(node, parent_scope.map(|s| s.to_trace_chain()).unwrap_or_default()),
340 GraphOperation::Remove => {
341 return Err(RepositoryError::Runtime(RuntimeError::Graph(format!(
342 "create graph cannot remove node {}",
343 node.entity
344 ))));
345 }
346 }
347
348 let current_scope = node.comment.as_ref().map(|c| ScopedCommentNode {
350 parent: parent_scope,
351 track: teaql_core::TraceNode {
352 entity_type: node.entity.clone(),
353 entity_id: node
354 .id()
355 .and_then(|v| match v {
356 Value::U64(n) => Some(*n),
357 Value::I64(n) => Some(*n as u64),
358 _ => None,
359 }),
360 comment: c.clone(),
361 },
362 });
363 let active_scope = current_scope.as_ref().or(parent_scope);
364
365 let descriptor = self
366 .repository
367 .metadata
368 .context
369 .require_entity(&node.entity)
370 .map_err(RepositoryError::Runtime)?;
371
372 let mut one_relations = Vec::new();
373 let mut many_relations = Vec::new();
374 for (name, children) in std::mem::take(&mut node.relations) {
375 let relation = descriptor.relation_by_name(&name).ok_or_else(|| {
376 RepositoryError::Runtime(RuntimeError::MissingRelation {
377 entity: node.entity.clone(),
378 relation: name.clone(),
379 })
380 })?;
381 if relation.many {
382 many_relations.push((name, relation.clone(), children));
383 } else {
384 one_relations.push((name, relation.clone(), children));
385 }
386 }
387
388 for (name, relation, children) in one_relations {
389 if children.len() > 1 {
390 return Err(RepositoryError::Runtime(RuntimeError::Graph(format!(
391 "relation {}.{} expects one child, got {}",
392 node.entity,
393 name,
394 children.len()
395 ))));
396 }
397 let mut saved_children = Vec::new();
398 for child in children {
399 ensure_relation_target(&node.entity, &name, &relation.target_entity, &child)?;
400 let child_repo = self.scoped_repository(child.entity.clone());
401 let saved_child = child_repo.insert_graph_node_scoped(child, active_scope)?;
402 if relation.attach {
403 let foreign_value = saved_child
404 .values
405 .get(&relation.foreign_key)
406 .cloned()
407 .ok_or_else(|| {
408 RepositoryError::Runtime(RuntimeError::Graph(format!(
409 "saved child {} missing foreign key {} for relation {}.{}",
410 relation.target_entity, relation.foreign_key, node.entity, name
411 )))
412 })?;
413 node.values
414 .insert(relation.local_key.clone(), foreign_value);
415 }
416 saved_children.push(saved_child);
417 }
418 node.relations.insert(name, saved_children);
419 }
420
421 let command = self
422 .prepare_insert_command(&InsertCommand {
423 entity: node.entity.clone(),
424 values: node.values.clone(),
425 trace_chain: Vec::new(),
426 })
427 .map_err(RepositoryError::Runtime)?;
428 let lineage = active_scope.map(|s| s.to_trace_chain()).unwrap_or_default();
429 self.execute_prepared_insert_with_comment(command.clone(), lineage)?;
430 node.values = command.values;
431
432 for (name, relation, children) in many_relations {
433 let local_value = node
434 .values
435 .get(&relation.local_key)
436 .cloned()
437 .ok_or_else(|| {
438 RepositoryError::Runtime(RuntimeError::Graph(format!(
439 "parent {} missing local key {} for relation {}",
440 node.entity, relation.local_key, name
441 )))
442 })?;
443 let mut saved_children = Vec::new();
444 for mut child in children {
445 ensure_relation_target(&node.entity, &name, &relation.target_entity, &child)?;
446 if relation.attach {
447 child
448 .values
449 .insert(relation.foreign_key.clone(), local_value.clone());
450 }
451 let child_repo = self.scoped_repository(child.entity.clone());
452 saved_children.push(child_repo.insert_graph_node_scoped(child, active_scope)?);
453 }
454 node.relations.insert(name, saved_children);
455 }
456
457 Ok(node)
458 }
459
460 fn upsert_graph_node_scoped<'s>(
461 &self,
462 mut node: GraphNode,
463 parent_scope: Option<&'s ScopedCommentNode<'s>>,
464 ) -> Result<GraphNode, RepositoryError<E::Error>> {
465 let current_scope = node.comment.as_ref().map(|c| ScopedCommentNode {
467 parent: parent_scope,
468 track: teaql_core::TraceNode {
469 entity_type: node.entity.clone(),
470 entity_id: node
471 .id()
472 .and_then(|v| match v {
473 Value::U64(n) => Some(*n),
474 Value::I64(n) => Some(*n as u64),
475 _ => None,
476 }),
477 comment: c.clone(),
478 },
479 });
480 let active_scope = current_scope.as_ref().or(parent_scope);
481
482 match node.operation {
483 GraphOperation::Upsert | GraphOperation::Create => {}
484 GraphOperation::Reference => return self.validate_reference_node(node, active_scope.map(|s| s.to_trace_chain()).unwrap_or_default()),
485 GraphOperation::Remove => {
486 self.validate_remove_node(&node, active_scope.map(|s| s.to_trace_chain()).unwrap_or_default())?;
487 self.delete_graph_node(&node, parent_scope)?;
488 return Ok(node);
489 }
490 }
491
492 let descriptor = self
493 .repository
494 .metadata
495 .context
496 .require_entity(&node.entity)
497 .map_err(RepositoryError::Runtime)?;
498 let Some(id_property) = descriptor.id_property() else {
499 return Err(RepositoryError::Runtime(RuntimeError::Graph(format!(
500 "entity {} has no id property for graph upsert",
501 node.entity
502 ))));
503 };
504 let Some(id) = node
505 .values
506 .get(&id_property.name)
507 .filter(|value| !is_unassigned_id_value(value))
508 .cloned()
509 else {
510 node.comment = None;
512 return self.insert_graph_node_scoped(node, active_scope);
513 };
514
515 if node.operation == GraphOperation::Create || self
516 .fetch_graph_current_row(&node.entity, &id_property.name, &id, active_scope.map(|s| s.to_trace_chain()).unwrap_or_default())?
517 .is_none()
518 {
519 node.comment = None;
520 return self.insert_graph_node_scoped(node, active_scope);
521 }
522
523 let mut one_relations = Vec::new();
524 let mut many_relations = Vec::new();
525 for (name, children) in std::mem::take(&mut node.relations) {
526 let relation = descriptor.relation_by_name(&name).ok_or_else(|| {
527 RepositoryError::Runtime(RuntimeError::MissingRelation {
528 entity: node.entity.clone(),
529 relation: name.clone(),
530 })
531 })?;
532 if relation.many {
533 many_relations.push((name, relation.clone(), children));
534 } else {
535 one_relations.push((name, relation.clone(), children));
536 }
537 }
538
539 for (name, relation, children) in one_relations {
540 if children.len() > 1 {
541 return Err(RepositoryError::Runtime(RuntimeError::Graph(format!(
542 "relation {}.{} expects one child, got {}",
543 node.entity,
544 name,
545 children.len()
546 ))));
547 }
548 let mut saved_children = Vec::new();
549 for child in children {
550 ensure_relation_target(&node.entity, &name, &relation.target_entity, &child)?;
551 let child_repo = self.scoped_repository(child.entity.clone());
552 let saved_child = child_repo.upsert_graph_node_scoped(child, active_scope)?;
553 if relation.attach {
554 let foreign_value = saved_child
555 .values
556 .get(&relation.foreign_key)
557 .cloned()
558 .ok_or_else(|| {
559 RepositoryError::Runtime(RuntimeError::Graph(format!(
560 "saved child {} missing foreign key {} for relation {}.{}",
561 relation.target_entity, relation.foreign_key, node.entity, name
562 )))
563 })?;
564 node.values
565 .insert(relation.local_key.clone(), foreign_value);
566 }
567 saved_children.push(saved_child);
568 }
569 node.relations.insert(name, saved_children);
570 }
571
572 let update = self.graph_update_command(&node, descriptor, id_property, &id);
573 if !update.values.is_empty() || update.expected_version.is_some() {
574 let prepared_update = self
575 .prepare_update_command(&update)
576 .map_err(RepositoryError::Runtime)?;
577 let lineage = active_scope.map(|s| s.to_trace_chain()).unwrap_or_default();
578 self.execute_prepared_update_with_comment(prepared_update.clone(), lineage)?;
579 for (field, value) in &prepared_update.values {
580 node.values.insert(field.clone(), value.clone());
581 }
582 if let Some(version_property) = descriptor.version_property() {
583 if let Some(expected_version) = prepared_update.expected_version {
584 node.values.insert(
585 version_property.name.clone(),
586 Value::I64(expected_version + 1),
587 );
588 }
589 }
590 }
591
592 for (name, relation, children) in many_relations {
593 let local_value = node
594 .values
595 .get(&relation.local_key)
596 .cloned()
597 .ok_or_else(|| {
598 RepositoryError::Runtime(RuntimeError::Graph(format!(
599 "parent {} missing local key {} for relation {}",
600 node.entity, relation.local_key, name
601 )))
602 })?;
603 let child_repo = self.scoped_repository(relation.target_entity.clone());
604 let existing_children = child_repo.fetch_graph_children(
605 &relation.target_entity,
606 &relation.foreign_key,
607 &local_value,
608 active_scope.map(|s| s.to_trace_chain()).unwrap_or_default(),
609 )?;
610 let child_descriptor = self
611 .repository
612 .metadata
613 .context
614 .require_entity(&relation.target_entity)
615 .map_err(RepositoryError::Runtime)?;
616 let child_id_property = child_descriptor.id_property().ok_or_else(|| {
617 RepositoryError::Runtime(RuntimeError::Graph(format!(
618 "entity {} has no id property for graph diff",
619 relation.target_entity
620 )))
621 })?;
622 let mut existing_by_id = BTreeMap::new();
623 for child in existing_children {
624 if let Some(id) = child.get(&child_id_property.name) {
625 existing_by_id.insert(graph_identity_key(id), child);
626 }
627 }
628
629 let mut seen = std::collections::BTreeSet::new();
630 let mut saved_children = Vec::new();
631 for mut child in children {
632 ensure_relation_target(&node.entity, &name, &relation.target_entity, &child)?;
633 if relation.attach && child.operation != GraphOperation::Reference {
634 child
635 .values
636 .insert(relation.foreign_key.clone(), local_value.clone());
637 }
638 if let Some(child_id) = child
639 .values
640 .get(&child_id_property.name)
641 .filter(|value| !is_unassigned_id_value(value))
642 {
643 let key = graph_identity_key(child_id);
644 if !seen.insert(key.clone()) {
645 return Err(RepositoryError::Runtime(RuntimeError::Graph(format!(
646 "duplicate child id {key} in relation {}.{}",
647 node.entity, name
648 ))));
649 }
650 }
651 saved_children.push(child_repo.upsert_graph_node_scoped(child, active_scope)?);
652 }
653
654 if relation.delete_missing {
655 for (id_key, existing) in existing_by_id {
656 if seen.contains(&id_key) {
657 continue;
658 }
659 let Some(existing_id) = existing.get(&child_id_property.name).cloned() else {
660 continue;
661 };
662 let mut delete =
663 DeleteCommand::new(relation.target_entity.clone(), existing_id);
664 if let Some(version) = graph_record_version(&existing, child_descriptor) {
665 delete = delete.expected_version(version);
666 }
667 let lineage = active_scope.map(|s| s.to_trace_chain()).unwrap_or_default();
668 child_repo.delete_scoped(&delete, lineage)?;
669 }
670 }
671
672 node.relations.insert(name, saved_children);
673 }
674
675 Ok(node)
676 }
677
678 fn validate_reference_node(
679 &self,
680 node: GraphNode,
681 trace_chain: Vec<teaql_core::TraceNode>,
682 ) -> Result<GraphNode, RepositoryError<E::Error>> {
683 if !node.relations.is_empty() {
684 return Err(RepositoryError::Runtime(RuntimeError::Graph(format!(
685 "reference node {} cannot contain child relations",
686 node.entity
687 ))));
688 }
689 let descriptor = self
690 .repository
691 .metadata
692 .context
693 .require_entity(&node.entity)
694 .map_err(RepositoryError::Runtime)?;
695 let id_property = descriptor.id_property().ok_or_else(|| {
696 RepositoryError::Runtime(RuntimeError::Graph(format!(
697 "entity {} has no id property for graph reference",
698 node.entity
699 )))
700 })?;
701 let id = node
702 .values
703 .get(&id_property.name)
704 .filter(|value| !is_unassigned_id_value(value))
705 .cloned()
706 .ok_or_else(|| {
707 RepositoryError::Runtime(RuntimeError::Graph(format!(
708 "reference node {} missing id property {}",
709 node.entity, id_property.name
710 )))
711 })?;
712
713 for field in node.values.keys() {
714 if field == &id_property.name {
715 continue;
716 }
717 if descriptor
718 .version_property()
719 .map(|property| field == &property.name)
720 .unwrap_or(false)
721 {
722 continue;
723 }
724 return Err(RepositoryError::Runtime(RuntimeError::Graph(format!(
725 "reference node {} cannot carry mutable field {}",
726 node.entity, field
727 ))));
728 }
729
730 let current = self
731 .fetch_graph_current_row(&node.entity, &id_property.name, &id, trace_chain)?
732 .ok_or_else(|| {
733 RepositoryError::Runtime(RuntimeError::Graph(format!(
734 "reference node {}({}) does not exist",
735 node.entity,
736 graph_identity_key(&id)
737 )))
738 })?;
739
740 if let Some(version_property) = descriptor.version_property() {
741 if let Some(Value::I64(existing_version)) = current.get(&version_property.name) {
742 if *existing_version < 0 {
743 return Err(RepositoryError::Runtime(RuntimeError::Graph(format!(
744 "reference node {}({}) is deleted",
745 node.entity,
746 graph_identity_key(&id)
747 ))));
748 }
749 if let Some(Value::I64(expected_version)) = node.values.get(&version_property.name)
750 {
751 if expected_version != existing_version {
752 return Err(RepositoryError::Runtime(
753 RuntimeError::OptimisticLockConflict {
754 entity: node.entity,
755 id: graph_identity_key(&id),
756 },
757 ));
758 }
759 }
760 }
761 }
762
763 Ok(GraphNode {
764 entity: node.entity,
765 values: current,
766 relations: BTreeMap::new(),
767 operation: GraphOperation::Reference,
768 comment: None,
769 })
770 }
771
772 fn validate_remove_node(&self, node: &GraphNode, trace_chain: Vec<teaql_core::TraceNode>) -> Result<(), RepositoryError<E::Error>> {
773 if !node.relations.is_empty() {
774 return Err(RepositoryError::Runtime(RuntimeError::Graph(format!(
775 "remove node {} cannot contain child relations",
776 node.entity
777 ))));
778 }
779 let descriptor = self
780 .repository
781 .metadata
782 .context
783 .require_entity(&node.entity)
784 .map_err(RepositoryError::Runtime)?;
785 let id_property = descriptor.id_property().ok_or_else(|| {
786 RepositoryError::Runtime(RuntimeError::Graph(format!(
787 "entity {} has no id property for graph remove",
788 node.entity
789 )))
790 })?;
791 let id = node
792 .values
793 .get(&id_property.name)
794 .filter(|value| !is_unassigned_id_value(value))
795 .cloned()
796 .ok_or_else(|| {
797 RepositoryError::Runtime(RuntimeError::Graph(format!(
798 "remove node {} missing id property {}",
799 node.entity, id_property.name
800 )))
801 })?;
802 let current = self
803 .fetch_graph_current_row(&node.entity, &id_property.name, &id, trace_chain)?
804 .ok_or_else(|| {
805 RepositoryError::Runtime(RuntimeError::Graph(format!(
806 "remove node {}({}) does not exist",
807 node.entity,
808 graph_identity_key(&id)
809 )))
810 })?;
811 if let Some(version_property) = descriptor.version_property() {
812 if let Some(Value::I64(existing_version)) = current.get(&version_property.name) {
813 if *existing_version < 0 {
814 return Err(RepositoryError::Runtime(RuntimeError::Graph(format!(
815 "remove node {}({}) is already deleted",
816 node.entity,
817 graph_identity_key(&id)
818 ))));
819 }
820 }
821 }
822 Ok(())
823 }
824
825 fn graph_node_from_record(
826 &self,
827 entity: &str,
828 record: Record,
829 ) -> Result<GraphNode, RuntimeError> {
830 let descriptor = self.repository.metadata.context.require_entity(entity)?;
831 let mut node = GraphNode::new(entity);
832
833 for (field, value) in record {
834 if field == "_comment" {
835 if let Value::Text(comment) = value {
836 node.set_comment(comment);
837 }
838 continue;
839 }
840 let Some(relation) = descriptor.relation_by_name(&field) else {
841 node.values.insert(field, value);
842 continue;
843 };
844
845 match value {
846 Value::Null => {
847 node.relations.entry(field).or_default();
848 }
849 Value::Object(record) => {
850 let child = self.graph_node_from_record(&relation.target_entity, record)?;
851 node.relations.entry(field).or_default().push(child);
852 }
853 Value::List(values) => {
854 let children = node.relations.entry(field.clone()).or_default();
855 for value in values {
856 let Value::Object(record) = value else {
857 return Err(RuntimeError::Graph(format!(
858 "relation {}.{} expects object children, got {:?}",
859 entity, field, value
860 )));
861 };
862 children
863 .push(self.graph_node_from_record(&relation.target_entity, record)?);
864 }
865 }
866 other => {
867 return Err(RuntimeError::Graph(format!(
868 "relation {}.{} expects object/list/null, got {:?}",
869 entity, field, other
870 )));
871 }
872 }
873 }
874
875 Ok(node)
876 }
877
878 fn graph_update_command(
879 &self,
880 node: &GraphNode,
881 descriptor: &EntityDescriptor,
882 id_property: &PropertyDescriptor,
883 id: &Value,
884 ) -> UpdateCommand {
885 let mut command = UpdateCommand::new(node.entity.clone(), id.clone());
886 if let Some(version_property) = descriptor.version_property() {
887 if let Some(Value::I64(version)) = node.values.get(&version_property.name) {
888 command = command.expected_version(*version);
889 }
890 }
891 for property in descriptor.properties.iter().filter(|property| {
892 !property.is_id && !property.is_version && node.values.contains_key(&property.name)
893 }) {
894 if property.name == id_property.name {
895 continue;
896 }
897 if let Some(value) = node.values.get(&property.name) {
898 command.values.insert(property.name.clone(), value.clone());
899 }
900 }
901 command
902 }
903
904 fn delete_graph_node<'s>(
905 &self,
906 node: &GraphNode,
907 parent_scope: Option<&'s ScopedCommentNode<'s>>,
908 ) -> Result<u64, RepositoryError<E::Error>> {
909 let descriptor = self
910 .repository
911 .metadata
912 .context
913 .require_entity(&node.entity)
914 .map_err(RepositoryError::Runtime)?;
915 let id_property = descriptor.id_property().ok_or_else(|| {
916 RepositoryError::Runtime(RuntimeError::Graph(format!(
917 "entity {} has no id property for graph remove",
918 node.entity
919 )))
920 })?;
921 let id = node
922 .values
923 .get(&id_property.name)
924 .filter(|value| !is_unassigned_id_value(value))
925 .cloned()
926 .ok_or_else(|| {
927 RepositoryError::Runtime(RuntimeError::Graph(format!(
928 "remove node {} missing id property {}",
929 node.entity, id_property.name
930 )))
931 })?;
932 let mut delete = DeleteCommand::new(node.entity.clone(), id);
933 if let Some(version_property) = descriptor.version_property() {
934 if let Some(Value::I64(version)) = node.values.get(&version_property.name) {
935 delete = delete.expected_version(*version);
936 }
937 }
938
939 let current_scope = node.comment.as_ref().map(|c| ScopedCommentNode {
941 parent: parent_scope,
942 track: teaql_core::TraceNode {
943 entity_type: node.entity.clone(),
944 entity_id: node
945 .id()
946 .and_then(|v| match v {
947 Value::U64(n) => Some(*n),
948 Value::I64(n) => Some(*n as u64),
949 _ => None,
950 }),
951 comment: c.clone(),
952 },
953 });
954 let active_scope = current_scope.as_ref().or(parent_scope);
955 let lineage = active_scope.map(|s| s.to_trace_chain()).unwrap_or_default();
956
957 self.delete_scoped(&delete, lineage)
958 }
959
960 fn fetch_graph_current_row(
961 &self,
962 entity: &str,
963 id_property: &str,
964 id: &Value,
965 trace_chain: Vec<teaql_core::TraceNode>,
966 ) -> Result<Option<Record>, RepositoryError<E::Error>> {
967 let mut query = SelectQuery::new(entity).filter(Expr::eq(id_property, id.clone()));
968 query.trace_chain = trace_chain;
969 let mut rows = self
970 .scoped_repository(entity.to_owned())
971 .fetch_all(&query)?;
972 Ok(rows.pop())
973 }
974
975 fn fetch_graph_children(
976 &self,
977 entity: &str,
978 foreign_key: &str,
979 parent_value: &Value,
980 trace_chain: Vec<teaql_core::TraceNode>,
981 ) -> Result<Vec<Record>, RepositoryError<E::Error>> {
982 let mut query = SelectQuery::new(entity).filter(Expr::eq(foreign_key, parent_value.clone()));
983 query.trace_chain = trace_chain;
984 self.scoped_repository(entity.to_owned()).fetch_all(&query)
985 }
986}