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}