data_modelling_sdk/validation/
tables.rs

1//! Table validation functionality
2//!
3//! Validates tables for naming conflicts, pattern exclusivity, etc.
4//!
5//! This module implements SDK-native validation against SDK models.
6
7use crate::models::Table;
8use uuid::Uuid;
9
10/// Result of table validation.
11///
12/// Contains any naming conflicts or pattern violations found during validation.
13#[derive(Debug)]
14#[must_use = "validation results should be checked for conflicts and violations"]
15pub struct TableValidationResult {
16    /// Naming conflicts found
17    pub naming_conflicts: Vec<NamingConflict>,
18    /// Pattern exclusivity violations
19    pub pattern_violations: Vec<PatternViolation>,
20}
21
22/// Naming conflict between two tables
23#[derive(Debug, Clone)]
24pub struct NamingConflict {
25    pub new_table_id: Uuid,
26    pub new_table_name: String,
27    pub existing_table_id: Uuid,
28    pub existing_table_name: String,
29}
30
31/// Pattern exclusivity violation
32#[derive(Debug, Clone)]
33pub struct PatternViolation {
34    pub table_id: Uuid,
35    pub table_name: String,
36    pub message: String,
37}
38
39/// Error during table validation
40#[derive(Debug, thiserror::Error)]
41pub enum TableValidationError {
42    #[error("Validation error: {0}")]
43    ValidationError(String),
44}
45
46/// Table validator
47#[derive(Default)]
48pub struct TableValidator;
49
50impl TableValidator {
51    /// Create a new table validator
52    ///
53    /// # Example
54    ///
55    /// ```rust
56    /// use data_modelling_sdk::validation::tables::TableValidator;
57    ///
58    /// let validator = TableValidator::new();
59    /// ```
60    pub fn new() -> Self {
61        Self
62    }
63
64    /// Detect naming conflicts between new tables and existing tables
65    ///
66    /// The logic checks for conflicts using unique keys:
67    /// (database_type, name, catalog_name, schema_name)
68    ///
69    /// # Arguments
70    ///
71    /// * `existing_tables` - Tables that already exist
72    /// * `new_tables` - New tables to check for conflicts
73    ///
74    /// # Returns
75    ///
76    /// A vector of `NamingConflict` structs for each conflict found.
77    ///
78    /// # Example
79    ///
80    /// ```rust
81    /// use data_modelling_sdk::validation::tables::TableValidator;
82    /// use data_modelling_sdk::models::{Table, Column};
83    ///
84    /// let validator = TableValidator::new();
85    /// let existing = vec![Table::new("users".to_string(), vec![])];
86    /// let new_tables = vec![Table::new("users".to_string(), vec![])];
87    ///
88    /// let conflicts = validator.detect_naming_conflicts(&existing, &new_tables);
89    /// assert_eq!(conflicts.len(), 1);
90    /// ```
91    pub fn detect_naming_conflicts(
92        &self,
93        existing_tables: &[Table],
94        new_tables: &[Table],
95    ) -> Vec<NamingConflict> {
96        let mut conflicts = Vec::new();
97
98        // Build a map of existing tables by unique key
99        let mut existing_map = std::collections::HashMap::new();
100        for table in existing_tables {
101            let key = table.get_unique_key();
102            existing_map.insert(key, table);
103        }
104
105        // Check new tables against existing
106        for new_table in new_tables {
107            let key = new_table.get_unique_key();
108
109            if let Some(existing) = existing_map.get(&key) {
110                conflicts.push(NamingConflict {
111                    new_table_id: new_table.id,
112                    new_table_name: new_table.name.clone(),
113                    existing_table_id: existing.id,
114                    existing_table_name: existing.name.clone(),
115                });
116            }
117        }
118
119        conflicts
120    }
121
122    /// Validate pattern exclusivity (SCD pattern and Data Vault classification are mutually exclusive)
123    ///
124    /// # Arguments
125    ///
126    /// * `table` - The table to validate
127    ///
128    /// # Returns
129    ///
130    /// `Ok(())` if valid, `Err(PatternViolation)` if both SCD pattern and Data Vault classification are set.
131    ///
132    /// # Example
133    ///
134    /// ```rust
135    /// use data_modelling_sdk::validation::tables::TableValidator;
136    /// use data_modelling_sdk::models::{Table, Column};
137    /// use data_modelling_sdk::models::enums::{SCDPattern, DataVaultClassification};
138    ///
139    /// let validator = TableValidator::new();
140    /// let mut table = Table::new("test".to_string(), vec![]);
141    /// table.scd_pattern = Some(SCDPattern::Type2);
142    /// table.data_vault_classification = Some(DataVaultClassification::Hub);
143    ///
144    /// let result = validator.validate_pattern_exclusivity(&table);
145    /// assert!(result.is_err());
146    /// ```
147    pub fn validate_pattern_exclusivity(
148        &self,
149        table: &Table,
150    ) -> std::result::Result<(), PatternViolation> {
151        if table.scd_pattern.is_some() && table.data_vault_classification.is_some() {
152            return Err(PatternViolation {
153                table_id: table.id,
154                table_name: table.name.clone(),
155                message: "SCD pattern and Data Vault classification are mutually exclusive"
156                    .to_string(),
157            });
158        }
159
160        Ok(())
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167    use crate::models::column::Column;
168    use crate::models::table::Table as SdkTable;
169
170    #[test]
171    fn detects_naming_conflicts_using_unique_key() {
172        let t1 = SdkTable::new(
173            "users".to_string(),
174            vec![Column::new("id".to_string(), "int".to_string())],
175        );
176        let t2 = SdkTable {
177            id: Uuid::new_v4(),
178            ..t1.clone()
179        };
180
181        let v = TableValidator::new().detect_naming_conflicts(&[t1], &[t2]);
182        assert_eq!(v.len(), 1);
183        assert_eq!(v[0].new_table_name, "users");
184    }
185
186    #[test]
187    fn enforces_pattern_exclusivity() {
188        let mut t = SdkTable::new("t".to_string(), vec![]);
189        t.scd_pattern = Some(crate::models::enums::SCDPattern::Type2);
190        t.data_vault_classification = Some(crate::models::enums::DataVaultClassification::Hub);
191
192        let err = TableValidator::new()
193            .validate_pattern_exclusivity(&t)
194            .unwrap_err();
195        assert!(err.message.contains("mutually exclusive"));
196    }
197}