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