1use super::{parse_ignore_patterns, should_ignore_column, 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
185pub fn compare_schemas(
187 old_schema: &Schema,
188 new_schema: &Schema,
189 config: &DiffConfig,
190) -> SchemaDiff {
191 let mut tables_added = Vec::new();
192 let mut tables_removed = Vec::new();
193 let mut tables_modified = Vec::new();
194
195 let ignore_patterns = parse_ignore_patterns(&config.ignore_columns);
197
198 for new_table in new_schema.iter() {
200 if !should_include_table(&new_table.name, &config.tables, &config.exclude) {
201 continue;
202 }
203
204 if old_schema.get_table(&new_table.name).is_none() {
205 let mut table_info = TableInfo::from(new_table);
207 if !ignore_patterns.is_empty() {
208 table_info.columns.retain(|col| {
209 !should_ignore_column(&new_table.name, &col.name, &ignore_patterns)
210 });
211 }
212 tables_added.push(table_info);
213 }
214 }
215
216 for old_table in old_schema.iter() {
218 if !should_include_table(&old_table.name, &config.tables, &config.exclude) {
219 continue;
220 }
221
222 match new_schema.get_table(&old_table.name) {
223 None => {
224 tables_removed.push(old_table.name.clone());
225 }
226 Some(new_table) => {
227 let modification =
228 compare_tables(old_table, new_table, &old_table.name, &ignore_patterns);
229 if modification.has_changes() {
230 tables_modified.push(modification);
231 }
232 }
233 }
234 }
235
236 SchemaDiff {
237 tables_added,
238 tables_removed,
239 tables_modified,
240 }
241}
242
243fn compare_tables(
245 old_table: &TableSchema,
246 new_table: &TableSchema,
247 table_name: &str,
248 ignore_patterns: &[Pattern],
249) -> TableModification {
250 let mut columns_added = Vec::new();
251 let mut columns_removed = Vec::new();
252 let mut columns_modified = Vec::new();
253
254 let old_columns: std::collections::HashMap<String, &Column> = old_table
256 .columns
257 .iter()
258 .map(|c| (c.name.to_lowercase(), c))
259 .collect();
260 let new_columns: std::collections::HashMap<String, &Column> = new_table
261 .columns
262 .iter()
263 .map(|c| (c.name.to_lowercase(), c))
264 .collect();
265
266 for new_col in &new_table.columns {
268 if should_ignore_column(table_name, &new_col.name, ignore_patterns) {
270 continue;
271 }
272 let key = new_col.name.to_lowercase();
273 if !old_columns.contains_key(&key) {
274 columns_added.push(ColumnInfo::from(new_col));
275 }
276 }
277
278 for old_col in &old_table.columns {
280 if should_ignore_column(table_name, &old_col.name, ignore_patterns) {
282 continue;
283 }
284 let key = old_col.name.to_lowercase();
285 match new_columns.get(&key) {
286 None => {
287 columns_removed.push(ColumnInfo::from(old_col));
288 }
289 Some(new_col) => {
290 if let Some(change) = compare_columns(old_col, new_col) {
291 columns_modified.push(change);
292 }
293 }
294 }
295 }
296
297 let old_pk: Vec<String> = old_table
299 .primary_key
300 .iter()
301 .filter_map(|id| old_table.column(*id).map(|c| c.name.clone()))
302 .collect();
303 let new_pk: Vec<String> = new_table
304 .primary_key
305 .iter()
306 .filter_map(|id| new_table.column(*id).map(|c| c.name.clone()))
307 .collect();
308
309 let pk_changed = old_pk != new_pk;
310
311 let old_fks: Vec<FkInfo> = old_table.foreign_keys.iter().map(FkInfo::from).collect();
313 let new_fks: Vec<FkInfo> = new_table.foreign_keys.iter().map(FkInfo::from).collect();
314
315 let fks_added: Vec<FkInfo> = new_fks
316 .iter()
317 .filter(|fk| !old_fks.contains(fk))
318 .cloned()
319 .collect();
320 let fks_removed: Vec<FkInfo> = old_fks
321 .iter()
322 .filter(|fk| !new_fks.contains(fk))
323 .cloned()
324 .collect();
325
326 let old_indexes: Vec<IndexInfo> = old_table.indexes.iter().map(IndexInfo::from).collect();
328 let new_indexes: Vec<IndexInfo> = new_table.indexes.iter().map(IndexInfo::from).collect();
329
330 let indexes_added: Vec<IndexInfo> = new_indexes
331 .iter()
332 .filter(|idx| !old_indexes.contains(idx))
333 .cloned()
334 .collect();
335 let indexes_removed: Vec<IndexInfo> = old_indexes
336 .iter()
337 .filter(|idx| !new_indexes.contains(idx))
338 .cloned()
339 .collect();
340
341 TableModification {
342 table_name: old_table.name.clone(),
343 columns_added,
344 columns_removed,
345 columns_modified,
346 pk_changed,
347 old_pk: if pk_changed { Some(old_pk) } else { None },
348 new_pk: if pk_changed { Some(new_pk) } else { None },
349 fks_added,
350 fks_removed,
351 indexes_added,
352 indexes_removed,
353 }
354}
355
356fn compare_columns(old_col: &Column, new_col: &Column) -> Option<ColumnChange> {
358 let type_changed = old_col.col_type != new_col.col_type;
359 let nullable_changed = old_col.is_nullable != new_col.is_nullable;
360
361 if !type_changed && !nullable_changed {
362 return None;
363 }
364
365 Some(ColumnChange {
366 name: old_col.name.clone(),
367 old_type: if type_changed {
368 Some(format_column_type(&old_col.col_type))
369 } else {
370 None
371 },
372 new_type: if type_changed {
373 Some(format_column_type(&new_col.col_type))
374 } else {
375 None
376 },
377 old_nullable: if nullable_changed {
378 Some(old_col.is_nullable)
379 } else {
380 None
381 },
382 new_nullable: if nullable_changed {
383 Some(new_col.is_nullable)
384 } else {
385 None
386 },
387 })
388}