Skip to main content

sz_orm_core/
schema_sync.rs

1//! Schema Sync — 自动结构同步
2//!
3//! # 概述
4//!
5//! 比较实体定义与 DB 现有表结构差异,自动生成并执行 DDL(CREATE/ALTER TABLE)。
6//! **禁止破坏性 DDL**(ADR-v2.1.0-004):不生成 DROP TABLE / DROP COLUMN。
7//!
8//! # 工作流
9//!
10//! 1. `introspect(conn)` → 读取 DB 现有表结构
11//! 2. `diff(entity, db)` → 计算 6 类变更
12//! 3. 检查破坏性变更 → 若有则返回 `Err(DestructiveChangeDetected)`
13//! 4. `generate(diff)` → 生成 DDL 语句
14//! 5. `sync(conn)` → 事务内执行 DDL
15//!
16//! # 示例
17//!
18//! ```ignore
19//! use sz_orm_core::schema_sync::{SchemaSync, TableDef, ColumnDef};
20//!
21//! let entity_tables = vec![
22//!     TableDef::new("users", vec![
23//!         ColumnDef::new("id", "BIGINT", false, true, None),
24//!         ColumnDef::new("email", "VARCHAR(255)", false, false, None),
25//!     ]),
26//! ];
27//!
28//! let sync = SchemaSync::new(entity_tables);
29//! let ddl = sync.sync_dry_run(&mut conn).await?;
30//! // ddl: ["ALTER TABLE users ADD COLUMN email VARCHAR(255)"]
31//! ```
32
33use crate::pool::Connection;
34use crate::DbError;
35
36// ============================================================================
37// 类型定义
38// ============================================================================
39
40/// 列定义
41#[derive(Debug, Clone, PartialEq)]
42pub struct ColumnDef {
43    /// 列名
44    pub name: String,
45    /// SQL 类型(如 "VARCHAR(255)"、"BIGINT")
46    pub sql_type: String,
47    /// 是否允许 NULL
48    pub nullable: bool,
49    /// 是否主键
50    pub primary_key: bool,
51    /// 默认值(None 表示无默认值)
52    pub default: Option<String>,
53}
54
55impl ColumnDef {
56    /// 创建新的列定义
57    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/// 表定义
75#[derive(Debug, Clone, PartialEq)]
76pub struct TableDef {
77    /// 表名
78    pub name: String,
79    /// 列定义列表
80    pub columns: Vec<ColumnDef>,
81}
82
83impl TableDef {
84    /// 创建新的表定义
85    pub fn new(name: impl Into<String>, columns: Vec<ColumnDef>) -> Self {
86        Self {
87            name: name.into(),
88            columns,
89        }
90    }
91
92    /// 按列名查找列
93    pub fn get_column(&self, name: &str) -> Option<&ColumnDef> {
94        self.columns.iter().find(|c| c.name == name)
95    }
96}
97
98/// Schema 差异
99#[derive(Debug, Clone, Default)]
100pub struct SchemaDiff {
101    /// 新增的表(实体有,DB 无)
102    pub added_tables: Vec<TableDef>,
103    /// 删除的表(DB 有,实体无)— 破坏性,仅记录不执行
104    pub dropped_tables: Vec<String>,
105    /// 新增的列(实体有,DB 无)
106    pub added_columns: Vec<(String, ColumnDef)>,
107    /// 删除的列(DB 有,实体无)— 破坏性,仅记录不执行
108    pub dropped_columns: Vec<(String, String)>,
109    /// 类型变更的列
110    pub type_changed_columns: Vec<(String, ColumnDef, ColumnDef)>,
111    /// 重命名的列(启发式:同位置不同名)
112    pub renamed_columns: Vec<(String, String, String)>,
113}
114
115impl SchemaDiff {
116    /// 是否为空(无任何变更)
117    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    /// 是否含破坏性变更
127    pub fn has_destructive_changes(&self) -> bool {
128        !self.dropped_tables.is_empty() || !self.dropped_columns.is_empty()
129    }
130}
131
132/// 同步结果
133#[derive(Debug, Clone)]
134pub struct SyncResult {
135    /// 受影响表名列表
136    pub affected_tables: Vec<String>,
137    /// 执行的 DDL 语句列表
138    pub executed_ddl: Vec<String>,
139}
140
141// ============================================================================
142// diff 纯函数
143// ============================================================================
144
145/// 比较实体定义与 DB 现有表结构,输出 6 类变更
146///
147/// # 参数
148///
149/// - `entity`:实体定义的表结构列表
150/// - `db`:DB 现有的表结构列表
151///
152/// # 返回
153///
154/// `SchemaDiff` 含 6 类变更
155pub 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    // 新增/删除的表
164    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    // 列级 diff(仅比较两边都存在的表)
176    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
185/// 比较两个表的列差异
186fn 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    // 新增的列
196    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    // 删除的列
205    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    // 类型变更
214    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
227// ============================================================================
228// DDL 生成
229// ============================================================================
230
231/// DDL 生成器 trait
232pub trait DdlGenerator: Send + Sync {
233    /// 根据 SchemaDiff 生成 DDL 语句列表
234    ///
235    /// **不生成破坏性 DDL**(DROP TABLE / DROP COLUMN)。
236    fn generate(&self, diff: &SchemaDiff) -> Result<Vec<String>, DbError>;
237}
238
239/// MySQL DDL 生成器
240pub 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        // 新增表
247        for table in &diff.added_tables {
248            ddl.push(generate_create_table_mysql(table));
249        }
250
251        // 新增列
252        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        // 类型变更
264        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        // 重命名
275        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
286/// 生成 MySQL CREATE TABLE
287fn 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
309/// PostgreSQL DDL 生成器
310pub 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)); // PG 语法与 MySQL 类似
318        }
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
349/// SQLite DDL 生成器
350pub 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            // SQLite 新增列必须允许 NULL 或有默认值
362            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        // SQLite 不支持 ALTER COLUMN TYPE,返回错误
375        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
392/// Oracle DDL 生成器
393pub 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
435/// MSSQL DDL 生成器
436pub 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
478// ============================================================================
479// SchemaSync 协调器
480// ============================================================================
481
482/// Schema Sync 协调器
483pub struct SchemaSync {
484    /// 实体定义的表结构
485    entity_tables: Vec<TableDef>,
486    /// DDL 生成器
487    ddl_generator: Box<dyn DdlGenerator>,
488}
489
490impl SchemaSync {
491    /// 创建 SchemaSync(按方言选择 DDL 生成器)
492    pub fn new(entity_tables: Vec<TableDef>) -> Self {
493        Self {
494            entity_tables,
495            ddl_generator: Box::new(MySqlDdlGenerator),
496        }
497    }
498
499    /// 创建 SchemaSync(指定 DDL 生成器)
500    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    /// 干运行:计算 DDL 但不执行
511    ///
512    /// 1. introspect → 读取 DB 现有表结构
513    /// 2. diff → 计算变更
514    /// 3. 检查破坏性变更 → 若有则返回 `Err(DestructiveChangeDetected)`
515    /// 4. generate → 生成 DDL
516    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    /// 执行同步:事务内执行 DDL
531    ///
532    /// 1. sync_dry_run → 获取 DDL
533    /// 2. begin_transaction
534    /// 3. 逐条执行 DDL
535    /// 4. commit / rollback
536    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    /// 仅计算 diff(不连接 DB)
571    pub fn diff_against(&self, db_tables: &[TableDef]) -> SchemaDiff {
572        diff(&self.entity_tables, db_tables)
573    }
574}
575
576// ============================================================================
577// 内省(简化版:从 DB 读取表结构)
578// ============================================================================
579
580/// 从 DB 读取现有表结构
581///
582/// 简化实现:返回空列表(实际应由各方言 introspector 实现)
583async fn introspect(conn: &mut dyn Connection) -> Result<Vec<TableDef>, DbError> {
584    // 简化:查询 information_schema.tables 获取表列表
585    // 实际实现应由各方言 introspector 提供
586    let _ = conn;
587    Ok(Vec::new())
588}
589
590// ============================================================================
591// 单元测试
592// ============================================================================
593
594#[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}