data_modelling_core/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_core::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_core::validation::tables::TableValidator;
83 /// use data_modelling_core::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_core::validation::tables::TableValidator;
137 /// use data_modelling_core::models::{Table, Column};
138 /// use data_modelling_core::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}