Skip to main content

sz_orm_migration/
migration.rs

1//! Migration system
2//!
3//! Provides database schema migration management
4
5use std::path::PathBuf;
6use sz_orm_model::DbError;
7use sz_orm_model::DbType;
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 sz_orm_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/// P1-4:校验迁移版本号安全性
203///
204/// 允许的格式:字母、数字、下划线、连字符、点(支持 "001"、"20240101"、"v1.0"、"create_users" 等常见格式)。
205/// 拒绝单引号、分号、注释、空格等可能用于 SQL 注入的字符。
206///
207/// 与 `validate_identifier` 的区别:版本号允许以数字开头(如 "001"),
208/// 而标识符通常要求以字母或下划线开头。
209fn validate_migration_version(version: &str) -> Result<(), DbError> {
210    if version.is_empty() || version.len() > 255 {
211        return Err(DbError::InvalidInput(format!(
212            "invalid migration version: empty or too long (max 255 chars): {:?}",
213            version
214        )));
215    }
216    // 只允许字母、数字、下划线、连字符、点
217    let valid = version
218        .chars()
219        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.');
220    if !valid {
221        return Err(DbError::InvalidInput(format!(
222            "invalid migration version: only ASCII alphanumeric, underscore, hyphen, dot allowed, got {:?}",
223            version
224        )));
225    }
226    // 额外拒绝 "--" 注释序列
227    if version.contains("--") {
228        return Err(DbError::InvalidInput(format!(
229            "invalid migration version: SQL comment sequence '--' not allowed: {:?}",
230            version
231        )));
232    }
233    Ok(())
234}
235
236/// 判断指定数据库方言是否支持 DDL 事务
237///
238/// - PostgreSQL:✅ 支持 DDL 事务(CREATE/ALTER/DROP 可回滚)
239/// - SQLite:✅ 支持 DDL 事务
240/// - MySQL:❌ DDL 语句隐式提交,无法回滚
241/// - Oracle:❌ DDL 语句前后隐式 COMMIT
242/// - SQL Server:❌ 部分 DDL 不支持事务内执行(保守处理)
243/// - 其他:❌ 默认不支持
244fn supports_ddl_transactions(db_type: DbType) -> bool {
245    matches!(db_type, DbType::PostgreSQL | DbType::Sqlite)
246}
247
248#[derive(Debug, Clone, PartialEq)]
249pub enum MigrationDirection {
250    Up,
251    Down,
252}
253
254pub struct Migrator {
255    context: MigrationContext,
256    migrations: Vec<Migration>,
257}
258
259impl Migrator {
260    pub fn new(context: MigrationContext) -> Self {
261        Self {
262            context,
263            migrations: Vec::new(),
264        }
265    }
266
267    pub fn add_migration(mut self, migration: Migration) -> Self {
268        self.migrations.push(migration);
269        self
270    }
271
272    pub fn add_migrations(mut self, migrations: Vec<Migration>) -> Self {
273        self.migrations.extend(migrations);
274        self
275    }
276
277    pub fn get_migrations(&self) -> &Vec<Migration> {
278        &self.migrations
279    }
280
281    pub fn get_pending_migrations(&self) -> Vec<&Migration> {
282        self.migrations.iter().filter(|m| m.batch == 0).collect()
283    }
284
285    pub fn get_applied_migrations(&self) -> Vec<&Migration> {
286        self.migrations.iter().filter(|m| m.batch > 0).collect()
287    }
288
289    pub fn latest_version(&self) -> Option<&str> {
290        self.migrations.last().map(|m| m.version.as_str())
291    }
292
293    pub fn find_migration(&self, version: &str) -> Option<&Migration> {
294        self.migrations.iter().find(|m| m.version == version)
295    }
296
297    /// 检测迁移版本冲突(重复版本号)
298    ///
299    /// 返回第一个冲突的版本号(如有)。
300    /// 在 `migrate`/`up`/`down` 等执行方法入口处调用,确保迁移列表无重复版本。
301    pub fn check_version_conflicts(&self) -> Result<(), DbError> {
302        let mut seen = std::collections::HashSet::new();
303        for m in &self.migrations {
304            if !seen.insert(&m.version) {
305                return Err(DbError::MigrationError(format!(
306                    "迁移版本冲突:版本号 '{}' 重复定义",
307                    m.version
308                )));
309            }
310        }
311        Ok(())
312    }
313
314    // ==================== P1-4:__migrations 持久化表自动创建 ====================
315
316    /// P1-4:生成 `__migrations` 表的 CREATE TABLE IF NOT EXISTS SQL
317    ///
318    /// 表结构(跨方言兼容):
319    /// ```sql
320    /// CREATE TABLE IF NOT EXISTS __migrations (
321    ///     version VARCHAR(255) NOT NULL PRIMARY KEY,
322    ///     name VARCHAR(255),
323    ///     batch INTEGER NOT NULL,
324    ///     executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
325    /// );
326    /// ```
327    ///
328    /// 不同方言的差异:
329    /// - MySQL/PG/SQLite:`TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP`
330    /// - Oracle:`TIMESTAMP DEFAULT CURRENT_TIMESTAMP`
331    /// - SQL Server:`DATETIME DEFAULT GETDATE()`
332    pub fn build_create_migrations_table_sql(&self) -> String {
333        let table = &self.context.table_name;
334        let timestamp_default = match self.context.db_type {
335            Some(DbType::SqlServer) => "DATETIME DEFAULT GETDATE()",
336            Some(DbType::Oracle) => "TIMESTAMP DEFAULT CURRENT_TIMESTAMP",
337            _ => "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP",
338        };
339        format!(
340            "CREATE TABLE IF NOT EXISTS {} (\
341                version VARCHAR(255) NOT NULL PRIMARY KEY, \
342                name VARCHAR(255), \
343                batch INTEGER NOT NULL, \
344                executed_at {ts}\
345            )",
346            table,
347            ts = timestamp_default
348        )
349    }
350
351    /// P1-4:确保 `__migrations` 表存在(若连接可用)
352    ///
353    /// 在 `migrate`/`up`/`down`/`rollback` 等方法入口处调用,
354    /// 自动创建持久化表(若不存在)。
355    ///
356    /// 若 `context.connection` 为 None,跳过(纯内存模式)。
357    async fn ensure_migrations_table(&mut self) -> Result<(), DbError> {
358        let sql = self.build_create_migrations_table_sql();
359        if let Some(ref mut conn) = self.context.connection {
360            conn.execute(&sql).await?;
361        }
362        Ok(())
363    }
364
365    /// P1-4:从 `__migrations` 表加载已执行的迁移记录
366    ///
367    /// 返回 `HashMap<version, batch>`,表示每个已执行迁移的版本号和批次号。
368    /// 调用方应根据返回值更新 `self.migrations` 中对应迁移的 `batch` 字段。
369    ///
370    /// 若 `context.connection` 为 None,返回空 map(纯内存模式)。
371    async fn load_applied_migrations(
372        &mut self,
373    ) -> Result<std::collections::HashMap<String, i32>, DbError> {
374        let mut applied = std::collections::HashMap::new();
375        if let Some(ref mut conn) = self.context.connection {
376            let sql = format!("SELECT version, batch FROM {}", self.context.table_name);
377            let rows = conn.query(&sql).await?;
378            for row in rows {
379                if let Some(sz_orm_model::Value::String(version)) = row.get("version") {
380                    let batch = match row.get("batch") {
381                        Some(sz_orm_model::Value::I32(b)) => *b,
382                        Some(sz_orm_model::Value::I64(b)) => *b as i32,
383                        _ => 0,
384                    };
385                    applied.insert(version.clone(), batch);
386                }
387            }
388        }
389        Ok(applied)
390    }
391
392    /// P1-4:根据 `__migrations` 表的记录同步内存中的迁移状态
393    ///
394    /// 在 `migrate`/`up`/`down` 等方法入口处调用,确保内存状态与数据库持久化状态一致。
395    async fn sync_state_from_db(&mut self) -> Result<(), DbError> {
396        let applied = self.load_applied_migrations().await?;
397        for migration in &mut self.migrations {
398            if let Some(batch) = applied.get(&migration.version) {
399                migration.batch = *batch;
400                migration.executed_at = Some(chrono::Utc::now());
401            } else {
402                migration.batch = 0;
403                migration.executed_at = None;
404            }
405        }
406        Ok(())
407    }
408
409    /// P1-4:向 `__migrations` 表插入一条执行记录
410    ///
411    /// 在每个迁移成功执行 up SQL 后调用。
412    async fn record_migration(
413        &mut self,
414        version: &str,
415        name: &str,
416        batch: i32,
417    ) -> Result<(), DbError> {
418        if let Some(ref mut conn) = self.context.connection {
419            // 安全校验:version 允许字母/数字/下划线/连字符/点(支持 "001"、"20240101"、"v1.0" 等格式),
420            // 但拒绝单引号、分号、注释等危险字符
421            validate_migration_version(version)?;
422            // name 可能为空或含下划线,放宽校验:仅拒绝单引号和分号
423            if name.contains('\'') || name.contains(';') {
424                return Err(DbError::MigrationError(format!("非法迁移名称: {}", name)));
425            }
426            let now = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S");
427            let sql = format!(
428                "INSERT INTO {} (version, name, batch, executed_at) VALUES ('{}', '{}', {}, '{}')",
429                self.context.table_name, version, name, batch, now
430            );
431            conn.execute(&sql).await?;
432        }
433        Ok(())
434    }
435
436    /// P1-4:从 `__migrations` 表删除一条执行记录
437    ///
438    /// 在每个迁移成功执行 down SQL 后调用。
439    async fn remove_migration(&mut self, version: &str) -> Result<(), DbError> {
440        if let Some(ref mut conn) = self.context.connection {
441            validate_migration_version(version)?;
442            let sql = format!(
443                "DELETE FROM {} WHERE version = '{}'",
444                self.context.table_name, version
445            );
446            conn.execute(&sql).await?;
447        }
448        Ok(())
449    }
450
451    /// 执行所有待迁移(batch=0)的 up SQL
452    ///
453    /// 若数据库方言支持 DDL 事务(PostgreSQL/SQLite),则用事务包裹所有待执行迁移,
454    /// 任一迁移失败时回滚全部变更,避免部分迁移导致的状态不一致。
455    /// 不支持 DDL 事务的方言(MySQL/Oracle/SQL Server)逐条执行,失败时保留已执行的变更。
456    ///
457    /// P1-4:若连接可用,会自动创建 `__migrations` 持久化表,并在执行前从表中
458    /// 同步已执行记录、执行后向表插入新记录。
459    pub async fn migrate(&mut self) -> Result<Vec<String>, DbError> {
460        // 版本冲突检测
461        self.check_version_conflicts()?;
462
463        // P1-4:确保 __migrations 表存在
464        self.ensure_migrations_table().await?;
465
466        // P1-4:从 __migrations 表同步已执行状态
467        self.sync_state_from_db().await?;
468
469        let mut applied = Vec::new();
470        let current_batch = self.migrations.iter().map(|m| m.batch).max().unwrap_or(0) + 1;
471
472        // 收集待迁移的索引(避免在循环中再次 position(),消除 O(n²) 复杂度)
473        let pending_indices: Vec<usize> = self
474            .migrations
475            .iter()
476            .enumerate()
477            .filter(|(_, m)| m.batch == 0)
478            .map(|(idx, _)| idx)
479            .collect();
480
481        if pending_indices.is_empty() {
482            return Ok(applied);
483        }
484
485        // 判断是否需要事务包裹
486        let use_transaction = self
487            .context
488            .db_type
489            .map(supports_ddl_transactions)
490            .unwrap_or(false);
491
492        // 开启事务(若方言支持)
493        if use_transaction {
494            if let Some(ref mut conn) = self.context.connection {
495                conn.begin_transaction().await?;
496            }
497        }
498
499        // 逐条执行迁移
500        for migration_idx in &pending_indices {
501            let sql_up = self.migrations[*migration_idx].sql_up.clone();
502            let version = self.migrations[*migration_idx].version.clone();
503            let name = self.migrations[*migration_idx].name.clone();
504
505            let exec_result = async {
506                if let Some(ref mut conn) = self.context.connection {
507                    if !sql_up.is_empty() {
508                        conn.execute(&sql_up).await?;
509                    }
510                }
511                Ok::<(), DbError>(())
512            }
513            .await;
514
515            if let Err(e) = exec_result {
516                // 事务包裹下回滚
517                if use_transaction {
518                    if let Some(ref mut conn) = self.context.connection {
519                        let _ = conn.rollback().await;
520                    }
521                }
522                return Err(e);
523            }
524
525            // 标记为已执行
526            let now = chrono::Utc::now();
527            self.migrations[*migration_idx].batch = current_batch;
528            self.migrations[*migration_idx].executed_at = Some(now);
529
530            // P1-4:向 __migrations 表插入记录
531            if let Err(e) = self.record_migration(&version, &name, current_batch).await {
532                // 事务包裹下回滚
533                if use_transaction {
534                    if let Some(ref mut conn) = self.context.connection {
535                        let _ = conn.rollback().await;
536                    }
537                }
538                return Err(e);
539            }
540
541            applied.push(version);
542        }
543
544        // 提交事务(若方言支持)
545        if use_transaction {
546            if let Some(ref mut conn) = self.context.connection {
547                conn.commit().await?;
548            }
549        }
550
551        Ok(applied)
552    }
553
554    /// 回滚指定版本(执行 down SQL)
555    ///
556    /// P1-4:若连接可用,会自动创建 `__migrations` 表,并在回滚后从表中删除记录。
557    pub async fn rollback(&mut self, version: &str) -> Result<(), DbError> {
558        // P1-4:确保 __migrations 表存在并同步状态
559        self.ensure_migrations_table().await?;
560        self.sync_state_from_db().await?;
561
562        let migration_idx = self
563            .migrations
564            .iter()
565            .position(|m| m.version == version)
566            .ok_or_else(|| DbError::MigrationError(format!("Migration {} not found", version)))?;
567
568        if self.migrations[migration_idx].batch == 0 {
569            return Err(DbError::MigrationError(format!(
570                "Migration {} not applied",
571                version
572            )));
573        }
574
575        let sql_down = self.migrations[migration_idx].sql_down.clone();
576
577        if let Some(ref mut conn) = self.context.connection {
578            if !sql_down.is_empty() {
579                conn.execute(&sql_down).await?;
580            }
581        }
582
583        self.migrations[migration_idx].batch = 0;
584        self.migrations[migration_idx].executed_at = None;
585
586        // P1-4:从 __migrations 表删除记录
587        self.remove_migration(version).await?;
588
589        Ok(())
590    }
591
592    /// 执行到指定版本(包括该版本)
593    ///
594    /// P1-4:若连接可用,会自动创建 `__migrations` 表,并在执行前从表中
595    /// 同步已执行记录、执行后向表插入新记录。
596    pub async fn up(&mut self, target_version: Option<&str>) -> Result<Vec<String>, DbError> {
597        // 版本冲突检测
598        self.check_version_conflicts()?;
599
600        // P1-4:确保 __migrations 表存在并同步状态
601        self.ensure_migrations_table().await?;
602        self.sync_state_from_db().await?;
603
604        let mut applied = Vec::new();
605        let current_batch = self.migrations.iter().map(|m| m.batch).max().unwrap_or(0) + 1;
606
607        // 收集待执行迁移的版本和索引(避免在循环中借用冲突)
608        let pending: Vec<(usize, String, String)> = self
609            .migrations
610            .iter()
611            .enumerate()
612            .filter(|(_, m)| m.batch == 0)
613            .take_while(|(_, m)| {
614                if let Some(target) = target_version {
615                    m.version.as_str() <= target
616                } else {
617                    true
618                }
619            })
620            .map(|(idx, m)| (idx, m.version.clone(), m.name.clone()))
621            .collect();
622
623        for (idx, version, name) in pending {
624            let sql_up = self.migrations[idx].sql_up.clone();
625            if let Some(ref mut conn) = self.context.connection {
626                if !sql_up.is_empty() {
627                    conn.execute(&sql_up).await?;
628                }
629            }
630
631            self.migrations[idx].batch = current_batch;
632            self.migrations[idx].executed_at = Some(chrono::Utc::now());
633
634            // P1-4:向 __migrations 表插入记录
635            self.record_migration(&version, &name, current_batch)
636                .await?;
637
638            applied.push(version);
639        }
640
641        Ok(applied)
642    }
643
644    /// 回滚到指定版本(执行该版本之后所有迁移的 down SQL)
645    ///
646    /// P1-4:若连接可用,会自动创建 `__migrations` 表,并在回滚后从表中删除记录。
647    pub async fn down(&mut self, target_version: Option<&str>) -> Result<Vec<String>, DbError> {
648        // 版本冲突检测
649        self.check_version_conflicts()?;
650
651        // P1-4:确保 __migrations 表存在并同步状态
652        self.ensure_migrations_table().await?;
653        self.sync_state_from_db().await?;
654
655        let mut rolled_back = Vec::new();
656
657        // 从后往前回滚,收集待回滚的索引和版本
658        let mut indices: Vec<usize> = (0..self.migrations.len()).collect();
659        indices.reverse();
660
661        let pending_rollback: Vec<(usize, String)> = indices
662            .iter()
663            .filter(|&&idx| self.migrations[idx].batch > 0)
664            .take_while(|&&idx| {
665                if let Some(target) = target_version {
666                    self.migrations[idx].version.as_str() > target
667                } else {
668                    true
669                }
670            })
671            .map(|&idx| (idx, self.migrations[idx].version.clone()))
672            .collect();
673
674        for (idx, version) in pending_rollback {
675            let sql_down = self.migrations[idx].sql_down.clone();
676            if let Some(ref mut conn) = self.context.connection {
677                if !sql_down.is_empty() {
678                    conn.execute(&sql_down).await?;
679                }
680            }
681
682            self.migrations[idx].batch = 0;
683            self.migrations[idx].executed_at = None;
684
685            // P1-4:从 __migrations 表删除记录
686            self.remove_migration(&version).await?;
687
688            rolled_back.push(version);
689        }
690
691        Ok(rolled_back)
692    }
693
694    /// 重置:回滚所有已执行的迁移,然后重新执行
695    pub async fn reset(&mut self) -> Result<Vec<String>, DbError> {
696        // 先全部回滚
697        self.down(None).await?;
698        // 再全部执行
699        self.migrate().await
700    }
701
702    /// 刷新:回滚所有已执行的迁移,然后重新执行
703    pub async fn refresh(&mut self) -> Result<Vec<String>, DbError> {
704        self.reset().await
705    }
706
707    /// 获取迁移进度
708    pub fn progress(&self) -> MigrationProgress {
709        let total = self.migrations.len();
710        let applied = self.migrations.iter().filter(|m| m.batch > 0).count();
711        MigrationProgress::new(total, applied)
712    }
713}
714
715#[derive(Debug, Clone)]
716pub struct MigrationProgress {
717    pub total: usize,
718    pub applied: usize,
719    pub pending: usize,
720    pub current_batch: i32,
721}
722
723impl MigrationProgress {
724    pub fn new(total: usize, applied: usize) -> Self {
725        Self {
726            total,
727            applied,
728            pending: total - applied,
729            current_batch: 0,
730        }
731    }
732
733    pub fn percent_complete(&self) -> f64 {
734        if self.total == 0 {
735            return 100.0;
736        }
737        (self.applied as f64 / self.total as f64) * 100.0
738    }
739}
740
741pub struct SchemaBuilder {
742    table_name: String,
743    columns: Vec<ColumnDef>,
744    indexes: Vec<IndexDef>,
745    foreign_keys: Vec<ForeignKeyDef>,
746    if_not_exists: bool,
747}
748
749impl SchemaBuilder {
750    pub fn new(table_name: &str) -> Self {
751        Self {
752            table_name: table_name.to_string(),
753            columns: Vec::new(),
754            indexes: Vec::new(),
755            foreign_keys: Vec::new(),
756            if_not_exists: true,
757        }
758    }
759
760    pub fn add_column(mut self, column: ColumnDef) -> Self {
761        self.columns.push(column);
762        self
763    }
764
765    pub fn add_index(mut self, index: IndexDef) -> Self {
766        self.indexes.push(index);
767        self
768    }
769
770    pub fn add_foreign_key(mut self, fk: ForeignKeyDef) -> Self {
771        self.foreign_keys.push(fk);
772        self
773    }
774
775    pub fn if_not_exists(mut self, value: bool) -> Self {
776        self.if_not_exists = value;
777        self
778    }
779
780    pub fn build(&self, db_type: DbType) -> Result<String, DbError> {
781        let mut sql = String::new();
782        sql.push_str("CREATE TABLE ");
783        if self.if_not_exists {
784            sql.push_str("IF NOT EXISTS ");
785        }
786        sql.push_str(&self.table_name);
787        sql.push_str(" (");
788
789        let col_defs: Vec<String> = self.columns.iter().map(|c| c.build(db_type)).collect();
790        sql.push_str(&col_defs.join(", "));
791
792        for index in &self.indexes {
793            sql.push_str(", ");
794            sql.push_str(&index.build(db_type));
795        }
796
797        for fk in &self.foreign_keys {
798            sql.push_str(", ");
799            sql.push_str(&fk.build(db_type)?);
800        }
801
802        sql.push(')');
803        Ok(sql)
804    }
805}
806
807#[derive(Debug, Clone)]
808pub struct ColumnDef {
809    pub name: String,
810    pub col_type: String,
811    pub length: Option<usize>,
812    pub precision: Option<(u32, u32)>,
813    pub nullable: bool,
814    pub default: Option<String>,
815    pub auto_increment: bool,
816    pub unique: bool,
817    pub comment: Option<String>,
818}
819
820impl ColumnDef {
821    pub fn new(name: &str, col_type: &str) -> Self {
822        Self {
823            name: name.to_string(),
824            col_type: col_type.to_string(),
825            length: None,
826            precision: None,
827            nullable: true,
828            default: None,
829            auto_increment: false,
830            unique: false,
831            comment: None,
832        }
833    }
834
835    pub fn not_null(mut self) -> Self {
836        self.nullable = false;
837        self
838    }
839
840    pub fn default(mut self, value: &str) -> Self {
841        self.default = Some(value.to_string());
842        self
843    }
844
845    pub fn auto_increment(mut self) -> Self {
846        self.auto_increment = true;
847        self
848    }
849
850    pub fn unique(mut self) -> Self {
851        self.unique = true;
852        self
853    }
854
855    pub fn comment(mut self, comment: &str) -> Self {
856        self.comment = Some(comment.to_string());
857        self
858    }
859
860    pub fn length(mut self, len: usize) -> Self {
861        self.length = Some(len);
862        self
863    }
864
865    fn build(&self, db_type: DbType) -> String {
866        let mut sql = format!("{} {}", self.name, self.col_type);
867        if let Some(len) = self.length {
868            if matches!(db_type, DbType::MySQL) {
869                sql.push_str(&format!("({})", len));
870            }
871        }
872        if self.auto_increment {
873            match db_type {
874                DbType::MySQL => sql.push_str(" AUTO_INCREMENT"),
875                DbType::PostgreSQL => sql.push_str(" GENERATED BY DEFAULT AS IDENTITY"),
876                DbType::Sqlite => sql.push_str(" AUTOINCREMENT"),
877                _ => {}
878            }
879        }
880        if !self.nullable {
881            sql.push_str(" NOT NULL");
882        }
883        if let Some(ref def) = self.default {
884            sql.push_str(&format!(" DEFAULT {}", def));
885        }
886        if self.unique {
887            sql.push_str(" UNIQUE");
888        }
889        sql
890    }
891}
892
893#[derive(Debug, Clone)]
894pub struct IndexDef {
895    pub name: String,
896    pub columns: Vec<String>,
897    pub unique: bool,
898    pub index_type: Option<String>,
899}
900
901impl IndexDef {
902    pub fn new(name: &str, columns: Vec<&str>) -> Self {
903        Self {
904            name: name.to_string(),
905            columns: columns.into_iter().map(|s| s.to_string()).collect(),
906            unique: false,
907            index_type: None,
908        }
909    }
910
911    pub fn unique(mut self) -> Self {
912        self.unique = true;
913        self
914    }
915
916    fn build(&self, _db_type: DbType) -> String {
917        let unique_str = if self.unique { "UNIQUE " } else { "" };
918        format!(
919            "{}KEY {} ({})",
920            unique_str,
921            self.name,
922            self.columns.join(", ")
923        )
924    }
925}
926
927#[derive(Debug, Clone)]
928pub struct ForeignKeyDef {
929    pub name: String,
930    pub column: String,
931    pub referenced_table: String,
932    pub referenced_column: String,
933    pub on_delete: Option<String>,
934    pub on_update: Option<String>,
935}
936
937impl ForeignKeyDef {
938    pub fn new(name: &str, column: &str, referenced_table: &str, referenced_column: &str) -> Self {
939        Self {
940            name: name.to_string(),
941            column: column.to_string(),
942            referenced_table: referenced_table.to_string(),
943            referenced_column: referenced_column.to_string(),
944            on_delete: None,
945            on_update: None,
946        }
947    }
948
949    pub fn on_delete(mut self, action: &str) -> Self {
950        self.on_delete = Some(action.to_string());
951        self
952    }
953
954    pub fn on_update(mut self, action: &str) -> Self {
955        self.on_update = Some(action.to_string());
956        self
957    }
958
959    fn build(&self, _db_type: DbType) -> Result<String, DbError> {
960        // v0.2.2 修复 C-3:FOREIGN KEY 标识符与 ON DELETE/ON UPDATE 动作严格校验
961        sz_orm_model::sql_safety::validate_identifier(&self.name, "foreign key constraint name")?;
962        sz_orm_model::sql_safety::validate_identifier(&self.column, "foreign key column")?;
963        sz_orm_model::sql_safety::validate_identifier(
964            &self.referenced_table,
965            "foreign key referenced table",
966        )?;
967        sz_orm_model::sql_safety::validate_identifier(
968            &self.referenced_column,
969            "foreign key referenced column",
970        )?;
971        if let Some(ref on_delete) = self.on_delete {
972            sz_orm_model::sql_safety::validate_fk_action(on_delete)?;
973        }
974        if let Some(ref on_update) = self.on_update {
975            sz_orm_model::sql_safety::validate_fk_action(on_update)?;
976        }
977        let mut sql = format!(
978            "CONSTRAINT {} FOREIGN KEY ({}) REFERENCES {} ({})",
979            self.name, self.column, self.referenced_table, self.referenced_column
980        );
981        if let Some(ref on_delete) = self.on_delete {
982            sql.push_str(&format!(" ON DELETE {}", on_delete.trim().to_uppercase()));
983        }
984        if let Some(ref on_update) = self.on_update {
985            sql.push_str(&format!(" ON UPDATE {}", on_update.trim().to_uppercase()));
986        }
987        Ok(sql)
988    }
989}
990
991#[cfg(test)]
992mod tests {
993    use super::*;
994
995    #[test]
996    fn test_migration_new() {
997        let m = Migration::new("001", "create_users", "CREATE TABLE...", "DROP TABLE...");
998        assert_eq!(m.version, "001");
999        assert_eq!(m.name, "create_users");
1000    }
1001
1002    #[test]
1003    fn test_migration_with_batch() {
1004        let m = Migration::new("001", "create_users", "UP", "DOWN").with_batch(1);
1005        assert_eq!(m.batch, 1);
1006    }
1007
1008    #[test]
1009    fn test_migrator_latest_version() {
1010        let ctx = MigrationContext::default();
1011        let migrator = Migrator::new(ctx)
1012            .add_migration(Migration::new("001", "v1", "UP", "DOWN"))
1013            .add_migration(Migration::new("002", "v2", "UP", "DOWN"));
1014
1015        assert_eq!(migrator.latest_version(), Some("002"));
1016    }
1017
1018    #[test]
1019    fn test_migrator_find_migration() {
1020        let ctx = MigrationContext::default();
1021        let migrator =
1022            Migrator::new(ctx).add_migration(Migration::new("001", "create_users", "UP", "DOWN"));
1023
1024        assert!(migrator.find_migration("001").is_some());
1025        assert!(migrator.find_migration("999").is_none());
1026    }
1027
1028    #[test]
1029    fn test_column_def() {
1030        let col = ColumnDef::new("id", "INT").not_null().auto_increment();
1031        assert_eq!(col.name, "id");
1032        assert!(!col.nullable);
1033        assert!(col.auto_increment);
1034    }
1035
1036    #[test]
1037    fn test_column_build_mysql() {
1038        let col = ColumnDef::new("id", "INT").not_null();
1039        let sql = col.build(DbType::MySQL);
1040        assert!(sql.contains("NOT NULL"));
1041    }
1042
1043    #[test]
1044    fn test_index_build() {
1045        let idx = IndexDef::new("idx_name", vec!["name"]).unique();
1046        let sql = idx.build(DbType::MySQL);
1047        assert!(sql.contains("UNIQUE KEY"));
1048    }
1049
1050    #[test]
1051    fn test_foreign_key_build() {
1052        let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id").on_delete("CASCADE");
1053        let sql = fk.build(DbType::MySQL).unwrap();
1054        assert!(sql.contains("FOREIGN KEY"));
1055        assert!(sql.contains("ON DELETE CASCADE"));
1056    }
1057
1058    #[test]
1059    fn test_foreign_key_build_normalizes_action_case() {
1060        // v0.2.2 修复 C-3:动作大小写不敏感,输出统一为大写
1061        let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id").on_delete("cascade");
1062        let sql = fk.build(DbType::MySQL).unwrap();
1063        assert!(sql.contains("ON DELETE CASCADE"));
1064    }
1065
1066    #[test]
1067    fn test_foreign_key_rejects_sql_injection_in_column() {
1068        let fk = ForeignKeyDef::new("fk_user", "user_id; DROP TABLE users", "users", "id");
1069        let result = fk.build(DbType::MySQL);
1070        assert!(result.is_err());
1071    }
1072
1073    #[test]
1074    fn test_foreign_key_rejects_sql_injection_in_ref_table() {
1075        let fk = ForeignKeyDef::new("fk_user", "user_id", "users; DROP TABLE users", "id");
1076        let result = fk.build(DbType::MySQL);
1077        assert!(result.is_err());
1078    }
1079
1080    #[test]
1081    fn test_foreign_key_rejects_sql_injection_in_on_delete() {
1082        let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id")
1083            .on_delete("CASCADE; DROP TABLE users");
1084        let result = fk.build(DbType::MySQL);
1085        assert!(result.is_err());
1086    }
1087
1088    #[test]
1089    fn test_foreign_key_rejects_invalid_on_update_action() {
1090        let fk = ForeignKeyDef::new("fk_user", "user_id", "users", "id").on_update("EVIL_ACTION");
1091        let result = fk.build(DbType::MySQL);
1092        assert!(result.is_err());
1093    }
1094
1095    #[test]
1096    fn test_schema_builder() {
1097        let schema = SchemaBuilder::new("users")
1098            .add_column(ColumnDef::new("id", "INT").not_null().auto_increment())
1099            .add_column(ColumnDef::new("name", "VARCHAR").length(255));
1100
1101        let sql = schema.build(DbType::MySQL).unwrap();
1102        assert!(sql.contains("CREATE TABLE"));
1103        assert!(sql.contains("users"));
1104    }
1105
1106    #[test]
1107    fn test_migration_progress() {
1108        let progress = MigrationProgress::new(10, 4);
1109        assert_eq!(progress.pending, 6);
1110        assert!((progress.percent_complete() - 40.0).abs() < 0.01);
1111    }
1112}