1use super::{should_include_table, DiffConfig};
4use crate::schema::{Column, ColumnType, ForeignKey, IndexDef, Schema, TableSchema};
5use glob::Pattern;
6use serde::Serialize;
7
8#[derive(Debug, Serialize)]
10pub struct SchemaDiff {
11 pub tables_added: Vec<TableInfo>,
13 pub tables_removed: Vec<String>,
15 pub tables_modified: Vec<TableModification>,
17}
18
19impl SchemaDiff {
20 pub fn has_changes(&self) -> bool {
22 !self.tables_added.is_empty()
23 || !self.tables_removed.is_empty()
24 || !self.tables_modified.is_empty()
25 }
26}
27
28#[derive(Debug, Serialize)]
30pub struct TableInfo {
31 pub name: String,
32 pub columns: Vec<ColumnInfo>,
33 pub primary_key: Vec<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub create_statement: Option<String>,
36}
37
38impl From<&TableSchema> for TableInfo {
39 fn from(t: &TableSchema) -> Self {
40 Self {
41 name: t.name.clone(),
42 columns: t.columns.iter().map(ColumnInfo::from).collect(),
43 primary_key: t
44 .primary_key
45 .iter()
46 .filter_map(|id| t.column(*id).map(|c| c.name.clone()))
47 .collect(),
48 create_statement: t.create_statement.clone(),
49 }
50 }
51}
52
53#[derive(Debug, Serialize, Clone)]
55pub struct ColumnInfo {
56 pub name: String,
57 pub col_type: String,
58 pub is_nullable: bool,
59 pub is_primary_key: bool,
60}
61
62impl From<&Column> for ColumnInfo {
63 fn from(c: &Column) -> Self {
64 Self {
65 name: c.name.clone(),
66 col_type: format_column_type(&c.col_type),
67 is_nullable: c.is_nullable,
68 is_primary_key: c.is_primary_key,
69 }
70 }
71}
72
73fn format_column_type(ct: &ColumnType) -> String {
74 match ct {
75 ColumnType::Int => "INT".to_string(),
76 ColumnType::BigInt => "BIGINT".to_string(),
77 ColumnType::Text => "TEXT".to_string(),
78 ColumnType::Uuid => "UUID".to_string(),
79 ColumnType::Decimal => "DECIMAL".to_string(),
80 ColumnType::DateTime => "DATETIME".to_string(),
81 ColumnType::Bool => "BOOLEAN".to_string(),
82 ColumnType::Other(s) => s.clone(),
83 }
84}
85
86#[derive(Debug, Serialize)]
88pub struct TableModification {
89 pub table_name: String,
91 pub columns_added: Vec<ColumnInfo>,
93 pub columns_removed: Vec<ColumnInfo>,
95 pub columns_modified: Vec<ColumnChange>,
97 pub pk_changed: bool,
99 #[serde(skip_serializing_if = "Option::is_none")]
101 pub old_pk: Option<Vec<String>>,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 pub new_pk: Option<Vec<String>>,
105 pub fks_added: Vec<FkInfo>,
107 pub fks_removed: Vec<FkInfo>,
109 pub indexes_added: Vec<IndexInfo>,
111 pub indexes_removed: Vec<IndexInfo>,
113}
114
115impl TableModification {
116 pub fn has_changes(&self) -> bool {
118 !self.columns_added.is_empty()
119 || !self.columns_removed.is_empty()
120 || !self.columns_modified.is_empty()
121 || self.pk_changed
122 || !self.fks_added.is_empty()
123 || !self.fks_removed.is_empty()
124 || !self.indexes_added.is_empty()
125 || !self.indexes_removed.is_empty()
126 }
127}
128
129#[derive(Debug, Serialize)]
131pub struct ColumnChange {
132 pub name: String,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub old_type: Option<String>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub new_type: Option<String>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub old_nullable: Option<bool>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub new_nullable: Option<bool>,
141}
142
143#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
145pub struct FkInfo {
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub name: Option<String>,
148 pub columns: Vec<String>,
149 pub referenced_table: String,
150 pub referenced_columns: Vec<String>,
151}
152
153impl From<&ForeignKey> for FkInfo {
154 fn from(fk: &ForeignKey) -> Self {
155 Self {
156 name: fk.name.clone(),
157 columns: fk.column_names.clone(),
158 referenced_table: fk.referenced_table.clone(),
159 referenced_columns: fk.referenced_columns.clone(),
160 }
161 }
162}
163
164#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
166pub struct IndexInfo {
167 pub name: String,
168 pub columns: Vec<String>,
169 pub is_unique: bool,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub index_type: Option<String>,
172}
173
174impl From<&IndexDef> for IndexInfo {
175 fn from(idx: &IndexDef) -> Self {
176 Self {
177 name: idx.name.clone(),
178 columns: idx.columns.clone(),
179 is_unique: idx.is_unique,
180 index_type: idx.index_type.clone(),
181 }
182 }
183}
184
185fn parse_ignore_patterns(patterns: &[String]) -> Vec<Pattern> {
187 patterns
188 .iter()
189 .filter_map(|p| Pattern::new(&p.to_lowercase()).ok())
190 .collect()
191}
192
193fn should_ignore_column(table: &str, column: &str, patterns: &[Pattern]) -> bool {
195 let full_name = format!("{}.{}", table.to_lowercase(), column.to_lowercase());
196 patterns.iter().any(|p| p.matches(&full_name))
197}
198
199pub fn compare_schemas(
201 old_schema: &Schema,
202 new_schema: &Schema,
203 config: &DiffConfig,
204) -> SchemaDiff {
205 let mut tables_added = Vec::new();
206 let mut tables_removed = Vec::new();
207 let mut tables_modified = Vec::new();
208
209 let ignore_patterns = parse_ignore_patterns(&config.ignore_columns);
211
212 for new_table in new_schema.iter() {
214 if !should_include_table(&new_table.name, &config.tables, &config.exclude) {
215 continue;
216 }
217
218 if old_schema.get_table(&new_table.name).is_none() {
219 let mut table_info = TableInfo::from(new_table);
221 if !ignore_patterns.is_empty() {
222 table_info.columns.retain(|col| {
223 !should_ignore_column(&new_table.name, &col.name, &ignore_patterns)
224 });
225 }
226 tables_added.push(table_info);
227 }
228 }
229
230 for old_table in old_schema.iter() {
232 if !should_include_table(&old_table.name, &config.tables, &config.exclude) {
233 continue;
234 }
235
236 match new_schema.get_table(&old_table.name) {
237 None => {
238 tables_removed.push(old_table.name.clone());
239 }
240 Some(new_table) => {
241 let modification =
242 compare_tables(old_table, new_table, &old_table.name, &ignore_patterns);
243 if modification.has_changes() {
244 tables_modified.push(modification);
245 }
246 }
247 }
248 }
249
250 SchemaDiff {
251 tables_added,
252 tables_removed,
253 tables_modified,
254 }
255}
256
257fn compare_tables(
259 old_table: &TableSchema,
260 new_table: &TableSchema,
261 table_name: &str,
262 ignore_patterns: &[Pattern],
263) -> TableModification {
264 let mut columns_added = Vec::new();
265 let mut columns_removed = Vec::new();
266 let mut columns_modified = Vec::new();
267
268 let old_columns: std::collections::HashMap<String, &Column> = old_table
270 .columns
271 .iter()
272 .map(|c| (c.name.to_lowercase(), c))
273 .collect();
274 let new_columns: std::collections::HashMap<String, &Column> = new_table
275 .columns
276 .iter()
277 .map(|c| (c.name.to_lowercase(), c))
278 .collect();
279
280 for new_col in &new_table.columns {
282 if should_ignore_column(table_name, &new_col.name, ignore_patterns) {
284 continue;
285 }
286 let key = new_col.name.to_lowercase();
287 if !old_columns.contains_key(&key) {
288 columns_added.push(ColumnInfo::from(new_col));
289 }
290 }
291
292 for old_col in &old_table.columns {
294 if should_ignore_column(table_name, &old_col.name, ignore_patterns) {
296 continue;
297 }
298 let key = old_col.name.to_lowercase();
299 match new_columns.get(&key) {
300 None => {
301 columns_removed.push(ColumnInfo::from(old_col));
302 }
303 Some(new_col) => {
304 if let Some(change) = compare_columns(old_col, new_col) {
305 columns_modified.push(change);
306 }
307 }
308 }
309 }
310
311 let old_pk: Vec<String> = old_table
313 .primary_key
314 .iter()
315 .filter_map(|id| old_table.column(*id).map(|c| c.name.clone()))
316 .collect();
317 let new_pk: Vec<String> = new_table
318 .primary_key
319 .iter()
320 .filter_map(|id| new_table.column(*id).map(|c| c.name.clone()))
321 .collect();
322
323 let pk_changed = old_pk != new_pk;
324
325 let old_fks: Vec<FkInfo> = old_table.foreign_keys.iter().map(FkInfo::from).collect();
327 let new_fks: Vec<FkInfo> = new_table.foreign_keys.iter().map(FkInfo::from).collect();
328
329 let fks_added: Vec<FkInfo> = new_fks
330 .iter()
331 .filter(|fk| !old_fks.contains(fk))
332 .cloned()
333 .collect();
334 let fks_removed: Vec<FkInfo> = old_fks
335 .iter()
336 .filter(|fk| !new_fks.contains(fk))
337 .cloned()
338 .collect();
339
340 let old_indexes: Vec<IndexInfo> = old_table.indexes.iter().map(IndexInfo::from).collect();
342 let new_indexes: Vec<IndexInfo> = new_table.indexes.iter().map(IndexInfo::from).collect();
343
344 let indexes_added: Vec<IndexInfo> = new_indexes
345 .iter()
346 .filter(|idx| !old_indexes.contains(idx))
347 .cloned()
348 .collect();
349 let indexes_removed: Vec<IndexInfo> = old_indexes
350 .iter()
351 .filter(|idx| !new_indexes.contains(idx))
352 .cloned()
353 .collect();
354
355 TableModification {
356 table_name: old_table.name.clone(),
357 columns_added,
358 columns_removed,
359 columns_modified,
360 pk_changed,
361 old_pk: if pk_changed { Some(old_pk) } else { None },
362 new_pk: if pk_changed { Some(new_pk) } else { None },
363 fks_added,
364 fks_removed,
365 indexes_added,
366 indexes_removed,
367 }
368}
369
370fn compare_columns(old_col: &Column, new_col: &Column) -> Option<ColumnChange> {
372 let type_changed = old_col.col_type != new_col.col_type;
373 let nullable_changed = old_col.is_nullable != new_col.is_nullable;
374
375 if !type_changed && !nullable_changed {
376 return None;
377 }
378
379 Some(ColumnChange {
380 name: old_col.name.clone(),
381 old_type: if type_changed {
382 Some(format_column_type(&old_col.col_type))
383 } else {
384 None
385 },
386 new_type: if type_changed {
387 Some(format_column_type(&new_col.col_type))
388 } else {
389 None
390 },
391 old_nullable: if nullable_changed {
392 Some(old_col.is_nullable)
393 } else {
394 None
395 },
396 new_nullable: if nullable_changed {
397 Some(new_col.is_nullable)
398 } else {
399 None
400 },
401 })
402}