1use sqlparser::ast::{Statement, ColumnDef, ColumnOption};
7use crate::analyzer::{SchemaAnalyzer, SchemaChange, ChangeType};
8use crate::{Schema, CompatibilityReport, MigrationPlan, ValidationResult, SchemaDiffError};
9use crate::error::Result;
10use crate::report::{CompatibilityIssue, IssueSeverity, ValidationError};
11use std::collections::HashMap;
12
13pub struct SqlAnalyzer;
15
16impl SchemaAnalyzer for SqlAnalyzer {
17 fn analyze_compatibility(&self, old: &Schema, new: &Schema) -> Result<CompatibilityReport> {
28 let metadata = HashMap::new();
29
30 let mut changes = Vec::new();
31 self.compare_schemas(old, new, &mut changes);
32
33 let compatibility_score = self.calculate_compatibility_score(&changes);
34 let validation_result = self.validate_changes(&changes)?;
35
36 Ok(CompatibilityReport {
37 changes,
38 compatibility_score,
39 is_compatible: compatibility_score >= 80,
40 issues: validation_result.errors.into_iter().map(|err| CompatibilityIssue {
41 severity: match err.code.as_str() {
42 "SQL001" => IssueSeverity::Error,
43 "SQL002" => IssueSeverity::Warning,
44 _ => IssueSeverity::Info,
45 },
46 description: err.message,
47 location: err.path,
48 }).collect(),
49 metadata,
50 })
51 }
52
53 fn generate_migration_path(&self, old: &Schema, new: &Schema) -> Result<MigrationPlan> {
64 let mut changes = Vec::new();
65 self.compare_schemas(old, new, &mut changes);
66
67 Ok(MigrationPlan::new(
68 old.version.to_string(),
69 new.version.to_string(),
70 changes,
71 ))
72 }
73
74 fn validate_changes(&self, changes: &[SchemaChange]) -> Result<ValidationResult> {
75 let mut errors = Vec::new();
76
77 for change in changes {
78 if let Some(issue) = self.validate_change(change) {
79 errors.push(ValidationError {
80 message: issue.description,
81 path: issue.location,
82 code: match issue.severity {
83 IssueSeverity::Error => "SQL001",
84 IssueSeverity::Warning => "SQL002",
85 IssueSeverity::Info => "SQL003",
86 }.to_string(),
87 });
88 }
89 }
90
91 Ok(ValidationResult {
92 errors: errors.clone(),
93 is_valid: errors.is_empty(),
94 context: HashMap::new(),
95 })
96 }
97}
98
99impl SqlAnalyzer {
100 fn compare_schemas(&self, old: &Schema, new: &Schema, changes: &mut Vec<SchemaChange>) {
101 if let (Ok(old_tables), Ok(new_tables)) = (
102 self.parse_tables(&old.content),
103 self.parse_tables(&new.content)
104 ) {
105 for old_table in old_tables.iter() {
107 if let Statement::CreateTable(ref old_table_data) = old_table {
108 let name = &old_table_data.name;
109 let old_columns = &old_table_data.columns;
110 if let Some(new_table) = new_tables.iter().find(|t| {
111 if let Statement::CreateTable(ref new_table_data) = t {
112 &new_table_data.name == name
113 } else {
114 false
115 }
116 }) {
117 if let Statement::CreateTable(ref new_table_data) = new_table {
118 let new_columns = &new_table_data.columns;
119 self.compare_columns(name.to_string(), old_columns, new_columns, changes);
120 }
121 } else {
122 let mut metadata = HashMap::new();
123 metadata.insert("table".to_string(), name.to_string());
124
125 changes.push(SchemaChange::new(
126 ChangeType::Removal,
127 format!("table/{}", name),
128 format!("Table '{}' was removed", name),
129 metadata,
130 ));
131 }
132 }
133 }
134
135 for new_table in new_tables.iter() {
137 if let Statement::CreateTable(ref new_table_data) = new_table {
138 let table_name = &new_table_data.name;
139 if !old_tables.iter().any(|t| {
140 if let Statement::CreateTable(ref old_table_data) = t {
141 &old_table_data.name == table_name
142 } else {
143 false
144 }
145 }) {
146 let mut metadata = HashMap::new();
147 metadata.insert("table".to_string(), table_name.to_string());
148
149 changes.push(SchemaChange::new(
150 ChangeType::Addition,
151 format!("table/{}", table_name),
152 format!("New table '{}' was added", table_name),
153 metadata,
154 ));
155 }
156 }
157 }
158 }
159 }
160
161 fn compare_columns(&self, table_name: String, old_columns: &[ColumnDef], new_columns: &[ColumnDef], changes: &mut Vec<SchemaChange>) {
162 for old_col in old_columns {
163 if let Some(new_col) = new_columns.iter().find(|c| c.name == old_col.name) {
164 if old_col.data_type != new_col.data_type {
166 let mut metadata = HashMap::new();
167 metadata.insert("table".to_string(), table_name.clone());
168 metadata.insert("column".to_string(), old_col.name.to_string());
169 metadata.insert("old_type".to_string(), format!("{:?}", old_col.data_type));
170 metadata.insert("new_type".to_string(), format!("{:?}", new_col.data_type));
171
172 changes.push(SchemaChange::new(
173 ChangeType::Modification,
174 format!("{}/{}", table_name, old_col.name),
175 format!("Column '{}' type changed from {:?} to {:?}",
176 old_col.name, old_col.data_type, new_col.data_type),
177 metadata,
178 ));
179 }
180
181 let old_opts: Vec<ColumnOption> = old_col.options.iter()
183 .map(|opt| opt.option.clone())
184 .collect();
185 let new_opts: Vec<ColumnOption> = new_col.options.iter()
186 .map(|opt| opt.option.clone())
187 .collect();
188
189 self.compare_column_constraints(
191 &table_name,
192 &old_col.name.to_string(),
193 &old_opts,
194 &new_opts,
195 changes,
196 );
197 } else {
198 let mut metadata = HashMap::new();
199 metadata.insert("table".to_string(), table_name.clone());
200 metadata.insert("column".to_string(), old_col.name.to_string());
201
202 changes.push(SchemaChange::new(
203 ChangeType::Removal,
204 format!("{}/{}", table_name, old_col.name),
205 format!("Column '{}' was removed", old_col.name),
206 metadata,
207 ));
208 }
209 }
210
211 for new_col in new_columns {
213 if !old_columns.iter().any(|c| c.name == new_col.name) {
214 let mut metadata = HashMap::new();
215 metadata.insert("table".to_string(), table_name.clone());
216 metadata.insert("column".to_string(), new_col.name.to_string());
217
218 changes.push(SchemaChange::new(
219 ChangeType::Addition,
220 format!("{}/{}", table_name, new_col.name),
221 format!("New column '{}' was added", new_col.name),
222 metadata,
223 ));
224 }
225 }
226 }
227
228 fn compare_column_constraints(
229 &self,
230 table_name: &str,
231 column_name: &str,
232 old_options: &[ColumnOption],
233 new_options: &[ColumnOption],
234 changes: &mut Vec<SchemaChange>,
235 ) {
236 for old_opt in old_options {
238 let found_in_new = new_options.iter().any(|new_opt| {
239 match (old_opt, new_opt) {
240 (ColumnOption::NotNull, ColumnOption::NotNull) => true,
241 (ColumnOption::Default(_), ColumnOption::Default(_)) => true,
242 (ColumnOption::Unique { is_primary, characteristics: _ },
243 ColumnOption::Unique { is_primary: new_primary, characteristics: _ }) => {
244 is_primary == new_primary
245 }
246 _ => false,
247 }
248 });
249
250 if !found_in_new {
251 let mut metadata = HashMap::new();
252 metadata.insert("table".to_string(), table_name.to_string());
253 metadata.insert("column".to_string(), column_name.to_string());
254 metadata.insert("constraint".to_string(), format!("{:?}", old_opt));
255
256 changes.push(SchemaChange::new(
257 ChangeType::Removal,
258 format!("{}/{}/constraints", table_name, column_name),
259 format!("Constraint removed from column '{}': {:?}", column_name, old_opt),
260 metadata,
261 ));
262 }
263 }
264
265 for new_opt in new_options {
267 let found_in_old = old_options.iter().any(|old_opt| {
268 match (old_opt, new_opt) {
269 (ColumnOption::NotNull, ColumnOption::NotNull) => true,
270 (ColumnOption::Default(_), ColumnOption::Default(_)) => true,
271 (ColumnOption::Unique { is_primary, characteristics: _ },
272 ColumnOption::Unique { is_primary: new_primary, characteristics: _ }) => {
273 is_primary == new_primary
274 }
275 _ => false,
276 }
277 });
278
279 if !found_in_old {
280 let mut metadata = HashMap::new();
281 metadata.insert("table".to_string(), table_name.to_string());
282 metadata.insert("column".to_string(), column_name.to_string());
283 metadata.insert("constraint".to_string(), format!("{:?}", new_opt));
284
285 changes.push(SchemaChange::new(
286 ChangeType::Addition,
287 format!("{}/{}/constraints", table_name, column_name),
288 format!("New constraint added to column '{}': {:?}", column_name, new_opt),
289 metadata,
290 ));
291 }
292 }
293 }
294
295 fn calculate_compatibility_score(&self, changes: &[SchemaChange]) -> u8 {
296 let base_score: u8 = 100;
297 let mut deductions: u8 = 0;
298
299 for change in changes {
300 match change.change_type {
301 ChangeType::Addition => deductions = deductions.saturating_add(5),
302 ChangeType::Removal => deductions = deductions.saturating_add(15),
303 ChangeType::Modification => deductions = deductions.saturating_add(10),
304 ChangeType::Rename => deductions = deductions.saturating_add(8),
305 }
306 }
307
308 base_score.saturating_sub(deductions)
309 }
310
311 fn validate_change(&self, change: &SchemaChange) -> Option<CompatibilityIssue> {
312 match change.change_type {
313 ChangeType::Removal => Some(CompatibilityIssue {
314 severity: IssueSeverity::Error,
315 description: format!("Breaking change: {}", change.description),
316 location: change.location.clone(),
317 }),
318 ChangeType::Modification => {
319 if change.location.contains("type") {
320 Some(CompatibilityIssue {
321 severity: IssueSeverity::Warning,
322 description: format!("Potential data loss: {}", change.description),
323 location: change.location.clone(),
324 })
325 } else {
326 None
327 }
328 }
329 _ => None,
330 }
331 }
332
333 fn parse_tables(&self, sql: &str) -> Result<Vec<Statement>> {
334 use sqlparser::dialect::GenericDialect;
335 use sqlparser::parser::Parser;
336
337 let dialect = GenericDialect {};
338 Parser::parse_sql(&dialect, sql)
339 .map_err(|e| SchemaDiffError::ParseError(format!("Failed to parse SQL: {}", e)))
340 }
341
342 #[allow(dead_code)]
343 fn generate_sql_for_change(&self, change: &SchemaChange) -> String {
344 match change.change_type {
345 ChangeType::Addition => {
346 if change.location.starts_with("table/") {
347 format!("CREATE TABLE {} (...);", change.location.strip_prefix("table/").unwrap_or(""))
348 } else {
349 format!("ALTER TABLE {} ADD COLUMN ...;", change.location)
350 }
351 }
352 ChangeType::Removal => {
353 if change.location.starts_with("table/") {
354 format!("DROP TABLE {};", change.location.strip_prefix("table/").unwrap_or(""))
355 } else {
356 format!("ALTER TABLE {} DROP COLUMN ...;", change.location)
357 }
358 }
359 ChangeType::Modification => {
360 format!("ALTER TABLE {} MODIFY COLUMN ...;", change.location)
361 }
362 ChangeType::Rename => {
363 format!("ALTER TABLE {} RENAME ...;", change.location)
364 }
365 }
366 }
367}