Skip to main content

sz_rust_cli/cmd/
migrate.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-2026 SZ-Rust Team
3//
4//! `migrate` / `migrate:status` 命令 — 整合 `sz-orm-core::migration`
5//!
6//! ## PHP 对齐
7//!
8//! PHP `migrate:status` 输出表格:
9//! ```text
10//! +---------+------------------+---------------------+
11//! | Version | Migration Name   | Run Time            |
12//! +---------+------------------+---------------------+
13//! | 001     | create_users     | 2024-01-01 00:00:00 |
14//! | 002     | add_index        | Pending             |
15//! +---------+------------------+---------------------+
16//! ```
17//!
18//! ## 整合说明
19//!
20//! 本模块使用 `sz_orm_core::migration::FileMigrationResolver`(sz-orm-core,经 sz-rust-orm-facade 透传) 解析迁移目录,
21//! 对齐 sz-orm 的迁移文件命名约定(`<version>_<name>_up.sql` / `<version>_<name>_down.sql`)。
22//!
23//! ### 离线模式(默认)
24//!
25//! 不连接数据库,仅解析并列出迁移文件。`migrate` 命令输出"将执行的 SQL",
26//! `migrate:status` 输出迁移列表(状态统一显示 `Pending*`,因离线无法确定执行历史)。
27//!
28//! ### 在线模式(提供 `--url` 时启用)
29//!
30//! 通过 `Migrator::migrate()` 执行真实迁移,需要注入 `MigrationContext::connection`。
31//! CLI 通过 `sz-orm-sqlx` 建立 PostgreSQL/MySQL/SQLite 连接池,包装为
32//! `Box<dyn Connection>` 注入 `MigrationContext`。
33//! `migrate:status` 在线模式下从 `__migrations` 表查询已应用版本,显示真实状态。
34
35use std::path::{Path, PathBuf};
36
37use clap::Args;
38use sz_rust_core::orm::migration::{FileMigrationResolver, Migration, MigrationResolver};
39use sz_rust_core::orm::{Connection, DbType};
40
41use crate::error::CliError;
42
43/// `migrate` 命令参数
44///
45/// 对齐 PHP `php think migrate` / `php think migrate:rollback`。
46#[derive(Args, Debug)]
47pub struct MigrateArgs {
48    /// 回滚最后一批迁移(对齐 PHP `migrate:rollback`)
49    #[arg(long)]
50    pub rollback: bool,
51
52    /// 迁移目录(默认 `migrations`)
53    #[arg(short = 'p', long, default_value = "migrations")]
54    pub path: String,
55
56    /// 数据库类型(默认 `postgres`,对齐 sz-orm `DbType`)
57    ///
58    /// 影响迁移解析的方言处理。支持值:
59    /// `mysql` / `postgres` / `sqlite` / `oracle` / `mssql` /
60    /// `oceanbase` / `dameng` / `kingbase` 等(详见 `DbType::from_str`)。
61    #[arg(long, default_value = "postgres")]
62    pub db_type: String,
63
64    /// 打印每个迁移的 SQL 内容(dry-run 模式,便于审查)
65    #[arg(long)]
66    pub show_sql: bool,
67
68    /// 数据库连接 URL(启用在线模式)
69    ///
70    /// 提供时连接数据库执行真实迁移;省略时为离线模式(仅列出待执行的 SQL)。
71    /// 格式示例:
72    /// - PostgreSQL: `postgres://user:pass@host:5432/dbname`
73    /// - MySQL: `mysql://user:pass@host:3306/dbname`
74    /// - SQLite: `sqlite://path/to/database.db`
75    #[arg(long)]
76    pub url: Option<String>,
77}
78
79/// 执行 migrate 命令
80///
81/// - 无 `--rollback`:执行所有待迁移(对齐 `php think migrate`)
82/// - 有 `--rollback`:回滚最后一批迁移(对齐 `php think migrate:rollback`)
83///
84/// # 模式
85///
86/// - **离线模式**(默认,未提供 `--url`):仅解析迁移目录并打印待执行内容
87/// - **在线模式**(提供 `--url`):连接数据库执行真实迁移
88pub async fn execute_migrate(args: &MigrateArgs) -> Result<(), CliError> {
89    let path = PathBuf::from(&args.path);
90
91    if !path.exists() {
92        return Err(CliError::Migration(format!(
93            "Migration directory not found: {}",
94            path.display()
95        )));
96    }
97
98    let db_type = DbType::from_str(&args.db_type)
99        .ok_or_else(|| CliError::Migration(format!("Unknown database type: {}", args.db_type)))?;
100
101    let migrations = resolve_migrations(&path, db_type)?;
102
103    if migrations.is_empty() {
104        println!("No migrations found in: {}", path.display());
105        return Ok(());
106    }
107
108    match &args.url {
109        None => execute_migrate_offline(args, &migrations),
110        Some(url) => execute_migrate_online(args, &migrations, url, db_type).await,
111    }
112}
113
114/// 离线模式执行 migrate(仅打印,不连库)
115fn execute_migrate_offline(args: &MigrateArgs, migrations: &[Migration]) -> Result<(), CliError> {
116    if args.rollback {
117        println!("Rolling back last batch in: {}", args.path);
118        if let Some(last) = migrations.last() {
119            println!("  Would rollback: {} ({})", last.version, last.name);
120            if args.show_sql {
121                println!("{}", print_sql_block("SQL DOWN", &last.sql_down));
122            }
123        }
124        println!("Note: Actual rollback requires database connection (offline mode).");
125    } else {
126        println!("Running migrations in: {}", args.path);
127        for m in migrations {
128            println!("  Would apply: {} ({})", m.version, m.name);
129            if args.show_sql {
130                println!("{}", print_sql_block("SQL UP", &m.sql_up));
131            }
132        }
133        println!(
134            "Total: {} migration(s). Note: Actual execution requires database connection (offline mode).",
135            migrations.len()
136        );
137    }
138    Ok(())
139}
140
141/// 在线模式执行 migrate(连接数据库真实执行)
142async fn execute_migrate_online(
143    args: &MigrateArgs,
144    migrations: &[Migration],
145    url: &str,
146    db_type: DbType,
147) -> Result<(), CliError> {
148    let mut conn = create_connection(url, db_type).await?;
149
150    if args.rollback {
151        // 回滚最后一个迁移
152        let last = migrations
153            .last()
154            .ok_or_else(|| CliError::Migration("No migrations to rollback".to_string()))?;
155        println!("Rolling back: {} ({})", last.version, last.name);
156        if args.show_sql {
157            println!("{}", print_sql_block("SQL DOWN", &last.sql_down));
158        }
159        if !last.sql_down.is_empty() {
160            let sql = prepare_sql_for_db(&last.sql_down, db_type);
161            conn.execute(&sql)
162                .await
163                .map_err(|e| CliError::Migration(format!("Rollback failed: {}", e)))?;
164        }
165        // 从 __migrations 表删除记录
166        delete_migration_record(&mut conn, &last.version, db_type).await?;
167        println!("Rollback completed: {} ({})", last.version, last.name);
168    } else {
169        // 确保 __migrations 表存在
170        ensure_migrations_table(&mut conn, db_type).await?;
171
172        // 查询已应用版本
173        let applied = fetch_applied_versions(&mut conn, db_type).await?;
174
175        // 过滤出待执行的迁移
176        let pending: Vec<&Migration> = migrations
177            .iter()
178            .filter(|m| !applied.contains(&m.version))
179            .collect();
180
181        if pending.is_empty() {
182            println!("No pending migrations. Database is up to date.");
183            return Ok(());
184        }
185
186        println!("Running {} pending migration(s):", pending.len());
187
188        let mut applied_count = 0;
189        for m in &pending {
190            if args.show_sql {
191                println!("{}", print_sql_block("SQL UP", &m.sql_up));
192            }
193            let sql = prepare_sql_for_db(&m.sql_up, db_type);
194
195            conn.execute(&sql).await.map_err(|e| {
196                CliError::Migration(format!("Migration {} failed: {}", m.version, e))
197            })?;
198            insert_migration_record(&mut conn, &m.version, &m.name, db_type).await?;
199            println!("  Applied: {}", m.version);
200            applied_count += 1;
201        }
202        println!("Migration completed: {} applied.", applied_count);
203    }
204
205    Ok(())
206}
207
208/// 执行 migrate:status 命令(兼容入口,使用默认 `postgres` 方言)
209///
210/// 对齐 PHP `php think migrate:status`,输出表格格式的迁移状态。
211///
212/// 等价于 [`execute_status_with`] 传入 `db_type="postgres"`、`show_sql=false`、`url=None`。
213pub async fn execute_status(path: &str) -> Result<(), CliError> {
214    execute_status_full(path, "postgres", false, None).await
215}
216
217/// 执行 migrate:status 命令(完整参数)
218///
219/// # 参数
220///
221/// - `path`:迁移目录
222/// - `db_type_str`:数据库类型字符串(由 `DbType::from_str` 解析)
223/// - `show_sql`:是否打印每个迁移的 SQL 内容
224pub async fn execute_status_with(
225    path: &str,
226    db_type_str: &str,
227    show_sql: bool,
228) -> Result<(), CliError> {
229    execute_status_full(path, db_type_str, show_sql, None).await
230}
231
232/// 执行 migrate:status 命令(完整参数,含在线模式)
233pub async fn execute_status_full(
234    path: &str,
235    db_type_str: &str,
236    show_sql: bool,
237    url: Option<&str>,
238) -> Result<(), CliError> {
239    let path_buf = PathBuf::from(path);
240
241    if !path_buf.exists() {
242        return Err(CliError::Migration(format!(
243            "Migration directory not found: {}",
244            path_buf.display()
245        )));
246    }
247
248    let db_type = DbType::from_str(db_type_str)
249        .ok_or_else(|| CliError::Migration(format!("Unknown database type: {}", db_type_str)))?;
250
251    let migrations = resolve_migrations(&path_buf, db_type)?;
252
253    if migrations.is_empty() {
254        println!("No migrations found in: {}", path_buf.display());
255        return Ok(());
256    }
257
258    // 在线模式:查询数据库已应用版本
259    let applied_versions = if let Some(url) = url {
260        let mut conn = create_connection(url, db_type).await?;
261        ensure_migrations_table(&mut conn, db_type).await?;
262        fetch_applied_versions(&mut conn, db_type).await?
263    } else {
264        std::collections::HashSet::new()
265    };
266
267    // 表格输出(对齐 PHP migrate:status 格式)
268    println!(
269        "{:<15} {:<30} {:<20}",
270        "Version", "Migration Name", "Status"
271    );
272    println!("{}", "-".repeat(65));
273
274    for m in &migrations {
275        let status = if applied_versions.contains(&m.version) {
276            "Applied"
277        } else if url.is_some() {
278            "Pending"
279        } else {
280            "Pending*"
281        };
282        println!("{:<15} {:<30} {:<20}", m.version, m.name, status);
283        if show_sql {
284            println!("{}", print_sql_block("SQL UP", &m.sql_up));
285            println!("{}", print_sql_block("SQL DOWN", &m.sql_down));
286        }
287    }
288
289    println!();
290    if url.is_some() {
291        let applied = migrations
292            .iter()
293            .filter(|m| applied_versions.contains(&m.version))
294            .count();
295        println!(
296            "Total: {} migration(s), {} applied, {} pending.",
297            migrations.len(),
298            applied,
299            migrations.len() - applied
300        );
301    } else {
302        println!("* Status cannot be determined without database connection (offline mode).");
303    }
304
305    Ok(())
306}
307
308/// 创建数据库连接(支持 5 后端:PostgreSQL/MySQL/SQLite/Oracle/MSSQL)
309///
310/// DSN scheme 自动识别后端:`postgres://` / `mysql://` / `sqlite:` /
311/// `oracle://` / `mssql://`。`db_type` 仅用于错误诊断,实际后端由 DSN 决定。
312///
313/// - MSSQL:绕过 AnyPool,直接用 MssqlPoolHandle 添加 TLS 配置
314/// - Oracle:绕过 AnyPool,用 `std::mem::forget` 阻止 OraclePoolHandle drop,
315///   避免 sz-orm-oracle 内部独立 tokio Runtime 在 async 上下文中 drop 时 panic
316async fn create_connection(url: &str, db_type: DbType) -> Result<Box<dyn Connection>, CliError> {
317    use std::sync::Arc;
318    use sz_orm_sqlx::any_driver::AnyPool;
319
320    if db_type == DbType::SqlServer {
321        use sz_orm_mssql::{MssqlConnectionFactory, MssqlPoolHandle};
322        use sz_rust_core::orm::ConnectionFactory;
323
324        let rest = url
325            .strip_prefix("mssql://")
326            .or_else(|| url.strip_prefix("sqlserver://"))
327            .ok_or_else(|| CliError::Migration("Invalid MSSQL DSN".to_string()))?;
328        let (userinfo, hostinfo) = rest
329            .split_once('@')
330            .ok_or_else(|| CliError::Migration("MSSQL DSN missing @".to_string()))?;
331        let (username, password) = userinfo
332            .split_once(':')
333            .ok_or_else(|| CliError::Migration("MSSQL DSN missing password".to_string()))?;
334        let (host_port, database) = hostinfo
335            .split_once('/')
336            .ok_or_else(|| CliError::Migration("MSSQL DSN missing database".to_string()))?;
337        let (host, port) = host_port.split_once(':').unwrap_or((host_port, "1433"));
338        let ado = format!(
339            "Server={host},{port};Database={database};User Id={username};Password={password};\
340             Encrypt=false;TrustServerCertificate=true;"
341        );
342        let pool = MssqlPoolHandle::connect(&ado)
343            .await
344            .map_err(|e| CliError::Migration(format!("MSSQL connect failed: {e}")))?;
345        let factory = MssqlConnectionFactory::new(Arc::new(pool));
346        let conn = factory
347            .create()
348            .await
349            .map_err(|e| CliError::Migration(format!("MSSQL acquire failed: {e}")))?;
350        return Ok(conn);
351    }
352
353    if db_type == DbType::Oracle {
354        use sz_orm_oracle::{OracleConnectionFactory, OraclePoolHandle};
355        use sz_rust_core::orm::ConnectionFactory;
356
357        let rest = url
358            .strip_prefix("oracle://")
359            .ok_or_else(|| CliError::Migration("Invalid Oracle DSN".to_string()))?;
360        let (userinfo, hostinfo) = rest
361            .split_once('@')
362            .ok_or_else(|| CliError::Migration("Oracle DSN missing @".to_string()))?;
363        let (username, password) = userinfo
364            .split_once(':')
365            .ok_or_else(|| CliError::Migration("Oracle DSN missing password".to_string()))?;
366        let pool = OraclePoolHandle::connect(username, password, hostinfo)
367            .map_err(|e| CliError::Migration(format!("Oracle connect failed: {e}")))?;
368        let pool_arc = Arc::new(pool);
369        let factory = OracleConnectionFactory::new(pool_arc.clone());
370        std::mem::forget(pool_arc);
371        let conn = factory
372            .create()
373            .await
374            .map_err(|e| CliError::Migration(format!("Oracle acquire failed: {e}")))?;
375        return Ok(conn);
376    }
377
378    let pool = AnyPool::connect(url)
379        .await
380        .map_err(|e| CliError::Migration(format!("{:?} connect failed: {}", db_type, e)))?;
381    let conn = pool
382        .create()
383        .await
384        .map_err(|e| CliError::Migration(format!("{:?} acquire failed: {}", db_type, e)))?;
385    Ok(Box::new(conn))
386}
387
388/// 确保 __migrations 表存在
389async fn ensure_migrations_table(
390    conn: &mut Box<dyn Connection>,
391    db_type: DbType,
392) -> Result<(), CliError> {
393    let sql: &str = match db_type {
394        DbType::PostgreSQL | DbType::Sqlite => {
395            "CREATE TABLE IF NOT EXISTS __migrations (
396                version VARCHAR(255) PRIMARY KEY,
397                name VARCHAR(255) NOT NULL,
398                batch INTEGER NOT NULL,
399                executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
400            )"
401        }
402        DbType::MySQL => {
403            "CREATE TABLE IF NOT EXISTS __migrations (
404                version VARCHAR(255) PRIMARY KEY,
405                name VARCHAR(255) NOT NULL,
406                batch INT NOT NULL,
407                executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
408            )"
409        }
410        DbType::Oracle => {
411            "CREATE TABLE \"__migrations\" (\
412                version VARCHAR2(255) PRIMARY KEY,\
413                name VARCHAR2(255) NOT NULL,\
414                batch NUMBER(10) NOT NULL,\
415                executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\
416            )"
417        }
418        DbType::SqlServer => {
419            "CREATE TABLE __migrations (\
420                 version NVARCHAR(255) PRIMARY KEY,\
421                 name NVARCHAR(255) NOT NULL,\
422                 batch INT NOT NULL,\
423                 executed_at DATETIME2 NOT NULL DEFAULT CURRENT_TIMESTAMP\
424             )"
425        }
426        _ => {
427            return Err(CliError::Migration(format!(
428                "Cannot ensure __migrations table for db_type {:?}",
429                db_type
430            )))
431        }
432    };
433    let result = conn.execute(sql).await;
434    match result {
435        Ok(_) => Ok(()),
436        Err(e) if db_type == DbType::SqlServer => {
437            let err_msg = format!("{}", e);
438            if err_msg.contains("2714") || err_msg.contains("already exists") {
439                Ok(())
440            } else {
441                Err(CliError::Migration(format!(
442                    "Failed to create __migrations table: {}",
443                    e
444                )))
445            }
446        }
447        Err(e) if db_type == DbType::Oracle => {
448            let err_msg = format!("{}", e);
449            if err_msg.contains("ORA-00955") || err_msg.contains("already exists") {
450                Ok(())
451            } else {
452                Err(CliError::Migration(format!(
453                    "Failed to create __migrations table: {}",
454                    e
455                )))
456            }
457        }
458        Err(e) => Err(CliError::Migration(format!(
459            "Failed to create __migrations table: {}",
460            e
461        ))),
462    }
463}
464
465/// 返回 __migrations 表名(Oracle 需双引号包裹,因 `__` 前缀在 Oracle 中非法)
466fn migrations_table_name(db_type: DbType) -> &'static str {
467    match db_type {
468        DbType::Oracle => "\"__migrations\"",
469        _ => "__migrations",
470    }
471}
472
473/// Oracle 不允许 SQL 末尾分号,执行前去除
474fn prepare_sql_for_db(sql: &str, db_type: DbType) -> String {
475    if db_type == DbType::Oracle {
476        sql.trim_end().trim_end_matches(';').to_string()
477    } else {
478        sql.to_string()
479    }
480}
481
482/// 查询已应用的迁移版本
483///
484/// 执行 `SELECT version FROM __migrations` 查询已应用的迁移版本集合。
485/// 用于 `migrate:status` 在线模式区分已应用/未应用迁移。
486async fn fetch_applied_versions(
487    conn: &mut Box<dyn Connection>,
488    db_type: DbType,
489) -> Result<std::collections::HashSet<String>, CliError> {
490    let table = migrations_table_name(db_type);
491    let sql = format!("SELECT version FROM {}", table);
492    let rows = conn
493        .query(&sql)
494        .await
495        .map_err(|e| CliError::Migration(format!("Failed to query __migrations: {}", e)))?;
496
497    let mut versions = std::collections::HashSet::new();
498    for row in &rows {
499        use sz_rust_core::orm::Value;
500        let val = row.get("version").or_else(|| row.get("VERSION"));
501        if let Some(val) = val {
502            match val {
503                Value::String(s) => versions.insert(s.clone()),
504                Value::I64(i) => versions.insert(i.to_string()),
505                Value::I32(i) => versions.insert(i.to_string()),
506                _ => false,
507            };
508        }
509    }
510    Ok(versions)
511}
512
513/// 删除 __migrations 表中的迁移记录
514///
515/// 参数化绑定防 SQL 注入(铁律 §1):`version` 虽源自迁移文件名而非用户输入,
516/// 仍统一走 `execute_with_params` 参数化路径,杜绝任何拼接风险。
517/// 插入迁移记录到 __migrations 表
518async fn insert_migration_record(
519    conn: &mut Box<dyn Connection>,
520    version: &str,
521    name: &str,
522    db_type: DbType,
523) -> Result<(), CliError> {
524    if !matches!(
525        db_type,
526        DbType::PostgreSQL | DbType::Sqlite | DbType::MySQL | DbType::Oracle | DbType::SqlServer
527    ) {
528        return Ok(());
529    }
530    use sz_rust_core::orm::Value;
531    let table = migrations_table_name(db_type);
532    let sql = format!(
533        "INSERT INTO {} (version, name, batch) VALUES (?, ?, 1)",
534        table
535    );
536    conn.execute_with_params(
537        &sql,
538        &[
539            Value::String(version.to_string()),
540            Value::String(name.to_string()),
541        ],
542    )
543    .await
544    .map_err(|e| CliError::Migration(format!("Failed to insert migration record: {}", e)))?;
545    conn.commit()
546        .await
547        .map_err(|e| CliError::Migration(format!("Failed to commit: {}", e)))?;
548    Ok(())
549}
550
551async fn delete_migration_record(
552    conn: &mut Box<dyn Connection>,
553    version: &str,
554    db_type: DbType,
555) -> Result<(), CliError> {
556    if !matches!(
557        db_type,
558        DbType::PostgreSQL | DbType::Sqlite | DbType::MySQL | DbType::Oracle | DbType::SqlServer
559    ) {
560        return Ok(());
561    }
562    use sz_rust_core::orm::Value;
563    let table = migrations_table_name(db_type);
564    let sql = format!("DELETE FROM {} WHERE version = ?", table);
565    conn.execute_with_params(&sql, &[Value::String(version.to_string())])
566        .await
567        .map_err(|e| CliError::Migration(format!("Failed to delete migration record: {}", e)))?;
568    conn.commit()
569        .await
570        .map_err(|e| CliError::Migration(format!("Failed to commit: {}", e)))?;
571    Ok(())
572}
573
574/// 解析迁移目录,返回排序后的迁移列表
575///
576/// 整合 [`FileMigrationResolver`],对齐 sz-orm 的迁移文件命名约定。
577///
578/// # 错误
579///
580/// - [`CliError::Migration`]:目录读取失败或迁移文件解析失败
581fn resolve_migrations(path: &Path, db_type: DbType) -> Result<Vec<Migration>, CliError> {
582    let resolver = FileMigrationResolver::new(path.to_path_buf());
583    resolver
584        .resolve(db_type)
585        .map_err(|e| CliError::Migration(format!("Failed to resolve migrations: {}", e)))
586}
587
588/// 打印 SQL 代码块(带标题分隔符)
589///
590/// 格式:
591/// ```text
592///   --- <title> ---
593///   <sql content>
594///   ----------------
595/// ```
596/// 返回格式化后的 SQL 代码块字符串(空 SQL 返回空串),由调用方输出。
597fn print_sql_block(title: &str, sql: &str) -> String {
598    if sql.is_empty() {
599        return String::new();
600    }
601    let mut out = format!("  --- {} ---\n", title);
602    for line in sql.lines() {
603        out.push_str(&format!("  {}\n", line));
604    }
605    out.push_str(&format!("  {}\n", "-".repeat(title.len() + 8)));
606    out
607}
608
609#[cfg(test)]
610mod tests {
611    use super::*;
612    use std::fs;
613    use std::io::Write;
614
615    /// 创建测试用迁移文件(`<version>_<name>_up.sql` + `<version>_<name>_down.sql`)
616    fn create_test_migration(dir: &Path, version: &str, name: &str) {
617        let up_name = format!("{}_{}_up.sql", version, name);
618        let down_name = format!("{}_{}_down.sql", version, name);
619
620        let up_path = dir.join(up_name);
621        let down_path = dir.join(down_name);
622
623        let mut up_file = fs::File::create(&up_path).unwrap();
624        writeln!(up_file, "-- {} up", name).unwrap();
625
626        let mut down_file = fs::File::create(&down_path).unwrap();
627        writeln!(down_file, "-- {} down", name).unwrap();
628    }
629
630    #[test]
631    fn test_resolve_migrations_empty() {
632        let temp = tempfile::tempdir().unwrap();
633        let path = temp.path().to_path_buf();
634        let result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
635        assert!(result.is_empty());
636    }
637
638    #[test]
639    fn test_resolve_migrations_with_files() {
640        let temp = tempfile::tempdir().unwrap();
641        let path = temp.path().to_path_buf();
642
643        create_test_migration(&path, "001", "create_users");
644        create_test_migration(&path, "002", "add_index");
645
646        let result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
647        assert_eq!(result.len(), 2);
648        assert_eq!(result[0].version, "001");
649        assert_eq!(result[0].name, "create_users");
650        assert_eq!(result[1].version, "002");
651        assert_eq!(result[1].name, "add_index");
652    }
653
654    #[test]
655    fn test_resolve_migrations_returns_sql_content() {
656        let temp = tempfile::tempdir().unwrap();
657        let path = temp.path().to_path_buf();
658
659        let up_path = path.join("001_init_up.sql");
660        let down_path = path.join("001_init_down.sql");
661        fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
662        fs::write(&down_path, "DROP TABLE users;").unwrap();
663
664        let result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
665        assert_eq!(result.len(), 1);
666        assert!(result[0].sql_up.contains("CREATE TABLE users"));
667        assert!(result[0].sql_down.contains("DROP TABLE users"));
668    }
669
670    #[test]
671    fn test_resolve_migrations_supports_multiple_db_types() {
672        let temp = tempfile::tempdir().unwrap();
673        let path = temp.path().to_path_buf();
674        create_test_migration(&path, "001", "init");
675
676        let mysql_result = resolve_migrations(&path, DbType::MySQL).unwrap();
677        let pg_result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
678
679        assert_eq!(mysql_result.len(), 1);
680        assert_eq!(pg_result.len(), 1);
681    }
682
683    #[tokio::test]
684    async fn test_execute_status_nonexistent_dir() {
685        let result = execute_status("/nonexistent/path/migrations").await;
686        assert!(matches!(result, Err(CliError::Migration(_))));
687    }
688
689    #[tokio::test]
690    async fn test_execute_status_empty_dir() {
691        let temp = tempfile::tempdir().unwrap();
692        let path = temp.path().to_str().unwrap();
693        let result = execute_status(path).await;
694        assert!(result.is_ok());
695    }
696
697    #[tokio::test]
698    async fn test_execute_status_with_migrations() {
699        let temp = tempfile::tempdir().unwrap();
700        let path = temp.path().to_path_buf();
701        create_test_migration(&path, "001", "create_users");
702
703        let path_str = temp.path().to_str().unwrap();
704        let result = execute_status(path_str).await;
705        assert!(result.is_ok());
706    }
707
708    #[tokio::test]
709    async fn test_execute_status_with_invalid_db_type() {
710        let temp = tempfile::tempdir().unwrap();
711        let path = temp.path().to_str().unwrap();
712        let result = execute_status_with(path, "invalid_db_type", false).await;
713        assert!(matches!(result, Err(CliError::Migration(_))));
714    }
715
716    #[tokio::test]
717    async fn test_execute_status_with_show_sql() {
718        let temp = tempfile::tempdir().unwrap();
719        let path = temp.path().to_path_buf();
720
721        let up_path = path.join("001_init_up.sql");
722        let down_path = path.join("001_init_down.sql");
723        fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
724        fs::write(&down_path, "DROP TABLE users;").unwrap();
725
726        let path_str = temp.path().to_str().unwrap();
727        let result = execute_status_with(path_str, "postgres", true).await;
728        assert!(result.is_ok());
729    }
730
731    #[tokio::test]
732    async fn test_execute_migrate_nonexistent_dir() {
733        let args = MigrateArgs {
734            rollback: false,
735            path: "/nonexistent/migrations".to_string(),
736            db_type: "postgres".to_string(),
737            show_sql: false,
738            url: None,
739        };
740        let result = execute_migrate(&args).await;
741        assert!(matches!(result, Err(CliError::Migration(_))));
742    }
743
744    #[tokio::test]
745    async fn test_execute_migrate_empty_dir() {
746        let temp = tempfile::tempdir().unwrap();
747        let args = MigrateArgs {
748            rollback: false,
749            path: temp.path().to_str().unwrap().to_string(),
750            db_type: "postgres".to_string(),
751            show_sql: false,
752            url: None,
753        };
754        let result = execute_migrate(&args).await;
755        assert!(result.is_ok());
756    }
757
758    #[tokio::test]
759    async fn test_execute_migrate_with_files_offline() {
760        let temp = tempfile::tempdir().unwrap();
761        let path = temp.path().to_path_buf();
762        create_test_migration(&path, "001", "create_users");
763
764        let args = MigrateArgs {
765            rollback: false,
766            path: temp.path().to_str().unwrap().to_string(),
767            db_type: "postgres".to_string(),
768            show_sql: false,
769            url: None,
770        };
771        let result = execute_migrate(&args).await;
772        assert!(result.is_ok());
773    }
774
775    #[tokio::test]
776    async fn test_execute_migrate_with_show_sql_offline() {
777        let temp = tempfile::tempdir().unwrap();
778        let path = temp.path().to_path_buf();
779
780        let up_path = path.join("001_init_up.sql");
781        let down_path = path.join("001_init_down.sql");
782        fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
783        fs::write(&down_path, "DROP TABLE users;").unwrap();
784
785        let args = MigrateArgs {
786            rollback: false,
787            path: temp.path().to_str().unwrap().to_string(),
788            db_type: "postgres".to_string(),
789            show_sql: true,
790            url: None,
791        };
792        let result = execute_migrate(&args).await;
793        assert!(result.is_ok());
794    }
795
796    #[tokio::test]
797    async fn test_execute_migrate_with_invalid_db_type() {
798        let temp = tempfile::tempdir().unwrap();
799        let args = MigrateArgs {
800            rollback: false,
801            path: temp.path().to_str().unwrap().to_string(),
802            db_type: "invalid_db_type".to_string(),
803            show_sql: false,
804            url: None,
805        };
806        let result = execute_migrate(&args).await;
807        assert!(matches!(result, Err(CliError::Migration(_))));
808    }
809
810    #[tokio::test]
811    async fn test_execute_migrate_rollback_offline() {
812        let temp = tempfile::tempdir().unwrap();
813        let path = temp.path().to_path_buf();
814        create_test_migration(&path, "001", "create_users");
815        create_test_migration(&path, "002", "add_index");
816
817        let args = MigrateArgs {
818            rollback: true,
819            path: temp.path().to_str().unwrap().to_string(),
820            db_type: "postgres".to_string(),
821            show_sql: false,
822            url: None,
823        };
824        let result = execute_migrate(&args).await;
825        assert!(result.is_ok());
826    }
827
828    #[tokio::test]
829    async fn test_execute_migrate_rollback_with_show_sql_offline() {
830        let temp = tempfile::tempdir().unwrap();
831        let path = temp.path().to_path_buf();
832
833        let up_path = path.join("001_init_up.sql");
834        let down_path = path.join("001_init_down.sql");
835        fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
836        fs::write(&down_path, "DROP TABLE users;").unwrap();
837
838        let args = MigrateArgs {
839            rollback: true,
840            path: temp.path().to_str().unwrap().to_string(),
841            db_type: "postgres".to_string(),
842            show_sql: true,
843            url: None,
844        };
845        let result = execute_migrate(&args).await;
846        assert!(result.is_ok());
847    }
848
849    #[test]
850    fn test_print_sql_block_empty_sql() {
851        // 空 SQL 不输出任何内容
852        let out = print_sql_block("SQL UP", "");
853        assert!(out.is_empty(), "空 SQL 不应产生输出,实际: {:?}", out);
854    }
855
856    #[test]
857    fn test_print_sql_block_with_content() {
858        let out = print_sql_block("SQL UP", "CREATE TABLE users (id INT);");
859        assert!(out.contains("--- SQL UP ---"), "应包含标题, 实际: {out}");
860        assert!(
861            out.contains("CREATE TABLE users (id INT);"),
862            "应包含 SQL 内容, 实际: {out}"
863        );
864        assert!(
865            out.contains(&"-".repeat("SQL UP".len() + 8)),
866            "应以分隔线结尾, 实际: {:?}",
867            out
868        );
869    }
870
871    #[tokio::test]
872    async fn test_execute_status_full_offline_no_url() {
873        let temp = tempfile::tempdir().unwrap();
874        let path = temp.path().to_path_buf();
875        create_test_migration(&path, "001", "init");
876
877        let path_str = temp.path().to_str().unwrap();
878        let result = execute_status_full(path_str, "postgres", false, None).await;
879        assert!(result.is_ok());
880    }
881
882    #[tokio::test]
883    async fn test_execute_status_full_offline_with_show_sql() {
884        let temp = tempfile::tempdir().unwrap();
885        let path = temp.path().to_path_buf();
886        let up_path = path.join("001_init_up.sql");
887        let down_path = path.join("001_init_down.sql");
888        fs::write(&up_path, "CREATE TABLE t (id INT);").unwrap();
889        fs::write(&down_path, "DROP TABLE t;").unwrap();
890
891        let path_str = temp.path().to_str().unwrap();
892        let result = execute_status_full(path_str, "postgres", true, None).await;
893        assert!(result.is_ok());
894    }
895
896    #[tokio::test]
897    async fn test_execute_status_full_invalid_db_type() {
898        let temp = tempfile::tempdir().unwrap();
899        let path_str = temp.path().to_str().unwrap();
900        let result = execute_status_full(path_str, "invalid_db", false, None).await;
901        assert!(matches!(result, Err(CliError::Migration(_))));
902    }
903
904    #[tokio::test]
905    async fn test_execute_migrate_online_with_invalid_url_returns_error() {
906        let temp = tempfile::tempdir().unwrap();
907        let path = temp.path().to_path_buf();
908        create_test_migration(&path, "001", "init");
909
910        let args = MigrateArgs {
911            rollback: false,
912            path: temp.path().to_str().unwrap().to_string(),
913            db_type: "postgres".to_string(),
914            show_sql: false,
915            url: Some("postgres://invalid:invalid@127.0.0.1:1/invalid".to_string()),
916        };
917        let result = execute_migrate(&args).await;
918        // 连接失败应返回错误(不 panic)
919        assert!(result.is_err());
920    }
921
922    #[tokio::test]
923    async fn test_create_connection_oracle_dsn_attempts_connect() {
924        let result = create_connection(
925            "oracle://invalid:invalid@127.0.0.1:1/invalid",
926            DbType::Oracle,
927        )
928        .await;
929        match result {
930            Err(e) => {
931                let err = format!("{}", e);
932                assert!(
933                    !err.contains("not supported"),
934                    "Oracle 应尝试连接而非拒绝: {err}"
935                );
936            }
937            Ok(_) => panic!("Oracle 连接应失败"),
938        }
939    }
940
941    #[tokio::test]
942    async fn test_create_connection_mssql_dsn_attempts_connect() {
943        let result = create_connection(
944            "mssql://invalid:invalid@127.0.0.1:1/invalid",
945            DbType::SqlServer,
946        )
947        .await;
948        match result {
949            Err(e) => {
950                let err = format!("{}", e);
951                assert!(
952                    !err.contains("not supported"),
953                    "MSSQL 应尝试连接而非拒绝: {err}"
954                );
955            }
956            Ok(_) => panic!("MSSQL 连接应失败"),
957        }
958    }
959
960    #[test]
961    fn test_migrations_table_name_postgres() {
962        assert_eq!(migrations_table_name(DbType::PostgreSQL), "__migrations");
963    }
964
965    #[test]
966    fn test_migrations_table_name_mysql() {
967        assert_eq!(migrations_table_name(DbType::MySQL), "__migrations");
968    }
969
970    #[test]
971    fn test_migrations_table_name_sqlite() {
972        assert_eq!(migrations_table_name(DbType::Sqlite), "__migrations");
973    }
974
975    #[test]
976    fn test_migrations_table_name_oracle() {
977        assert_eq!(migrations_table_name(DbType::Oracle), "\"__migrations\"");
978    }
979
980    #[test]
981    fn test_migrations_table_name_mssql() {
982        assert_eq!(migrations_table_name(DbType::SqlServer), "__migrations");
983    }
984
985    #[test]
986    fn test_prepare_sql_for_db_postgres() {
987        let sql = "CREATE TABLE users (id INT);";
988        assert_eq!(prepare_sql_for_db(sql, DbType::PostgreSQL), sql);
989    }
990
991    #[test]
992    fn test_prepare_sql_for_db_oracle_strips_semicolon() {
993        let sql = "CREATE TABLE users (id INT);";
994        assert_eq!(
995            prepare_sql_for_db(sql, DbType::Oracle),
996            "CREATE TABLE users (id INT)"
997        );
998    }
999
1000    #[test]
1001    fn test_prepare_sql_for_db_oracle_no_semicolon() {
1002        let sql = "CREATE TABLE users (id INT)";
1003        assert_eq!(prepare_sql_for_db(sql, DbType::Oracle), sql);
1004    }
1005
1006    #[test]
1007    fn test_prepare_sql_for_db_oracle_trailing_whitespace() {
1008        let sql = "CREATE TABLE users (id INT);  \n";
1009        assert_eq!(
1010            prepare_sql_for_db(sql, DbType::Oracle),
1011            "CREATE TABLE users (id INT)"
1012        );
1013    }
1014
1015    #[test]
1016    fn test_prepare_sql_for_db_mysql_no_change() {
1017        let sql = "CREATE TABLE users (id INT);";
1018        assert_eq!(prepare_sql_for_db(sql, DbType::MySQL), sql);
1019    }
1020}