data_modelling_sdk/validation/
relationships.rs

1//! Relationship validation functionality
2//!
3//! Validates relationships for circular dependencies, self-references, etc.
4//!
5//! This module implements SDK-native validation against SDK models.
6
7use crate::models::Relationship;
8use anyhow::Result;
9use petgraph::{Directed, Graph};
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13/// Result of relationship validation.
14///
15/// Contains any circular dependencies or self-references found during validation.
16#[derive(Debug, Serialize, Deserialize)]
17#[must_use = "validation results should be checked for circular dependencies and self-references"]
18pub struct RelationshipValidationResult {
19    /// Circular dependencies found
20    pub circular_dependencies: Vec<CircularDependency>,
21    /// Self-references found
22    pub self_references: Vec<SelfReference>,
23}
24
25/// Circular dependency detected
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct CircularDependency {
28    pub relationship_id: Uuid,
29    pub cycle_path: Vec<Uuid>,
30}
31
32/// Self-reference detected
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct SelfReference {
35    pub relationship_id: Uuid,
36    pub table_id: Uuid,
37}
38
39/// Error during relationship validation
40#[derive(Debug, thiserror::Error, Serialize, Deserialize)]
41pub enum RelationshipValidationError {
42    #[error("Validation error: {0}")]
43    ValidationError(String),
44}
45
46/// Relationship validator
47#[derive(Default)]
48pub struct RelationshipValidator;
49
50impl RelationshipValidator {
51    /// Create a new relationship validator
52    ///
53    /// # Example
54    ///
55    /// ```rust
56    /// use data_modelling_sdk::validation::relationships::RelationshipValidator;
57    ///
58    /// let validator = RelationshipValidator::new();
59    /// ```
60    pub fn new() -> Self {
61        Self
62    }
63
64    /// Check for circular dependencies using graph cycle detection
65    ///
66    /// Uses petgraph to detect cycles in the relationship graph. If adding the new relationship
67    /// would create a cycle, returns the cycle path.
68    ///
69    /// # Arguments
70    ///
71    /// * `relationships` - Existing relationships in the model
72    /// * `source_table_id` - Source table ID of the new relationship
73    /// * `target_table_id` - Target table ID of the new relationship
74    ///
75    /// # Returns
76    ///
77    /// A tuple of (has_cycle: bool, cycle_path: Option<Vec<Uuid>>).
78    /// If a cycle is detected, the cycle path contains the table IDs forming the cycle.
79    ///
80    /// # Example
81    ///
82    /// ```rust
83    /// use data_modelling_sdk::validation::relationships::RelationshipValidator;
84    /// use data_modelling_sdk::models::Relationship;
85    ///
86    /// let validator = RelationshipValidator::new();
87    /// let table_a = uuid::Uuid::new_v4();
88    /// let table_b = uuid::Uuid::new_v4();
89    /// let table_c = uuid::Uuid::new_v4();
90    ///
91    /// // Create a cycle: A -> B -> C -> A
92    /// let rels = vec![
93    ///     Relationship::new(table_a, table_b),
94    ///     Relationship::new(table_b, table_c),
95    /// ];
96    ///
97    /// let (has_cycle, _) = validator.check_circular_dependency(&rels, table_c, table_a).unwrap();
98    /// assert!(has_cycle);
99    /// ```
100    pub fn check_circular_dependency(
101        &self,
102        relationships: &[Relationship],
103        source_table_id: Uuid,
104        target_table_id: Uuid,
105    ) -> Result<(bool, Option<Vec<Uuid>>), RelationshipValidationError> {
106        // Build a directed graph from relationships
107        let mut graph = Graph::<Uuid, Uuid, Directed>::new();
108        let mut node_map = std::collections::HashMap::new();
109
110        // Add all tables as nodes
111        for rel in relationships {
112            let source_node = *node_map
113                .entry(rel.source_table_id)
114                .or_insert_with(|| graph.add_node(rel.source_table_id));
115            let target_node = *node_map
116                .entry(rel.target_table_id)
117                .or_insert_with(|| graph.add_node(rel.target_table_id));
118            graph.add_edge(source_node, target_node, rel.id);
119        }
120
121        // Add the new relationship being checked
122        let source_node = *node_map
123            .entry(source_table_id)
124            .or_insert_with(|| graph.add_node(source_table_id));
125        let target_node = *node_map
126            .entry(target_table_id)
127            .or_insert_with(|| graph.add_node(target_table_id));
128        // Synthetic edge id (only used internally in the graph).
129        let edge_id = Uuid::new_v4();
130        graph.add_edge(source_node, target_node, edge_id);
131
132        // Check for cycles using simple reachability check
133        // Note: find_negative_cycle requires FloatMeasure trait, so we use a simpler approach
134        // Check if target can reach source (which would create a cycle)
135        if self.can_reach(&graph, &node_map, target_table_id, source_table_id) {
136            // Build cycle path
137            let cycle_path = self
138                .find_path(&graph, &node_map, target_table_id, source_table_id)
139                .unwrap_or_default();
140            return Ok((true, Some(cycle_path)));
141        }
142
143        Ok((false, None))
144    }
145
146    /// Check if target can reach source in the graph
147    fn can_reach(
148        &self,
149        graph: &Graph<Uuid, Uuid, Directed>,
150        node_map: &std::collections::HashMap<Uuid, petgraph::graph::NodeIndex>,
151        from: Uuid,
152        to: Uuid,
153    ) -> bool {
154        if let (Some(&from_idx), Some(&to_idx)) = (node_map.get(&from), node_map.get(&to)) {
155            // Use DFS to check reachability
156            let mut visited = std::collections::HashSet::new();
157            let mut stack = vec![from_idx];
158
159            while let Some(node) = stack.pop() {
160                if node == to_idx {
161                    return true;
162                }
163                if visited.insert(node) {
164                    for neighbor in graph.neighbors(node) {
165                        if !visited.contains(&neighbor) {
166                            stack.push(neighbor);
167                        }
168                    }
169                }
170            }
171        }
172        false
173    }
174
175    /// Find a path from source to target
176    fn find_path(
177        &self,
178        graph: &Graph<Uuid, Uuid, Directed>,
179        node_map: &std::collections::HashMap<Uuid, petgraph::graph::NodeIndex>,
180        from: Uuid,
181        to: Uuid,
182    ) -> Option<Vec<Uuid>> {
183        if let (Some(&from_idx), Some(&to_idx)) = (node_map.get(&from), node_map.get(&to)) {
184            // Use BFS to find path
185            let mut visited = std::collections::HashSet::new();
186            let mut queue = std::collections::VecDeque::new();
187            let mut parent = std::collections::HashMap::new();
188
189            queue.push_back(from_idx);
190            visited.insert(from_idx);
191
192            while let Some(node) = queue.pop_front() {
193                if node == to_idx {
194                    // Reconstruct path
195                    let mut path = Vec::new();
196                    let mut current = Some(to_idx);
197                    while let Some(node_idx) = current {
198                        path.push(graph[node_idx]);
199                        current = parent.get(&node_idx).copied();
200                    }
201                    path.reverse();
202                    return Some(path);
203                }
204
205                for neighbor in graph.neighbors(node) {
206                    if !visited.contains(&neighbor) {
207                        visited.insert(neighbor);
208                        parent.insert(neighbor, node);
209                        queue.push_back(neighbor);
210                    }
211                }
212            }
213        }
214        None
215    }
216
217    /// Validate that source and target tables are different
218    pub fn validate_no_self_reference(
219        &self,
220        source_table_id: Uuid,
221        target_table_id: Uuid,
222    ) -> Result<(), SelfReference> {
223        if source_table_id == target_table_id {
224            return Err(SelfReference {
225                relationship_id: Uuid::new_v4(),
226                table_id: source_table_id,
227            });
228        }
229        Ok(())
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn detects_self_reference() {
239        let id = Uuid::new_v4();
240        let err = RelationshipValidator::new()
241            .validate_no_self_reference(id, id)
242            .unwrap_err();
243        assert_eq!(err.table_id, id);
244    }
245
246    #[test]
247    fn detects_cycle() {
248        let a = Uuid::new_v4();
249        let b = Uuid::new_v4();
250        let rel1 = Relationship::new(a, b);
251        let validator = RelationshipValidator::new();
252        // adding b->a would create a cycle
253        let (has_cycle, path) = validator.check_circular_dependency(&[rel1], b, a).unwrap();
254        assert!(has_cycle);
255        assert!(path.is_some());
256    }
257}