Skip to main content

sz_orm_core/
migration.rs

1//! Migration system
2//!
3//! Provides database schema migration management
4
5use crate::db_type::DbType;
6use crate::error::DbError;
7use std::path::PathBuf;
8
9pub struct Migration {
10    pub version: String,
11    pub name: String,
12    pub sql_up: String,
13    pub sql_down: String,
14    pub batch: i32,
15    pub executed_at: Option<chrono::DateTime<chrono::Utc>>,
16}
17
18impl Migration {
19    pub fn new(version: &str, name: &str, sql_up: &str, sql_down: &str) -> Self {
20        Self {
21            version: version.to_string(),
22            name: name.to_string(),
23            sql_up: sql_up.to_string(),
24            sql_down: sql_down.to_string(),
25            batch: 0,
26            executed_at: None,
27        }
28    }
29
30    pub fn with_batch(mut self, batch: i32) -> Self {
31        self.batch = batch;
32        self
33    }
34
35    pub fn with_executed_at(mut self, time: chrono::DateTime<chrono::Utc>) -> Self {
36        self.executed_at = Some(time);
37        self
38    }
39}
40
41impl std::fmt::Debug for Migration {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        f.debug_struct("Migration")
44            .field("version", &self.version)
45            .field("name", &self.name)
46            .field("batch", &self.batch)
47            .finish()
48    }
49}
50
51pub trait MigrationResolver: Send + Sync {
52    fn resolve(&self, db_type: DbType) -> Result<Vec<Migration>, DbError>;
53}
54
55pub struct FileMigrationResolver {
56    pub path: PathBuf,
57}
58
59impl FileMigrationResolver {
60    pub fn new(path: PathBuf) -> Self {
61        Self { path }
62    }
63}
64
65impl MigrationResolver for FileMigrationResolver {
66    fn resolve(&self, db_type: DbType) -> Result<Vec<Migration>, DbError> {
67        let mut migrations = Vec::new();
68
69        // 读取迁移目录
70        let entries = match std::fs::read_dir(&self.path) {
71            Ok(entries) => entries,
72            Err(e) => {
73                return Err(DbError::MigrationError(format!(
74                    "Cannot read migration directory {}: {}",
75                    self.path.display(),
76                    e
77                )));
78            }
79        };
80
81        let _ = db_type; // 当前实现不区分数据库类型
82
83        // 收集所有 .sql 文件
84        let mut sql_files: Vec<std::path::PathBuf> = Vec::new();
85        for entry in entries {
86            let entry = entry.map_err(|e| {
87                DbError::MigrationError(format!("Cannot read directory entry: {}", e))
88            })?;
89            let path = entry.path();
90            if path.extension().and_then(|s| s.to_str()) == Some("sql") {
91                sql_files.push(path);
92            }
93        }
94
95        // 按文件名排序
96        sql_files.sort();
97
98        // 解析文件名格式:<version>_<name>_up.sql 或 <version>_<name>_down.sql
99        // 也支持简单的 <name>.sql(不区分 up/down)
100        let mut version_map: std::collections::HashMap<
101            String,
102            (Option<String>, Option<String>, String),
103        > = std::collections::HashMap::new();
104
105        for path in sql_files {
106            let filename = match path.file_stem().and_then(|s| s.to_str()) {
107                Some(name) => name.to_string(),
108                None => continue,
109            };
110
111            let content = std::fs::read_to_string(&path).map_err(|e| {
112                DbError::MigrationError(format!(
113                    "Cannot read migration file {}: {}",
114                    path.display(),
115                    e
116                ))
117            })?;
118
119            // 尝试解析文件名
120            if filename.ends_with("_up") {
121                let base = &filename[..filename.len() - 3];
122                let (version, name) = parse_migration_filename(base);
123                let entry = version_map
124                    .entry(version.clone())
125                    .or_insert((None, None, name));
126                entry.0 = Some(content);
127            } else if filename.ends_with("_down") {
128                let base = &filename[..filename.len() - 5];
129                let (version, name) = parse_migration_filename(base);
130                let entry = version_map
131                    .entry(version.clone())
132                    .or_insert((None, None, name));
133                entry.1 = Some(content);
134            } else {
135                // 简单格式:整个文件作为 up SQL,down 为空
136                let (version, name) = parse_migration_filename(&filename);
137                let entry = version_map
138                    .entry(version.clone())
139                    .or_insert((None, None, name));
140                if entry.0.is_none() {
141                    entry.0 = Some(content);
142                }
143            }
144        }
145
146        // 转换为 Migration 列表并按 version 排序
147        type VersionEntry = (Option<String>, Option<String>, String);
148        let mut sorted_versions: Vec<(String, VersionEntry)> = version_map.into_iter().collect();
149        sorted_versions.sort_by(|a, b| a.0.cmp(&b.0));
150
151        for (version, (sql_up, sql_down, name)) in sorted_versions {
152            let migration = Migration::new(
153                &version,
154                &name,
155                sql_up.unwrap_or_default().as_str(),
156                sql_down.unwrap_or_default().as_str(),
157            );
158            migrations.push(migration);
159        }
160
161        Ok(migrations)
162    }
163}
164
165/// 解析迁移文件名:格式 <version>_<name>,如 "001_create_users"
166fn parse_migration_filename(filename: &str) -> (String, String) {
167    if let Some(underscore_pos) = filename.find('_') {
168        let version = filename[..underscore_pos].to_string();
169        let name = filename[underscore_pos + 1..].to_string();
170        (version, name)
171    } else {
172        // 没有下划线,整个作为 version,name 为空
173        (filename.to_string(), filename.to_string())
174    }
175}
176
177pub struct MigrationContext {
178    pub table_name: String,
179    pub connection: Option<Box<dyn crate::pool::Connection>>,
180    /// 数据库类型(用于判断是否支持 DDL 事务包裹)
181    pub db_type: Option<DbType>,
182}
183
184impl Default for MigrationContext {
185    fn default() -> Self {
186        Self {
187            table_name: "__migrations".to_string(),
188            connection: None,
189            db_type: None,
190        }
191    }
192}
193
194impl MigrationContext {
195    /// 设置数据库类型
196    pub fn with_db_type(mut self, db_type: DbType) -> Self {
197        self.db_type = Some(db_type);
198        self
199    }
200}
201
202/// 判断指定数据库方言是否支持 DDL 事务
203///
204/// - PostgreSQL:✅ 支持 DDL 事务(CREATE/ALTER/DROP 可回滚)
205/// - SQLite:✅ 支持 DDL 事务
206/// - MySQL:❌ DDL 语句隐式提交,无法回滚
207/// - Oracle:❌ DDL 语句前后隐式 COMMIT
208/// - SQL Server:❌ 部分 DDL 不支持事务内执行(保守处理)
209/// - 其他:❌ 默认不支持
210fn supports_ddl_transactions(db_type: DbType) -> bool {
211    matches!(db_type, DbType::PostgreSQL | DbType::Sqlite)
212}
213
214#[derive(Debug, Clone, PartialEq)]
215pub enum MigrationDirection {
216    Up,
217    Down,
218}
219
220pub struct Migrator {
221    context: MigrationContext,
222    migrations: Vec<Migration>,
223}
224
225impl Migrator {
226    pub fn new(context: MigrationContext) -> Self {
227        Self {
228            context,
229            migrations: Vec::new(),
230        }
231    }
232
233    pub fn add_migration(mut self, migration: Migration) -> Self {
234        self.migrations.push(migration);
235        self
236    }
237
238    pub fn add_migrations(mut self, migrations: Vec<Migration>) -> Self {
239        self.migrations.extend(migrations);
240        self
241    }
242
243    pub fn get_migrations(&self) -> &Vec<Migration> {
244        &self.migrations
245    }
246
247    pub fn get_pending_migrations(&self) -> Vec<&Migration> {
248        self.migrations.iter().filter(|m| m.batch == 0).collect()
249    }
250
251    pub fn get_applied_migrations(&self) -> Vec<&Migration> {
252        self.migrations.iter().filter(|m| m.batch > 0).collect()
253    }
254
255    pub fn latest_version(&self) -> Option<&str> {
256        self.migrations.last().map(|m| m.version.as_str())
257    }
258
259    pub fn find_migration(&self, version: &str) -> Option<&Migration> {
260        self.migrations.iter().find(|m| m.version == version)
261    }
262
263    /// 检测迁移版本冲突(重复版本号)
264    ///
265    /// 返回第一个冲突的版本号(如有)。
266    /// 在 `migrate`/`up`/`down` 等执行方法入口处调用,确保迁移列表无重复版本。
267    pub fn check_version_conflicts(&self) -> Result<(), DbError> {
268        let mut seen = std::collections::HashSet::new();
269        for m in &self.migrations {
270            if !seen.insert(&m.version) {
271                return Err(DbError::MigrationError(format!(
272                    "迁移版本冲突:版本号 '{}' 重复定义",
273                    m.version
274                )));
275            }
276        }
277        Ok(())
278    }
279
280    /// 执行所有待迁移(batch=0)的 up SQL
281    ///
282    /// 若数据库方言支持 DDL 事务(PostgreSQL/SQLite),则用事务包裹所有待执行迁移,
283    /// 任一迁移失败时回滚全部变更,避免部分迁移导致的状态不一致。
284    /// 不支持 DDL 事务的方言(MySQL/Oracle/SQL Server)逐条执行,失败时保留已执行的变更。
285    pub async fn migrate(&mut self) -> Result<Vec<String>, DbError> {
286        // 版本冲突检测
287        self.check_version_conflicts()?;
288
289        let mut applied = Vec::new();
290        let current_batch = self.migrations.iter().map(|m| m.batch).max().unwrap_or(0) + 1;
291
292        // 收集待迁移的索引(避免在循环中再次 position(),消除 O(n²) 复杂度)
293        let pending_indices: Vec<usize> = self
294            .migrations
295            .iter()
296            .enumerate()
297            .filter(|(_, m)| m.batch == 0)
298            .map(|(idx, _)| idx)
299            .collect();
300
301        if pending_indices.is_empty() {
302            return Ok(applied);
303        }
304
305        // 判断是否需要事务包裹
306        let use_transaction = self
307            .context
308            .db_type
309            .map(supports_ddl_transactions)
310            .unwrap_or(false);
311
312        // 开启事务(若方言支持)
313        if use_transaction {
314            if let Some(ref mut conn) = self.context.connection {
315                conn.begin_transaction().await?;
316            }
317        }
318
319        // 逐条执行迁移
320        for migration_idx in &pending_indices {
321            let sql_up = self.migrations[*migration_idx].sql_up.clone();
322
323            let exec_result = async {
324                if let Some(ref mut conn) = self.context.connection {
325                    if !sql_up.is_empty() {
326                        conn.execute(&sql_up).await?;
327                    }
328                }
329                Ok::<(), DbError>(())
330            }
331            .await;
332
333            if let Err(e) = exec_result {
334                // 事务包裹下回滚
335                if use_transaction {
336                    if let Some(ref mut conn) = self.context.connection {
337                        let _ = conn.rollback().await;
338                    }
339                }
340                return Err(e);
341            }
342
343            // 标记为已执行
344            let now = chrono::Utc::now();
345            self.migrations[*migration_idx].batch = current_batch;
346            self.migrations[*migration_idx].executed_at = Some(now);
347
348            applied.push(self.migrations[*migration_idx].version.clone());
349        }
350
351        // 提交事务(若方言支持)
352        if use_transaction {
353            if let Some(ref mut conn) = self.context.connection {
354                conn.commit().await?;
355            }
356        }
357
358        Ok(applied)
359    }
360
361    /// 回滚指定版本(执行 down SQL)
362    pub async fn rollback(&mut self, version: &str) -> Result<(), DbError> {
363        let migration_idx = self
364            .migrations
365            .iter()
366            .position(|m| m.version == version)
367            .ok_or_else(|| DbError::MigrationError(format!("Migration {} not found", version)))?;
368
369        if self.migrations[migration_idx].batch == 0 {
370            return Err(DbError::MigrationError(format!(
371                "Migration {} not applied",
372                version
373            )));
374        }
375
376        let sql_down = self.migrations[migration_idx].sql_down.clone();
377
378        if let Some(ref mut conn) = self.context.connection {
379            if !sql_down.is_empty() {
380                conn.execute(&sql_down).await?;
381            }
382        }
383
384        self.migrations[migration_idx].batch = 0;
385        self.migrations[migration_idx].executed_at = None;
386        Ok(())
387    }
388
389    /// 执行到指定版本(包括该版本)
390    pub async fn up(&mut self, target_version: Option<&str>) -> Result<Vec<String>, DbError> {
391        // 版本冲突检测
392        self.check_version_conflicts()?;
393
394        let mut applied = Vec::new();
395        let current_batch = self.migrations.iter().map(|m| m.batch).max().unwrap_or(0) + 1;
396
397        for migration in &mut self.migrations {
398            if migration.batch > 0 {
399                continue; // 已执行
400            }
401
402            if let Some(target) = target_version {
403                if migration.version.as_str() > target {
404                    break; // 超过目标版本
405                }
406            }
407
408            let sql_up = migration.sql_up.clone();
409            if let Some(ref mut conn) = self.context.connection {
410                if !sql_up.is_empty() {
411                    conn.execute(&sql_up).await?;
412                }
413            }
414
415            migration.batch = current_batch;
416            migration.executed_at = Some(chrono::Utc::now());
417            applied.push(migration.version.clone());
418        }
419
420        Ok(applied)
421    }
422
423    /// 回滚到指定版本(执行该版本之后所有迁移的 down SQL)
424    pub async fn down(&mut self, target_version: Option<&str>) -> Result<Vec<String>, DbError> {
425        // 版本冲突检测
426        self.check_version_conflicts()?;
427
428        let mut rolled_back = Vec::new();
429
430        // 从后往前回滚
431        let mut indices: Vec<usize> = (0..self.migrations.len()).collect();
432        indices.reverse();
433
434        for idx in indices {
435            let migration = &mut self.migrations[idx];
436            if migration.batch == 0 {
437                continue; // 未执行
438            }
439
440            if let Some(target) = target_version {
441                if migration.version.as_str() <= target {
442                    break; // 到达目标版本
443                }
444            }
445
446            let sql_down = migration.sql_down.clone();
447            if let Some(ref mut conn) = self.context.connection {
448                if !sql_down.is_empty() {
449                    conn.execute(&sql_down).await?;
450                }
451            }
452
453            migration.batch = 0;
454            migration.executed_at = None;
455            rolled_back.push(migration.version.clone());
456        }
457
458        Ok(rolled_back)
459    }
460
461    /// 重置:回滚所有已执行的迁移,然后重新执行
462    pub async fn reset(&mut self) -> Result<Vec<String>, DbError> {
463        // 先全部回滚
464        self.down(None).await?;
465        // 再全部执行
466        self.migrate().await
467    }
468
469    /// 刷新:回滚所有已执行的迁移,然后重新执行
470    pub async fn refresh(&mut self) -> Result<Vec<String>, DbError> {
471        self.reset().await
472    }
473
474    /// 获取迁移进度
475    pub fn progress(&self) -> MigrationProgress {
476        let total = self.migrations.len();
477        let applied = self.migrations.iter().filter(|m| m.batch > 0).count();
478        MigrationProgress::new(total, applied)
479    }
480}
481
482#[derive(Debug, Clone)]
483pub struct MigrationProgress {
484    pub total: usize,
485    pub applied: usize,
486    pub pending: usize,
487    pub current_batch: i32,
488}
489
490impl MigrationProgress {
491    pub fn new(total: usize, applied: usize) -> Self {
492        Self {
493            total,
494            applied,
495            pending: total - applied,
496            current_batch: 0,
497        }
498    }
499
500    pub fn percent_complete(&self) -> f64 {
501        if self.total == 0 {
502            return 100.0;
503        }
504        (self.applied as f64 / self.total as f64) * 100.0
505    }
506}
507
508pub struct SchemaBuilder {
509    table_name: String,
510    columns: Vec<ColumnDef>,
511    indexes: Vec<IndexDef>,
512    foreign_keys: Vec<ForeignKeyDef>,
513    if_not_exists: bool,
514}
515
516impl SchemaBuilder {
517    pub fn new(table_name: &str) -> Self {
518        Self {
519            table_name: table_name.to_string(),
520            columns: Vec::new(),
521            indexes: Vec::new(),
522            foreign_keys: Vec::new(),
523            if_not_exists: true,
524        }
525    }
526
527    pub fn add_column(mut self, column: ColumnDef) -> Self {
528        self.columns.push(column);
529        self
530    }
531
532    pub fn add_index(mut self, index: IndexDef) -> Self {
533        self.indexes.push(index);
534        self
535    }
536
537    pub fn add_foreign_key(mut self, fk: ForeignKeyDef) -> Self {
538        self.foreign_keys.push(fk);
539        self
540    }
541
542    pub fn if_not_exists(mut self, value: bool) -> Self {
543        self.if_not_exists = value;
544        self
545    }
546
547    pub fn build(&self, db_type: DbType) -> String {
548        let mut sql = String::new();
549        sql.push_str("CREATE TABLE ");
550        if self.if_not_exists {
551            sql.push_str("IF NOT EXISTS ");
552        }
553        sql.push_str(&self.table_name);
554        sql.push_str(" (");
555
556        let col_defs: Vec<String> = self.columns.iter().map(|c| c.build(db_type)).collect();
557        sql.push_str(&col_defs.join(", "));
558
559        for index in &self.indexes {
560            sql.push_str(", ");
561            sql.push_str(&index.build(db_type));
562        }
563
564        for fk in &self.foreign_keys {
565            sql.push_str(", ");
566            sql.push_str(&fk.build(db_type));
567        }
568
569        sql.push(')');
570        sql
571    }
572}
573
574#[derive(Debug, Clone)]
575pub struct ColumnDef {
576    pub name: String,
577    pub col_type: String,
578    pub length: Option<usize>,
579    pub precision: Option<(u32, u32)>,
580    pub nullable: bool,
581    pub default: Option<String>,
582    pub auto_increment: bool,
583    pub unique: bool,
584    pub comment: Option<String>,
585}
586
587impl ColumnDef {
588    pub fn new(name: &str, col_type: &str) -> Self {
589        Self {
590            name: name.to_string(),
591            col_type: col_type.to_string(),
592            length: None,
593            precision: None,
594            nullable: true,
595            default: None,
596            auto_increment: false,
597            unique: false,
598            comment: None,
599        }
600    }
601
602    pub fn not_null(mut self) -> Self {
603        self.nullable = false;
604        self
605    }
606
607    pub fn default(mut self, value: &str) -> Self {
608        self.default = Some(value.to_string());
609        self
610    }
611
612    pub fn auto_increment(mut self) -> Self {
613        self.auto_increment = true;
614        self
615    }
616
617    pub fn unique(mut self) -> Self {
618        self.unique = true;
619        self
620    }
621
622    pub fn comment(mut self, comment: &str) -> Self {
623        self.comment = Some(comment.to_string());
624        self
625    }
626
627    pub fn length(mut self, len: usize) -> Self {
628        self.length = Some(len);
629        self
630    }
631
632    fn build(&self, db_type: DbType) -> String {
633        let mut sql = format!("{} {}", self.name, self.col_type);
634        if let Some(len) = self.length {
635            if matches!(db_type, DbType::MySQL) {
636                sql.push_str(&format!("({})", len));
637            }
638        }
639        if self.auto_increment {
640            match db_type {
641                DbType::MySQL => sql.push_str(" AUTO_INCREMENT"),
642                DbType::PostgreSQL => sql.push_str(" GENERATED BY DEFAULT AS IDENTITY"),
643                DbType::Sqlite => sql.push_str(" AUTOINCREMENT"),
644                _ => {}
645            }
646        }
647        if !self.nullable {
648            sql.push_str(" NOT NULL");
649        }
650        if let Some(ref def) = self.default {
651            sql.push_str(&format!(" DEFAULT {}", def));
652        }
653        if self.unique {
654            sql.push_str(" UNIQUE");
655        }
656        sql
657    }
658}
659
660#[derive(Debug, Clone)]
661pub struct IndexDef {
662    pub name: String,
663    pub columns: Vec<String>,
664    pub unique: bool,
665    pub index_type: Option<String>,
666}
667
668impl IndexDef {
669    pub fn new(name: &str, columns: Vec<&str>) -> Self {
670        Self {
671            name: name.to_string(),
672            columns: columns.into_iter().map(|s| s.to_string()).collect(),
673            unique: false,
674            index_type: None,
675        }
676    }
677
678    pub fn unique(mut self) -> Self {
679        self.unique = true;
680        self
681    }
682
683    fn build(&self, _db_type: DbType) -> String {
684        let unique_str = if self.unique { "UNIQUE " } else { "" };
685        format!(
686            "{}KEY {} ({})",
687            unique_str,
688            self.name,
689            self.columns.join(", ")
690        )
691    }
692}
693
694#[derive(Debug, Clone)]
695pub struct ForeignKeyDef {
696    pub name: String,
697    pub column: String,
698    pub referenced_table: String,
699    pub referenced_column: String,
700    pub on_delete: Option<String>,
701    pub on_update: Option<String>,
702}
703
704impl ForeignKeyDef {
705    pub fn new(name: &str, column: &str, referenced_table: &str, referenced_column: &str) -> Self {
706        Self {
707            name: name.to_string(),
708            column: column.to_string(),
709            referenced_table: referenced_table.to_string(),
710            referenced_column: referenced_column.to_string(),
711            on_delete: None,
712            on_update: None,
713        }
714    }
715
716    pub fn on_delete(mut self, action: &str) -> Self {
717        self.on_delete = Some(action.to_string());
718        self
719    }
720
721    pub fn on_update(mut self, action: &str) -> Self {
722        self.on_update = Some(action.to_string());
723        self
724    }
725
726    fn build(&self, _db_type: DbType) -> String {
727        // v0.2.2 修复 C-3:FOREIGN KEY 标识符与 ON DELETE/ON UPDATE 动作严格校验
728        crate::sql_safety::validate_identifier(&self.name, "foreign key constraint name")
729            .expect("invalid foreign key constraint name");
730        crate::sql_safety::validate_identifier(&self.column, "foreign key column")
731            .expect("invalid foreign key column name");
732        crate::sql_safety::validate_identifier(
733            &self.referenced_table,
734            "foreign key referenced table",
735        )
736        .expect("invalid foreign key referenced table name");
737        crate::sql_safety::validate_identifier(
738            &self.referenced_column,
739            "foreign key referenced column",
740        )
741        .expect("invalid foreign key referenced column name");
742        if let Some(ref on_delete) = self.on_delete {
743            crate::sql_safety::validate_fk_action(on_delete).expect("invalid ON DELETE action");
744        }
745        if let Some(ref on_update) = self.on_update {
746            crate::sql_safety::validate_fk_action(on_update).expect("invalid ON UPDATE action");
747        }
748        let mut sql = format!(
749            "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({})",
750            self.name, self.column, self.referenced_table, self.referenced_column
751        );
752        if let Some(ref on_delete) = self.on_delete {
753            sql.push_str(&format!(" ON DELETE {}", on_delete.trim().to_uppercase()));
754        }
755        if let Some(ref on_update) = self.on_update {
756            sql.push_str(&format!(" ON UPDATE {}", on_update.trim().to_uppercase()));
757        }
758        sql
759    }
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765
766    #[test]
767    fn test_migration_new() {
768        let m = Migration::new("001", "create_users", "CREATE TABLE...", "DROP TABLE...");
769        assert_eq!(m.version, "001");
770        assert_eq!(m.name, "create_users");
771    }
772
773    #[test]
774    fn test_migration_with_batch() {
775        let m = Migration::new("001", "create_users", "UP", "DOWN").with_batch(1);
776        assert_eq!(m.batch, 1);
777    }
778
779    #[test]
780    fn test_migrator_latest_version() {
781        let ctx = MigrationContext::default();
782        let migrator = Migrator::new(ctx)
783            .add_migration(Migration::new("001", "v1", "UP", "DOWN"))
784            .add_migration(Migration::new("002", "v2", "UP", "DOWN"));
785
786        assert_eq!(migrator.latest_version(), Some("002"));
787    }
788
789    #[test]
790    fn test_migrator_find_migration() {
791        let ctx = MigrationContext::default();
792        let migrator =
793            Migrator::new(ctx).add_migration(Migration::new("001", "create_users", "UP", "DOWN"));
794
795        assert!(migrator.find_migration("001").is_some());
796        assert!(migrator.find_migration("999").is_none());
797    }
798
799    #[test]
800    fn test_column_def() {
801        let col = ColumnDef::new("id", "INT").not_null().auto_increment();
802        assert_eq!(col.name, "id");
803        assert!(!col.nullable);
804        assert!(col.auto_increment);
805    }
806
807    #[test]
808    fn test_column_build_mysql() {
809        let col = ColumnDef::new("id", "INT").not_null();
810        let sql = col.build(DbType::MySQL);
811        assert!(sql.contains("NOT NULL"));
812    }
813
814    #[test]
815    fn test_index_build() {
816        let idx = IndexDef::new("idx_name", vec!["name"]).unique();
817        let sql = idx.build(DbType::MySQL);
818        assert!(sql.contains("UNIQUE KEY"));
819    }
820
821    #[test]
822    fn test_foreign_key_build() {
823        let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id").on_delete("CASCADE");
824        let sql = fk.build(DbType::MySQL);
825        assert!(sql.contains("FOREIGN KEY"));
826        assert!(sql.contains("ON DELETE CASCADE"));
827    }
828
829    #[test]
830    fn test_foreign_key_build_normalizes_action_case() {
831        // v0.2.2 修复 C-3:动作大小写不敏感,输出统一为大写
832        let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id").on_delete("cascade");
833        let sql = fk.build(DbType::MySQL);
834        assert!(sql.contains("ON DELETE CASCADE"));
835    }
836
837    #[test]
838    #[should_panic(expected = "invalid foreign key column name")]
839    fn test_foreign_key_rejects_sql_injection_in_column() {
840        let fk = ForeignKeyDef::new("fk_user", "user_id; DROP TABLE users", "users", "id");
841        let _ = fk.build(DbType::MySQL);
842    }
843
844    #[test]
845    #[should_panic(expected = "invalid foreign key referenced table name")]
846    fn test_foreign_key_rejects_sql_injection_in_ref_table() {
847        let fk = ForeignKeyDef::new("fk_user", "user_id", "users; DROP TABLE users", "id");
848        let _ = fk.build(DbType::MySQL);
849    }
850
851    #[test]
852    #[should_panic(expected = "invalid ON DELETE action")]
853    fn test_foreign_key_rejects_sql_injection_in_on_delete() {
854        let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id")
855            .on_delete("CASCADE; DROP TABLE users");
856        let _ = fk.build(DbType::MySQL);
857    }
858
859    #[test]
860    #[should_panic(expected = "invalid ON UPDATE action")]
861    fn test_foreign_key_rejects_invalid_on_update_action() {
862        let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id").on_update("EVIL_ACTION");
863        let _ = fk.build(DbType::MySQL);
864    }
865
866    #[test]
867    fn test_schema_builder() {
868        let schema = SchemaBuilder::new("users")
869            .add_column(ColumnDef::new("id", "INT").not_null().auto_increment())
870            .add_column(ColumnDef::new("name", "VARCHAR").length(255));
871
872        let sql = schema.build(DbType::MySQL);
873        assert!(sql.contains("CREATE TABLE"));
874        assert!(sql.contains("users"));
875    }
876
877    #[test]
878    fn test_migration_progress() {
879        let progress = MigrationProgress::new(10, 4);
880        assert_eq!(progress.pending, 6);
881        assert!((progress.percent_complete() - 40.0).abs() < 0.01);
882    }
883}