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