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
141pub fn diff(entity: &[TableDef], db: &[TableDef]) -> SchemaDiff {
156 let mut result = SchemaDiff::default();
157
158 let db_map: std::collections::HashMap<&str, &TableDef> =
159 db.iter().map(|t| (t.name.as_str(), t)).collect();
160 let entity_map: std::collections::HashMap<&str, &TableDef> =
161 entity.iter().map(|t| (t.name.as_str(), t)).collect();
162
163 for t in entity {
165 if !db_map.contains_key(t.name.as_str()) {
166 result.added_tables.push(t.clone());
167 }
168 }
169 for t in db {
170 if !entity_map.contains_key(t.name.as_str()) {
171 result.dropped_tables.push(t.name.clone());
172 }
173 }
174
175 for entity_table in entity {
177 if let Some(db_table) = db_map.get(entity_table.name.as_str()) {
178 diff_columns(&mut result, entity_table, db_table);
179 }
180 }
181
182 result
183}
184
185fn diff_columns(result: &mut SchemaDiff, entity: &TableDef, db: &TableDef) {
187 let db_col_map: std::collections::HashMap<&str, &ColumnDef> =
188 db.columns.iter().map(|c| (c.name.as_str(), c)).collect();
189 let entity_col_map: std::collections::HashMap<&str, &ColumnDef> = entity
190 .columns
191 .iter()
192 .map(|c| (c.name.as_str(), c))
193 .collect();
194
195 for col in &entity.columns {
197 if !db_col_map.contains_key(col.name.as_str()) {
198 result
199 .added_columns
200 .push((entity.name.clone(), col.clone()));
201 }
202 }
203
204 for col in &db.columns {
206 if !entity_col_map.contains_key(col.name.as_str()) {
207 result
208 .dropped_columns
209 .push((entity.name.clone(), col.name.clone()));
210 }
211 }
212
213 for entity_col in &entity.columns {
215 if let Some(db_col) = db_col_map.get(entity_col.name.as_str()) {
216 if entity_col.sql_type != db_col.sql_type || entity_col.nullable != db_col.nullable {
217 result.type_changed_columns.push((
218 entity.name.clone(),
219 (*db_col).clone(),
220 entity_col.clone(),
221 ));
222 }
223 }
224 }
225}
226
227pub trait DdlGenerator: Send + Sync {
233 fn generate(&self, diff: &SchemaDiff) -> Result<Vec<String>, DbError>;
237}
238
239pub struct MySqlDdlGenerator;
241
242impl DdlGenerator for MySqlDdlGenerator {
243 fn generate(&self, diff: &SchemaDiff) -> Result<Vec<String>, DbError> {
244 let mut ddl = Vec::new();
245
246 for table in &diff.added_tables {
248 ddl.push(generate_create_table_mysql(table));
249 }
250
251 for (table, col) in &diff.added_columns {
253 ddl.push(format!(
254 "ALTER TABLE {} ADD COLUMN {} {}{}{}",
255 table,
256 col.name,
257 col.sql_type,
258 if col.nullable { "" } else { " NOT NULL" },
259 if col.primary_key { " PRIMARY KEY" } else { "" }
260 ));
261 }
262
263 for (table, _old, new) in &diff.type_changed_columns {
265 ddl.push(format!(
266 "ALTER TABLE {} MODIFY COLUMN {} {}{}",
267 table,
268 new.name,
269 new.sql_type,
270 if new.nullable { "" } else { " NOT NULL" }
271 ));
272 }
273
274 for (table, old, new) in &diff.renamed_columns {
276 ddl.push(format!(
277 "ALTER TABLE {} RENAME COLUMN {} TO {}",
278 table, old, new
279 ));
280 }
281
282 Ok(ddl)
283 }
284}
285
286fn generate_create_table_mysql(table: &TableDef) -> String {
288 let columns: Vec<String> = table
289 .columns
290 .iter()
291 .map(|c| {
292 format!(
293 "{} {}{}{}{}",
294 c.name,
295 c.sql_type,
296 if c.nullable { "" } else { " NOT NULL" },
297 if c.primary_key { " PRIMARY KEY" } else { "" },
298 c.default
299 .as_ref()
300 .map(|d| format!(" DEFAULT {}", d))
301 .unwrap_or_default()
302 )
303 })
304 .collect();
305
306 format!("CREATE TABLE {} ({})", table.name, columns.join(", "))
307}
308
309pub struct PgDdlGenerator;
311
312impl DdlGenerator for PgDdlGenerator {
313 fn generate(&self, diff: &SchemaDiff) -> Result<Vec<String>, DbError> {
314 let mut ddl = Vec::new();
315
316 for table in &diff.added_tables {
317 ddl.push(generate_create_table_mysql(table)); }
319
320 for (table, col) in &diff.added_columns {
321 ddl.push(format!(
322 "ALTER TABLE {} ADD COLUMN {} {}{}{}",
323 table,
324 col.name,
325 col.sql_type,
326 if col.nullable { "" } else { " NOT NULL" },
327 if col.primary_key { " PRIMARY KEY" } else { "" }
328 ));
329 }
330
331 for (table, _old, new) in &diff.type_changed_columns {
332 ddl.push(format!(
333 "ALTER TABLE {} ALTER COLUMN {} TYPE {}",
334 table, new.name, new.sql_type
335 ));
336 }
337
338 for (table, old, new) in &diff.renamed_columns {
339 ddl.push(format!(
340 "ALTER TABLE {} RENAME COLUMN {} TO {}",
341 table, old, new
342 ));
343 }
344
345 Ok(ddl)
346 }
347}
348
349pub struct SqliteDdlGenerator;
351
352impl DdlGenerator for SqliteDdlGenerator {
353 fn generate(&self, diff: &SchemaDiff) -> Result<Vec<String>, DbError> {
354 let mut ddl = Vec::new();
355
356 for table in &diff.added_tables {
357 ddl.push(generate_create_table_mysql(table));
358 }
359
360 for (table, col) in &diff.added_columns {
361 ddl.push(format!(
363 "ALTER TABLE {} ADD COLUMN {} {}{}",
364 table,
365 col.name,
366 col.sql_type,
367 col.default
368 .as_ref()
369 .map(|d| format!(" DEFAULT {}", d))
370 .unwrap_or_else(|| " DEFAULT NULL".to_string())
371 ));
372 }
373
374 if !diff.type_changed_columns.is_empty() {
376 return Err(DbError::Unsupported(
377 "SQLite does not support altering column type; table rebuild required".to_string(),
378 ));
379 }
380
381 for (table, old, new) in &diff.renamed_columns {
382 ddl.push(format!(
383 "ALTER TABLE {} RENAME COLUMN {} TO {}",
384 table, old, new
385 ));
386 }
387
388 Ok(ddl)
389 }
390}
391
392pub struct OracleDdlGenerator;
394
395impl DdlGenerator for OracleDdlGenerator {
396 fn generate(&self, diff: &SchemaDiff) -> Result<Vec<String>, DbError> {
397 let mut ddl = Vec::new();
398
399 for table in &diff.added_tables {
400 ddl.push(generate_create_table_mysql(table));
401 }
402
403 for (table, col) in &diff.added_columns {
404 ddl.push(format!(
405 "ALTER TABLE {} ADD ({} {}{}{})",
406 table,
407 col.name,
408 col.sql_type,
409 if col.nullable { "" } else { " NOT NULL" },
410 if col.primary_key { " PRIMARY KEY" } else { "" }
411 ));
412 }
413
414 for (table, _old, new) in &diff.type_changed_columns {
415 ddl.push(format!(
416 "ALTER TABLE {} MODIFY ({} {}{})",
417 table,
418 new.name,
419 new.sql_type,
420 if new.nullable { "" } else { " NOT NULL" }
421 ));
422 }
423
424 for (table, old, new) in &diff.renamed_columns {
425 ddl.push(format!(
426 "ALTER TABLE {} RENAME COLUMN {} TO {}",
427 table, old, new
428 ));
429 }
430
431 Ok(ddl)
432 }
433}
434
435pub struct MssqlDdlGenerator;
437
438impl DdlGenerator for MssqlDdlGenerator {
439 fn generate(&self, diff: &SchemaDiff) -> Result<Vec<String>, DbError> {
440 let mut ddl = Vec::new();
441
442 for table in &diff.added_tables {
443 ddl.push(generate_create_table_mysql(table));
444 }
445
446 for (table, col) in &diff.added_columns {
447 ddl.push(format!(
448 "ALTER TABLE {} ADD {} {}{}{}",
449 table,
450 col.name,
451 col.sql_type,
452 if col.nullable { "" } else { " NOT NULL" },
453 if col.primary_key { " PRIMARY KEY" } else { "" }
454 ));
455 }
456
457 for (table, _old, new) in &diff.type_changed_columns {
458 ddl.push(format!(
459 "ALTER TABLE {} ALTER COLUMN {} {}{}",
460 table,
461 new.name,
462 new.sql_type,
463 if new.nullable { "" } else { " NOT NULL" }
464 ));
465 }
466
467 for (table, old, new) in &diff.renamed_columns {
468 ddl.push(format!(
469 "EXEC sp_rename '{}.{}', '{}', 'COLUMN'",
470 table, old, new
471 ));
472 }
473
474 Ok(ddl)
475 }
476}
477
478pub struct SchemaSync {
484 entity_tables: Vec<TableDef>,
486 ddl_generator: Box<dyn DdlGenerator>,
488}
489
490impl SchemaSync {
491 pub fn new(entity_tables: Vec<TableDef>) -> Self {
493 Self {
494 entity_tables,
495 ddl_generator: Box::new(MySqlDdlGenerator),
496 }
497 }
498
499 pub fn with_generator(
501 entity_tables: Vec<TableDef>,
502 ddl_generator: Box<dyn DdlGenerator>,
503 ) -> Self {
504 Self {
505 entity_tables,
506 ddl_generator,
507 }
508 }
509
510 pub async fn sync_dry_run(&self, conn: &mut dyn Connection) -> Result<Vec<String>, DbError> {
517 let db_tables = introspect(conn).await?;
518 let diff_result = diff(&self.entity_tables, &db_tables);
519
520 if diff_result.has_destructive_changes() {
521 return Err(DbError::Internal(format!(
522 "DestructiveChangeDetected: dropped_tables={:?}, dropped_columns={:?}",
523 diff_result.dropped_tables, diff_result.dropped_columns
524 )));
525 }
526
527 self.ddl_generator.generate(&diff_result)
528 }
529
530 pub async fn sync(&self, conn: &mut dyn Connection) -> Result<SyncResult, DbError> {
537 let ddl = self.sync_dry_run(conn).await?;
538
539 if ddl.is_empty() {
540 return Ok(SyncResult {
541 affected_tables: Vec::new(),
542 executed_ddl: Vec::new(),
543 });
544 }
545
546 conn.begin_transaction().await?;
547
548 let mut executed = Vec::new();
549 for ddl_stmt in &ddl {
550 match conn.execute(ddl_stmt).await {
551 Ok(_) => executed.push(ddl_stmt.clone()),
552 Err(e) => {
553 let _ = conn.rollback().await;
554 return Err(DbError::Internal(format!(
555 "DDL execution failed: {} — SQL: {}",
556 e, ddl_stmt
557 )));
558 }
559 }
560 }
561
562 conn.commit().await?;
563
564 Ok(SyncResult {
565 affected_tables: self.entity_tables.iter().map(|t| t.name.clone()).collect(),
566 executed_ddl: executed,
567 })
568 }
569
570 pub fn diff_against(&self, db_tables: &[TableDef]) -> SchemaDiff {
572 diff(&self.entity_tables, db_tables)
573 }
574}
575
576async fn introspect(conn: &mut dyn Connection) -> Result<Vec<TableDef>, DbError> {
584 let _ = conn;
587 Ok(Vec::new())
588}
589
590#[cfg(test)]
595mod tests {
596 use super::*;
597
598 fn make_column(name: &str, sql_type: &str) -> ColumnDef {
599 ColumnDef::new(name, sql_type, true, false, None)
600 }
601
602 fn make_table(name: &str, columns: Vec<ColumnDef>) -> TableDef {
603 TableDef::new(name, columns)
604 }
605
606 #[test]
607 fn test_diff_add_table() {
608 let entity = vec![make_table("users", vec![make_column("id", "BIGINT")])];
609 let db = vec![];
610
611 let result = diff(&entity, &db);
612
613 assert_eq!(result.added_tables.len(), 1);
614 assert_eq!(result.added_tables[0].name, "users");
615 }
616
617 #[test]
618 fn test_diff_drop_table() {
619 let entity = vec![];
620 let db = vec![make_table("legacy", vec![make_column("id", "BIGINT")])];
621
622 let result = diff(&entity, &db);
623
624 assert_eq!(result.dropped_tables.len(), 1);
625 assert_eq!(result.dropped_tables[0], "legacy");
626 assert!(result.has_destructive_changes());
627 }
628
629 #[test]
630 fn test_diff_add_column() {
631 let entity = vec![make_table(
632 "users",
633 vec![
634 make_column("id", "BIGINT"),
635 make_column("email", "VARCHAR(255)"),
636 ],
637 )];
638 let db = vec![make_table("users", vec![make_column("id", "BIGINT")])];
639
640 let result = diff(&entity, &db);
641
642 assert_eq!(result.added_columns.len(), 1);
643 assert_eq!(result.added_columns[0].0, "users");
644 assert_eq!(result.added_columns[0].1.name, "email");
645 }
646
647 #[test]
648 fn test_diff_drop_column() {
649 let entity = vec![make_table("users", vec![make_column("id", "BIGINT")])];
650 let db = vec![make_table(
651 "users",
652 vec![
653 make_column("id", "BIGINT"),
654 make_column("legacy_col", "TEXT"),
655 ],
656 )];
657
658 let result = diff(&entity, &db);
659
660 assert_eq!(result.dropped_columns.len(), 1);
661 assert_eq!(
662 result.dropped_columns[0],
663 ("users".to_string(), "legacy_col".to_string())
664 );
665 assert!(result.has_destructive_changes());
666 }
667
668 #[test]
669 fn test_diff_type_change() {
670 let entity = vec![make_table(
671 "users",
672 vec![
673 make_column("id", "BIGINT"),
674 make_column("name", "VARCHAR(255)"),
675 ],
676 )];
677 let db = vec![make_table(
678 "users",
679 vec![
680 make_column("id", "BIGINT"),
681 make_column("name", "VARCHAR(100)"),
682 ],
683 )];
684
685 let result = diff(&entity, &db);
686
687 assert_eq!(result.type_changed_columns.len(), 1);
688 assert_eq!(result.type_changed_columns[0].0, "users");
689 assert_eq!(result.type_changed_columns[0].1.sql_type, "VARCHAR(100)");
690 assert_eq!(result.type_changed_columns[0].2.sql_type, "VARCHAR(255)");
691 }
692
693 #[test]
694 fn test_diff_no_change() {
695 let entity = vec![make_table("users", vec![make_column("id", "BIGINT")])];
696 let db = vec![make_table("users", vec![make_column("id", "BIGINT")])];
697
698 let result = diff(&entity, &db);
699
700 assert!(result.is_empty());
701 }
702
703 #[test]
704 fn test_mysql_ddl_add_table() {
705 let diff_result = SchemaDiff {
706 added_tables: vec![make_table(
707 "users",
708 vec![ColumnDef::new("id", "BIGINT", false, true, None)],
709 )],
710 ..Default::default()
711 };
712
713 let ddl = MySqlDdlGenerator.generate(&diff_result).unwrap();
714 assert_eq!(ddl.len(), 1);
715 assert!(ddl[0].contains("CREATE TABLE users"));
716 assert!(ddl[0].contains("id BIGINT NOT NULL PRIMARY KEY"));
717 }
718
719 #[test]
720 fn test_mysql_ddl_add_column() {
721 let diff_result = SchemaDiff {
722 added_columns: vec![(
723 "users".to_string(),
724 ColumnDef::new("email", "VARCHAR(255)", false, false, None),
725 )],
726 ..Default::default()
727 };
728
729 let ddl = MySqlDdlGenerator.generate(&diff_result).unwrap();
730 assert_eq!(ddl.len(), 1);
731 assert!(ddl[0].contains("ALTER TABLE users ADD COLUMN email VARCHAR(255) NOT NULL"));
732 }
733
734 #[test]
735 fn test_pg_ddl_type_change() {
736 let diff_result = SchemaDiff {
737 type_changed_columns: vec![(
738 "users".to_string(),
739 ColumnDef::new("name", "VARCHAR(100)", true, false, None),
740 ColumnDef::new("name", "VARCHAR(255)", true, false, None),
741 )],
742 ..Default::default()
743 };
744
745 let ddl = PgDdlGenerator.generate(&diff_result).unwrap();
746 assert_eq!(ddl.len(), 1);
747 assert!(ddl[0].contains("ALTER TABLE users ALTER COLUMN name TYPE VARCHAR(255)"));
748 }
749
750 #[test]
751 fn test_sqlite_ddl_type_change_unsupported() {
752 let diff_result = SchemaDiff {
753 type_changed_columns: vec![(
754 "users".to_string(),
755 ColumnDef::new("name", "VARCHAR(100)", true, false, None),
756 ColumnDef::new("name", "VARCHAR(255)", true, false, None),
757 )],
758 ..Default::default()
759 };
760
761 let result = SqliteDdlGenerator.generate(&diff_result);
762 assert!(result.is_err());
763 }
764
765 #[test]
766 fn test_oracle_ddl_add_column() {
767 let diff_result = SchemaDiff {
768 added_columns: vec![(
769 "users".to_string(),
770 ColumnDef::new("email", "VARCHAR2(255)", true, false, None),
771 )],
772 ..Default::default()
773 };
774
775 let ddl = OracleDdlGenerator.generate(&diff_result).unwrap();
776 assert_eq!(ddl.len(), 1);
777 assert!(ddl[0].contains("ALTER TABLE users ADD (email VARCHAR2(255))"));
778 }
779
780 #[test]
781 fn test_mssql_ddl_rename() {
782 let diff_result = SchemaDiff {
783 renamed_columns: vec![(
784 "users".to_string(),
785 "old_name".to_string(),
786 "new_name".to_string(),
787 )],
788 ..Default::default()
789 };
790
791 let ddl = MssqlDdlGenerator.generate(&diff_result).unwrap();
792 assert_eq!(ddl.len(), 1);
793 assert!(ddl[0].contains("EXEC sp_rename 'users.old_name', 'new_name', 'COLUMN'"));
794 }
795
796 #[test]
797 fn test_destructive_change_detected() {
798 let diff_result = SchemaDiff {
799 dropped_columns: vec![("users".to_string(), "legacy".to_string())],
800 ..Default::default()
801 };
802
803 assert!(diff_result.has_destructive_changes());
804 }
805
806 #[test]
807 fn test_schema_diff_is_empty() {
808 let empty = SchemaDiff::default();
809 assert!(empty.is_empty());
810
811 let non_empty = SchemaDiff {
812 added_columns: vec![(
813 "users".to_string(),
814 ColumnDef::new("email", "VARCHAR(255)", true, false, None),
815 )],
816 ..Default::default()
817 };
818 assert!(!non_empty.is_empty());
819 }
820
821 #[test]
822 fn test_sync_result() {
823 let result = SyncResult {
824 affected_tables: vec!["users".to_string()],
825 executed_ddl: vec!["ALTER TABLE users ADD COLUMN email VARCHAR(255)".to_string()],
826 };
827 assert_eq!(result.affected_tables.len(), 1);
828 assert_eq!(result.executed_ddl.len(), 1);
829 }
830}