Skip to main content

teaql_runtime/repository/
graph.rs

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    /// Create a new entity graph with an annotation comment on the root node.
140    /// This assumes all new nodes do not exist in the database, skipping existence checks
141    /// and throwing an exception on primary key conflict.
142    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        // Create scope node on the current stack frame if this node has a comment
240        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        // Create scope node on the current stack frame if this node has a comment
349        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        // Create scope node on the current stack frame if this node has a comment
466        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            // Strip comment to prevent duplicate scope — already captured in active_scope
511            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        // Create scope node for deletion if parent/node comment is present
940        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}