1use crate::pool::Connection;
34use crate::DbError;
35
36#[derive(Debug, Clone, PartialEq)]
42pub struct ColumnDef {
43 pub name: String,
45 pub sql_type: String,
47 pub nullable: bool,
49 pub primary_key: bool,
51 pub default: Option<String>,
53}
54
55impl ColumnDef {
56 pub fn new(
58 name: impl Into<String>,
59 sql_type: impl Into<String>,
60 nullable: bool,
61 primary_key: bool,
62 default: Option<String>,
63 ) -> Self {
64 Self {
65 name: name.into(),
66 sql_type: sql_type.into(),
67 nullable,
68 primary_key,
69 default,
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq)]
76pub struct TableDef {
77 pub name: String,
79 pub columns: Vec<ColumnDef>,
81}
82
83impl TableDef {
84 pub fn new(name: impl Into<String>, columns: Vec<ColumnDef>) -> Self {
86 Self {
87 name: name.into(),
88 columns,
89 }
90 }
91
92 pub fn get_column(&self, name: &str) -> Option<&ColumnDef> {
94 self.columns.iter().find(|c| c.name == name)
95 }
96}
97
98#[derive(Debug, Clone, Default)]
100pub struct SchemaDiff {
101 pub added_tables: Vec<TableDef>,
103 pub dropped_tables: Vec<String>,
105 pub added_columns: Vec<(String, ColumnDef)>,
107 pub dropped_columns: Vec<(String, String)>,
109 pub type_changed_columns: Vec<(String, ColumnDef, ColumnDef)>,
111 pub renamed_columns: Vec<(String, String, String)>,
113}
114
115impl SchemaDiff {
116 pub fn is_empty(&self) -> bool {
118 self.added_tables.is_empty()
119 && self.dropped_tables.is_empty()
120 && self.added_columns.is_empty()
121 && self.dropped_columns.is_empty()
122 && self.type_changed_columns.is_empty()
123 && self.renamed_columns.is_empty()
124 }
125
126 pub fn has_destructive_changes(&self) -> bool {
128 !self.dropped_tables.is_empty() || !self.dropped_columns.is_empty()
129 }
130}
131
132#[derive(Debug, Clone)]
134pub struct SyncResult {
135 pub affected_tables: Vec<String>,
137 pub executed_ddl: Vec<String>,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum Confirm {
146 Yes,
148 No,
150}
151
152pub trait DataMigrationHook: Send + Sync {
157 fn before_drop_column<'a>(
159 &'a self,
160 conn: &'a mut dyn Connection,
161 table: &'a str,
162 column: &'a str,
163 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), DbError>> + Send + 'a>>;
164
165 fn before_rename_column<'a>(
167 &'a self,
168 conn: &'a mut dyn Connection,
169 table: &'a str,
170 old_name: &'a str,
171 new_name: &'a str,
172 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), DbError>> + Send + 'a>>;
173}
174
175#[derive(Debug, Clone)]
177pub struct DestructiveSyncResult {
178 pub executed_ddl: Vec<String>,
180 pub hooks_called: usize,
182 pub audit_entries: usize,
184}
185
186pub fn diff(entity: &[TableDef], db: &[TableDef]) -> SchemaDiff {
201 let mut result = SchemaDiff::default();
202
203 let db_map: std::collections::HashMap<&str, &TableDef> =
204 db.iter().map(|t| (t.name.as_str(), t)).collect();
205 let entity_map: std::collections::HashMap<&str, &TableDef> =
206 entity.iter().map(|t| (t.name.as_str(), t)).collect();
207
208 for t in entity {
210 if !db_map.contains_key(t.name.as_str()) {
211 result.added_tables.push(t.clone());
212 }
213 }
214 for t in db {
215 if !entity_map.contains_key(t.name.as_str()) {
216 result.dropped_tables.push(t.name.clone());
217 }
218 }
219
220 for entity_table in entity {
222 if let Some(db_table) = db_map.get(entity_table.name.as_str()) {
223 diff_columns(&mut result, entity_table, db_table);
224 }
225 }
226
227 result
228}
229
230fn diff_columns(result: &mut SchemaDiff, entity: &TableDef, db: &TableDef) {
232 diff_columns_with_threshold(result, entity, db, 2, 0.3);
233}
234
235fn diff_columns_with_threshold(
237 result: &mut SchemaDiff,
238 entity: &TableDef,
239 db: &TableDef,
240 max_distance: usize,
241 max_ratio: f64,
242) {
243 let db_col_map: std::collections::HashMap<&str, &ColumnDef> =
244 db.columns.iter().map(|c| (c.name.as_str(), c)).collect();
245 let entity_col_map: std::collections::HashMap<&str, &ColumnDef> = entity
246 .columns
247 .iter()
248 .map(|c| (c.name.as_str(), c))
249 .collect();
250
251 let mut added_columns: Vec<&ColumnDef> = Vec::new();
252 let mut dropped_columns: Vec<&ColumnDef> = Vec::new();
253
254 for col in &entity.columns {
255 if !db_col_map.contains_key(col.name.as_str()) {
256 added_columns.push(col);
257 }
258 }
259 for col in &db.columns {
260 if !entity_col_map.contains_key(col.name.as_str()) {
261 dropped_columns.push(col);
262 }
263 }
264
265 let mut renamed_added: Vec<usize> = Vec::new();
267 let mut renamed_dropped: Vec<usize> = Vec::new();
268 for (i, dropped_col) in dropped_columns.iter().enumerate() {
269 if renamed_dropped.contains(&i) {
270 continue;
271 }
272 for (j, added_col) in added_columns.iter().enumerate() {
273 if renamed_added.contains(&j) {
274 continue;
275 }
276 if dropped_col.sql_type != added_col.sql_type {
277 continue;
278 }
279 let dist = levenshtein(&dropped_col.name, &added_col.name);
280 let ratio = dist as f64 / dropped_col.name.len().max(added_col.name.len()) as f64;
281 if dist <= max_distance || ratio <= max_ratio {
282 result.renamed_columns.push((
283 entity.name.clone(),
284 dropped_col.name.clone(),
285 added_col.name.clone(),
286 ));
287 renamed_dropped.push(i);
288 renamed_added.push(j);
289 break;
290 }
291 }
292 }
293
294 for (j, col) in added_columns.iter().enumerate() {
295 if !renamed_added.contains(&j) {
296 result
297 .added_columns
298 .push((entity.name.clone(), (*col).clone()));
299 }
300 }
301 for (i, col) in dropped_columns.iter().enumerate() {
302 if !renamed_dropped.contains(&i) {
303 result
304 .dropped_columns
305 .push((entity.name.clone(), col.name.clone()));
306 }
307 }
308
309 for entity_col in &entity.columns {
311 if let Some(db_col) = db_col_map.get(entity_col.name.as_str()) {
312 if entity_col.sql_type != db_col.sql_type || entity_col.nullable != db_col.nullable {
313 result.type_changed_columns.push((
314 entity.name.clone(),
315 (*db_col).clone(),
316 entity_col.clone(),
317 ));
318 }
319 }
320 }
321}
322
323fn levenshtein(a: &str, b: &str) -> usize {
325 let a_chars: Vec<char> = a.chars().collect();
326 let b_chars: Vec<char> = b.chars().collect();
327 let m = a_chars.len();
328 let n = b_chars.len();
329
330 if m == 0 {
331 return n;
332 }
333 if n == 0 {
334 return m;
335 }
336
337 let mut prev: Vec<usize> = (0..=n).collect();
338 let mut curr = vec![0usize; n + 1];
339
340 for i in 1..=m {
341 curr[0] = i;
342 for j in 1..=n {
343 let cost = if a_chars[i - 1] == b_chars[j - 1] {
344 0
345 } else {
346 1
347 };
348 curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
349 }
350 std::mem::swap(&mut prev, &mut curr);
351 }
352
353 prev[n]
354}
355
356pub trait DdlGenerator: Send + Sync {
362 fn generate(&self, diff: &SchemaDiff) -> Result<Vec<String>, DbError>;
366}
367
368pub struct MySqlDdlGenerator;
370
371impl DdlGenerator for MySqlDdlGenerator {
372 fn generate(&self, diff: &SchemaDiff) -> Result<Vec<String>, DbError> {
373 let mut ddl = Vec::new();
374
375 for table in &diff.added_tables {
377 ddl.push(generate_create_table_mysql(table));
378 }
379
380 for (table, col) in &diff.added_columns {
382 ddl.push(format!(
383 "ALTER TABLE {} ADD COLUMN {} {}{}{}",
384 table,
385 col.name,
386 col.sql_type,
387 if col.nullable { "" } else { " NOT NULL" },
388 if col.primary_key { " PRIMARY KEY" } else { "" }
389 ));
390 }
391
392 for (table, _old, new) in &diff.type_changed_columns {
394 ddl.push(format!(
395 "ALTER TABLE {} MODIFY COLUMN {} {}{}",
396 table,
397 new.name,
398 new.sql_type,
399 if new.nullable { "" } else { " NOT NULL" }
400 ));
401 }
402
403 for (table, old, new) in &diff.renamed_columns {
405 ddl.push(format!(
406 "ALTER TABLE {} RENAME COLUMN {} TO {}",
407 table, old, new
408 ));
409 }
410
411 Ok(ddl)
412 }
413}
414
415fn generate_create_table_mysql(table: &TableDef) -> String {
417 let columns: Vec<String> = table
418 .columns
419 .iter()
420 .map(|c| {
421 format!(
422 "{} {}{}{}{}",
423 c.name,
424 c.sql_type,
425 if c.nullable { "" } else { " NOT NULL" },
426 if c.primary_key { " PRIMARY KEY" } else { "" },
427 c.default
428 .as_ref()
429 .map(|d| format!(" DEFAULT {}", d))
430 .unwrap_or_default()
431 )
432 })
433 .collect();
434
435 format!("CREATE TABLE {} ({})", table.name, columns.join(", "))
436}
437
438pub struct PgDdlGenerator;
440
441impl DdlGenerator for PgDdlGenerator {
442 fn generate(&self, diff: &SchemaDiff) -> Result<Vec<String>, DbError> {
443 let mut ddl = Vec::new();
444
445 for table in &diff.added_tables {
446 ddl.push(generate_create_table_mysql(table)); }
448
449 for (table, col) in &diff.added_columns {
450 ddl.push(format!(
451 "ALTER TABLE {} ADD COLUMN {} {}{}{}",
452 table,
453 col.name,
454 col.sql_type,
455 if col.nullable { "" } else { " NOT NULL" },
456 if col.primary_key { " PRIMARY KEY" } else { "" }
457 ));
458 }
459
460 for (table, _old, new) in &diff.type_changed_columns {
461 ddl.push(format!(
462 "ALTER TABLE {} ALTER COLUMN {} TYPE {}",
463 table, new.name, new.sql_type
464 ));
465 }
466
467 for (table, old, new) in &diff.renamed_columns {
468 ddl.push(format!(
469 "ALTER TABLE {} RENAME COLUMN {} TO {}",
470 table, old, new
471 ));
472 }
473
474 Ok(ddl)
475 }
476}
477
478pub struct SqliteDdlGenerator;
480
481impl DdlGenerator for SqliteDdlGenerator {
482 fn generate(&self, diff: &SchemaDiff) -> Result<Vec<String>, DbError> {
483 let mut ddl = Vec::new();
484
485 for table in &diff.added_tables {
486 ddl.push(generate_create_table_mysql(table));
487 }
488
489 for (table, col) in &diff.added_columns {
490 ddl.push(format!(
492 "ALTER TABLE {} ADD COLUMN {} {}{}",
493 table,
494 col.name,
495 col.sql_type,
496 col.default
497 .as_ref()
498 .map(|d| format!(" DEFAULT {}", d))
499 .unwrap_or_else(|| " DEFAULT NULL".to_string())
500 ));
501 }
502
503 if !diff.type_changed_columns.is_empty() {
505 return Err(DbError::Unsupported(
506 "SQLite does not support altering column type; table rebuild required".to_string(),
507 ));
508 }
509
510 for (table, old, new) in &diff.renamed_columns {
511 ddl.push(format!(
512 "ALTER TABLE {} RENAME COLUMN {} TO {}",
513 table, old, new
514 ));
515 }
516
517 Ok(ddl)
518 }
519}
520
521pub struct OracleDdlGenerator;
523
524impl DdlGenerator for OracleDdlGenerator {
525 fn generate(&self, diff: &SchemaDiff) -> Result<Vec<String>, DbError> {
526 let mut ddl = Vec::new();
527
528 for table in &diff.added_tables {
529 ddl.push(generate_create_table_mysql(table));
530 }
531
532 for (table, col) in &diff.added_columns {
533 ddl.push(format!(
534 "ALTER TABLE {} ADD ({} {}{}{})",
535 table,
536 col.name,
537 col.sql_type,
538 if col.nullable { "" } else { " NOT NULL" },
539 if col.primary_key { " PRIMARY KEY" } else { "" }
540 ));
541 }
542
543 for (table, _old, new) in &diff.type_changed_columns {
544 ddl.push(format!(
545 "ALTER TABLE {} MODIFY ({} {}{})",
546 table,
547 new.name,
548 new.sql_type,
549 if new.nullable { "" } else { " NOT NULL" }
550 ));
551 }
552
553 for (table, old, new) in &diff.renamed_columns {
554 ddl.push(format!(
555 "ALTER TABLE {} RENAME COLUMN {} TO {}",
556 table, old, new
557 ));
558 }
559
560 Ok(ddl)
561 }
562}
563
564pub struct MssqlDdlGenerator;
566
567impl DdlGenerator for MssqlDdlGenerator {
568 fn generate(&self, diff: &SchemaDiff) -> Result<Vec<String>, DbError> {
569 let mut ddl = Vec::new();
570
571 for table in &diff.added_tables {
572 ddl.push(generate_create_table_mysql(table));
573 }
574
575 for (table, col) in &diff.added_columns {
576 ddl.push(format!(
577 "ALTER TABLE {} ADD {} {}{}{}",
578 table,
579 col.name,
580 col.sql_type,
581 if col.nullable { "" } else { " NOT NULL" },
582 if col.primary_key { " PRIMARY KEY" } else { "" }
583 ));
584 }
585
586 for (table, _old, new) in &diff.type_changed_columns {
587 ddl.push(format!(
588 "ALTER TABLE {} ALTER COLUMN {} {}{}",
589 table,
590 new.name,
591 new.sql_type,
592 if new.nullable { "" } else { " NOT NULL" }
593 ));
594 }
595
596 for (table, old, new) in &diff.renamed_columns {
597 ddl.push(format!(
598 "EXEC sp_rename '{}.{}', '{}', 'COLUMN'",
599 table, old, new
600 ));
601 }
602
603 Ok(ddl)
604 }
605}
606
607pub struct SchemaSync {
613 entity_tables: Vec<TableDef>,
615 ddl_generator: Box<dyn DdlGenerator>,
617 rename_max_distance: usize,
619 rename_max_ratio: f64,
621}
622
623impl SchemaSync {
624 pub fn new(entity_tables: Vec<TableDef>) -> Self {
626 Self {
627 entity_tables,
628 ddl_generator: Box::new(MySqlDdlGenerator),
629 rename_max_distance: 2,
630 rename_max_ratio: 0.3,
631 }
632 }
633
634 pub fn with_generator(
636 entity_tables: Vec<TableDef>,
637 ddl_generator: Box<dyn DdlGenerator>,
638 ) -> Self {
639 Self {
640 entity_tables,
641 ddl_generator,
642 rename_max_distance: 2,
643 rename_max_ratio: 0.3,
644 }
645 }
646
647 pub fn with_rename_threshold(mut self, max_distance: usize, max_ratio: f64) -> Self {
649 self.rename_max_distance = max_distance;
650 self.rename_max_ratio = max_ratio;
651 self
652 }
653
654 pub async fn sync_dry_run(&self, conn: &mut dyn Connection) -> Result<Vec<String>, DbError> {
661 let db_tables = introspect(conn).await?;
662 let diff_result = self.diff_against(&db_tables);
663
664 if diff_result.has_destructive_changes() {
665 return Err(DbError::Internal(format!(
666 "DestructiveChangeDetected: dropped_tables={:?}, dropped_columns={:?}",
667 diff_result.dropped_tables, diff_result.dropped_columns
668 )));
669 }
670
671 self.ddl_generator.generate(&diff_result)
672 }
673
674 pub async fn sync(&self, conn: &mut dyn Connection) -> Result<SyncResult, DbError> {
681 let ddl = self.sync_dry_run(conn).await?;
682
683 if ddl.is_empty() {
684 return Ok(SyncResult {
685 affected_tables: Vec::new(),
686 executed_ddl: Vec::new(),
687 });
688 }
689
690 conn.begin_transaction().await?;
691
692 let mut executed = Vec::new();
693 for ddl_stmt in &ddl {
694 match conn.execute(ddl_stmt).await {
695 Ok(_) => executed.push(ddl_stmt.clone()),
696 Err(e) => {
697 let _ = conn.rollback().await;
698 return Err(DbError::Internal(format!(
699 "DDL execution failed: {} — SQL: {}",
700 e, ddl_stmt
701 )));
702 }
703 }
704 }
705
706 conn.commit().await?;
707
708 Ok(SyncResult {
709 affected_tables: self.entity_tables.iter().map(|t| t.name.clone()).collect(),
710 executed_ddl: executed,
711 })
712 }
713
714 pub fn diff_against(&self, db_tables: &[TableDef]) -> SchemaDiff {
716 let mut result = SchemaDiff::default();
717
718 let db_map: std::collections::HashMap<&str, &TableDef> =
719 db_tables.iter().map(|t| (t.name.as_str(), t)).collect();
720 let entity_map: std::collections::HashMap<&str, &TableDef> = self
721 .entity_tables
722 .iter()
723 .map(|t| (t.name.as_str(), t))
724 .collect();
725
726 for t in &self.entity_tables {
727 if !db_map.contains_key(t.name.as_str()) {
728 result.added_tables.push(t.clone());
729 }
730 }
731 for t in db_tables {
732 if !entity_map.contains_key(t.name.as_str()) {
733 result.dropped_tables.push(t.name.clone());
734 }
735 }
736
737 for entity_table in &self.entity_tables {
738 if let Some(db_table) = db_map.get(entity_table.name.as_str()) {
739 diff_columns_with_threshold(
740 &mut result,
741 entity_table,
742 db_table,
743 self.rename_max_distance,
744 self.rename_max_ratio,
745 );
746 }
747 }
748
749 result
750 }
751
752 pub async fn destructive_sync(
769 &self,
770 conn: &mut dyn Connection,
771 confirm: Confirm,
772 hooks: Option<&dyn DataMigrationHook>,
773 ) -> Result<DestructiveSyncResult, DbError> {
774 if confirm != Confirm::Yes {
775 return Err(DbError::InvalidInput(
776 "破坏性同步需要显式确认:请传入 Confirm::Yes".to_string(),
777 ));
778 }
779
780 let db_tables = introspect(conn).await?;
781 let diff_result = self.diff_against(&db_tables);
782 let ddl = self.ddl_generator.generate(&diff_result)?;
783
784 let mut destructive_ddl = Vec::new();
785 for (table, col) in &diff_result.dropped_columns {
786 destructive_ddl.push(format!("ALTER TABLE {} DROP COLUMN {}", table, col));
787 }
788 for (table, old, new) in &diff_result.renamed_columns {
789 destructive_ddl.push(format!(
790 "ALTER TABLE {} RENAME COLUMN {} TO {}",
791 table, old, new
792 ));
793 }
794
795 let all_ddl: Vec<String> = ddl.into_iter().chain(destructive_ddl).collect();
796 if all_ddl.is_empty() {
797 return Ok(DestructiveSyncResult {
798 executed_ddl: Vec::new(),
799 hooks_called: 0,
800 audit_entries: 0,
801 });
802 }
803
804 conn.begin_transaction().await?;
805
806 let mut executed = Vec::new();
807 let mut hooks_called = 0usize;
808
809 for ddl_stmt in &all_ddl {
810 if let Some(hook) = hooks {
811 if ddl_stmt.contains("DROP COLUMN") {
812 let parts: Vec<&str> = ddl_stmt.split_whitespace().collect();
813 if parts.len() >= 5 {
814 let table = parts[2];
815 let column = parts[4];
816 if let Err(e) = hook.before_drop_column(conn, table, column).await {
817 let _ = conn.rollback().await;
818 return Err(DbError::Hook(format!(
819 "before_drop_column 钩子失败: {}",
820 e
821 )));
822 }
823 hooks_called += 1;
824 }
825 } else if ddl_stmt.contains("RENAME COLUMN") {
826 let parts: Vec<&str> = ddl_stmt.split_whitespace().collect();
827 if parts.len() >= 6 {
828 let table = parts[2];
829 let old_name = parts[4];
830 let new_name = parts[6];
831 if let Err(e) = hook
832 .before_rename_column(conn, table, old_name, new_name)
833 .await
834 {
835 let _ = conn.rollback().await;
836 return Err(DbError::Hook(format!(
837 "before_rename_column 钩子失败: {}",
838 e
839 )));
840 }
841 hooks_called += 1;
842 }
843 }
844 }
845
846 match conn.execute(ddl_stmt).await {
847 Ok(_) => executed.push(ddl_stmt.clone()),
848 Err(e) => {
849 let _ = conn.rollback().await;
850 return Err(DbError::Internal(format!(
851 "DDL execution failed: {} — SQL: {}",
852 e, ddl_stmt
853 )));
854 }
855 }
856 }
857
858 conn.commit().await?;
859
860 Ok(DestructiveSyncResult {
861 audit_entries: executed.len(),
862 executed_ddl: executed,
863 hooks_called,
864 })
865 }
866}
867
868async fn introspect(conn: &mut dyn Connection) -> Result<Vec<TableDef>, DbError> {
876 let _ = conn;
879 Ok(Vec::new())
880}
881
882#[cfg(test)]
887mod tests {
888 use super::*;
889
890 fn make_column(name: &str, sql_type: &str) -> ColumnDef {
891 ColumnDef::new(name, sql_type, true, false, None)
892 }
893
894 fn make_table(name: &str, columns: Vec<ColumnDef>) -> TableDef {
895 TableDef::new(name, columns)
896 }
897
898 #[test]
899 fn test_diff_add_table() {
900 let entity = vec![make_table("users", vec![make_column("id", "BIGINT")])];
901 let db = vec![];
902
903 let result = diff(&entity, &db);
904
905 assert_eq!(result.added_tables.len(), 1);
906 assert_eq!(result.added_tables[0].name, "users");
907 }
908
909 #[test]
910 fn test_diff_drop_table() {
911 let entity = vec![];
912 let db = vec![make_table("legacy", vec![make_column("id", "BIGINT")])];
913
914 let result = diff(&entity, &db);
915
916 assert_eq!(result.dropped_tables.len(), 1);
917 assert_eq!(result.dropped_tables[0], "legacy");
918 assert!(result.has_destructive_changes());
919 }
920
921 #[test]
922 fn test_diff_add_column() {
923 let entity = vec![make_table(
924 "users",
925 vec![
926 make_column("id", "BIGINT"),
927 make_column("email", "VARCHAR(255)"),
928 ],
929 )];
930 let db = vec![make_table("users", vec![make_column("id", "BIGINT")])];
931
932 let result = diff(&entity, &db);
933
934 assert_eq!(result.added_columns.len(), 1);
935 assert_eq!(result.added_columns[0].0, "users");
936 assert_eq!(result.added_columns[0].1.name, "email");
937 }
938
939 #[test]
940 fn test_diff_drop_column() {
941 let entity = vec![make_table("users", vec![make_column("id", "BIGINT")])];
942 let db = vec![make_table(
943 "users",
944 vec![
945 make_column("id", "BIGINT"),
946 make_column("legacy_col", "TEXT"),
947 ],
948 )];
949
950 let result = diff(&entity, &db);
951
952 assert_eq!(result.dropped_columns.len(), 1);
953 assert_eq!(
954 result.dropped_columns[0],
955 ("users".to_string(), "legacy_col".to_string())
956 );
957 assert!(result.has_destructive_changes());
958 }
959
960 #[test]
961 fn test_diff_type_change() {
962 let entity = vec![make_table(
963 "users",
964 vec![
965 make_column("id", "BIGINT"),
966 make_column("name", "VARCHAR(255)"),
967 ],
968 )];
969 let db = vec![make_table(
970 "users",
971 vec![
972 make_column("id", "BIGINT"),
973 make_column("name", "VARCHAR(100)"),
974 ],
975 )];
976
977 let result = diff(&entity, &db);
978
979 assert_eq!(result.type_changed_columns.len(), 1);
980 assert_eq!(result.type_changed_columns[0].0, "users");
981 assert_eq!(result.type_changed_columns[0].1.sql_type, "VARCHAR(100)");
982 assert_eq!(result.type_changed_columns[0].2.sql_type, "VARCHAR(255)");
983 }
984
985 #[test]
986 fn test_diff_no_change() {
987 let entity = vec![make_table("users", vec![make_column("id", "BIGINT")])];
988 let db = vec![make_table("users", vec![make_column("id", "BIGINT")])];
989
990 let result = diff(&entity, &db);
991
992 assert!(result.is_empty());
993 }
994
995 #[test]
996 fn test_mysql_ddl_add_table() {
997 let diff_result = SchemaDiff {
998 added_tables: vec![make_table(
999 "users",
1000 vec![ColumnDef::new("id", "BIGINT", false, true, None)],
1001 )],
1002 ..Default::default()
1003 };
1004
1005 let ddl = MySqlDdlGenerator.generate(&diff_result).unwrap();
1006 assert_eq!(ddl.len(), 1);
1007 assert!(ddl[0].contains("CREATE TABLE users"));
1008 assert!(ddl[0].contains("id BIGINT NOT NULL PRIMARY KEY"));
1009 }
1010
1011 #[test]
1012 fn test_mysql_ddl_add_column() {
1013 let diff_result = SchemaDiff {
1014 added_columns: vec![(
1015 "users".to_string(),
1016 ColumnDef::new("email", "VARCHAR(255)", false, false, None),
1017 )],
1018 ..Default::default()
1019 };
1020
1021 let ddl = MySqlDdlGenerator.generate(&diff_result).unwrap();
1022 assert_eq!(ddl.len(), 1);
1023 assert!(ddl[0].contains("ALTER TABLE users ADD COLUMN email VARCHAR(255) NOT NULL"));
1024 }
1025
1026 #[test]
1027 fn test_pg_ddl_type_change() {
1028 let diff_result = SchemaDiff {
1029 type_changed_columns: vec![(
1030 "users".to_string(),
1031 ColumnDef::new("name", "VARCHAR(100)", true, false, None),
1032 ColumnDef::new("name", "VARCHAR(255)", true, false, None),
1033 )],
1034 ..Default::default()
1035 };
1036
1037 let ddl = PgDdlGenerator.generate(&diff_result).unwrap();
1038 assert_eq!(ddl.len(), 1);
1039 assert!(ddl[0].contains("ALTER TABLE users ALTER COLUMN name TYPE VARCHAR(255)"));
1040 }
1041
1042 #[test]
1043 fn test_sqlite_ddl_type_change_unsupported() {
1044 let diff_result = SchemaDiff {
1045 type_changed_columns: vec![(
1046 "users".to_string(),
1047 ColumnDef::new("name", "VARCHAR(100)", true, false, None),
1048 ColumnDef::new("name", "VARCHAR(255)", true, false, None),
1049 )],
1050 ..Default::default()
1051 };
1052
1053 let result = SqliteDdlGenerator.generate(&diff_result);
1054 assert!(result.is_err());
1055 }
1056
1057 #[test]
1058 fn test_oracle_ddl_add_column() {
1059 let diff_result = SchemaDiff {
1060 added_columns: vec![(
1061 "users".to_string(),
1062 ColumnDef::new("email", "VARCHAR2(255)", true, false, None),
1063 )],
1064 ..Default::default()
1065 };
1066
1067 let ddl = OracleDdlGenerator.generate(&diff_result).unwrap();
1068 assert_eq!(ddl.len(), 1);
1069 assert!(ddl[0].contains("ALTER TABLE users ADD (email VARCHAR2(255))"));
1070 }
1071
1072 #[test]
1073 fn test_mssql_ddl_rename() {
1074 let diff_result = SchemaDiff {
1075 renamed_columns: vec![(
1076 "users".to_string(),
1077 "old_name".to_string(),
1078 "new_name".to_string(),
1079 )],
1080 ..Default::default()
1081 };
1082
1083 let ddl = MssqlDdlGenerator.generate(&diff_result).unwrap();
1084 assert_eq!(ddl.len(), 1);
1085 assert!(ddl[0].contains("EXEC sp_rename 'users.old_name', 'new_name', 'COLUMN'"));
1086 }
1087
1088 #[test]
1089 fn test_destructive_change_detected() {
1090 let diff_result = SchemaDiff {
1091 dropped_columns: vec![("users".to_string(), "legacy".to_string())],
1092 ..Default::default()
1093 };
1094
1095 assert!(diff_result.has_destructive_changes());
1096 }
1097
1098 #[test]
1099 fn test_schema_diff_is_empty() {
1100 let empty = SchemaDiff::default();
1101 assert!(empty.is_empty());
1102
1103 let non_empty = SchemaDiff {
1104 added_columns: vec![(
1105 "users".to_string(),
1106 ColumnDef::new("email", "VARCHAR(255)", true, false, None),
1107 )],
1108 ..Default::default()
1109 };
1110 assert!(!non_empty.is_empty());
1111 }
1112
1113 #[test]
1114 fn test_sync_result() {
1115 let result = SyncResult {
1116 affected_tables: vec!["users".to_string()],
1117 executed_ddl: vec!["ALTER TABLE users ADD COLUMN email VARCHAR(255)".to_string()],
1118 };
1119 assert_eq!(result.affected_tables.len(), 1);
1120 assert_eq!(result.executed_ddl.len(), 1);
1121 }
1122
1123 #[test]
1124 fn test_levenshtein() {
1125 assert_eq!(levenshtein("user_name", "username"), 1);
1126 assert_eq!(levenshtein("name", "title"), 4);
1127 assert_eq!(levenshtein("abc", "abc"), 0);
1128 assert_eq!(levenshtein("", "abc"), 3);
1129 assert_eq!(levenshtein("abc", ""), 3);
1130 }
1131
1132 #[test]
1133 fn test_rename_detection() {
1134 let entity = vec![TableDef::new(
1135 "users",
1136 vec![
1137 ColumnDef::new("id", "BIGINT", false, true, None),
1138 ColumnDef::new("username", "VARCHAR(255)", true, false, None),
1139 ],
1140 )];
1141 let db = vec![TableDef::new(
1142 "users",
1143 vec![
1144 ColumnDef::new("id", "BIGINT", false, true, None),
1145 ColumnDef::new("user_name", "VARCHAR(255)", true, false, None),
1146 ],
1147 )];
1148 let diff_result = diff(&entity, &db);
1149 assert!(!diff_result.renamed_columns.is_empty());
1150 assert_eq!(
1151 diff_result.renamed_columns[0],
1152 (
1153 "users".to_string(),
1154 "user_name".to_string(),
1155 "username".to_string()
1156 )
1157 );
1158 assert!(diff_result.dropped_columns.is_empty());
1159 assert!(diff_result.added_columns.is_empty());
1160 }
1161
1162 #[test]
1163 fn test_rename_no_match_different_type() {
1164 let entity = vec![TableDef::new(
1165 "users",
1166 vec![
1167 ColumnDef::new("id", "BIGINT", false, true, None),
1168 ColumnDef::new("username", "INT", true, false, None),
1169 ],
1170 )];
1171 let db = vec![TableDef::new(
1172 "users",
1173 vec![
1174 ColumnDef::new("id", "BIGINT", false, true, None),
1175 ColumnDef::new("user_name", "VARCHAR(255)", true, false, None),
1176 ],
1177 )];
1178 let diff_result = diff(&entity, &db);
1179 assert!(diff_result.renamed_columns.is_empty());
1180 assert!(!diff_result.dropped_columns.is_empty());
1181 assert!(!diff_result.added_columns.is_empty());
1182 }
1183
1184 #[test]
1185 fn test_rename_no_match_distance_too_large() {
1186 let entity = vec![TableDef::new(
1187 "users",
1188 vec![
1189 ColumnDef::new("id", "BIGINT", false, true, None),
1190 ColumnDef::new("title", "VARCHAR(255)", true, false, None),
1191 ],
1192 )];
1193 let db = vec![TableDef::new(
1194 "users",
1195 vec![
1196 ColumnDef::new("id", "BIGINT", false, true, None),
1197 ColumnDef::new("name", "VARCHAR(255)", true, false, None),
1198 ],
1199 )];
1200 let diff_result = diff(&entity, &db);
1201 assert!(diff_result.renamed_columns.is_empty());
1202 }
1203
1204 #[test]
1205 fn test_confirm_enum() {
1206 assert_eq!(Confirm::Yes, Confirm::Yes);
1207 assert_ne!(Confirm::Yes, Confirm::No);
1208 }
1209
1210 #[test]
1211 fn test_destructive_sync_result() {
1212 let result = DestructiveSyncResult {
1213 executed_ddl: vec!["ALTER TABLE users DROP COLUMN old_col".to_string()],
1214 hooks_called: 1,
1215 audit_entries: 1,
1216 };
1217 assert_eq!(result.executed_ddl.len(), 1);
1218 assert_eq!(result.hooks_called, 1);
1219 }
1220
1221 #[test]
1222 fn test_schema_sync_with_rename_threshold() {
1223 let sync = SchemaSync::new(vec![]).with_rename_threshold(5, 0.5);
1224 assert_eq!(sync.rename_max_distance, 5);
1225 assert!((sync.rename_max_ratio - 0.5).abs() < f64::EPSILON);
1226 }
1227}