use std::path::{Path, PathBuf};
use clap::Args;
use sz_rust_core::orm::migration::{FileMigrationResolver, Migration, MigrationResolver};
use sz_rust_core::orm::{Connection, DbType};
use crate::error::CliError;
#[derive(Args, Debug)]
pub struct MigrateArgs {
#[arg(long)]
pub rollback: bool,
#[arg(short = 'p', long, default_value = "migrations")]
pub path: String,
#[arg(long, default_value = "postgres")]
pub db_type: String,
#[arg(long)]
pub show_sql: bool,
#[arg(long)]
pub url: Option<String>,
}
pub async fn execute_migrate(args: &MigrateArgs) -> Result<(), CliError> {
let path = PathBuf::from(&args.path);
if !path.exists() {
return Err(CliError::Migration(format!(
"Migration directory not found: {}",
path.display()
)));
}
let db_type = DbType::from_str(&args.db_type)
.ok_or_else(|| CliError::Migration(format!("Unknown database type: {}", args.db_type)))?;
let migrations = resolve_migrations(&path, db_type)?;
if migrations.is_empty() {
println!("No migrations found in: {}", path.display());
return Ok(());
}
match &args.url {
None => execute_migrate_offline(args, &migrations),
Some(url) => execute_migrate_online(args, &migrations, url, db_type).await,
}
}
fn execute_migrate_offline(args: &MigrateArgs, migrations: &[Migration]) -> Result<(), CliError> {
if args.rollback {
println!("Rolling back last batch in: {}", args.path);
if let Some(last) = migrations.last() {
println!(" Would rollback: {} ({})", last.version, last.name);
if args.show_sql {
println!("{}", print_sql_block("SQL DOWN", &last.sql_down));
}
}
println!("Note: Actual rollback requires database connection (offline mode).");
} else {
println!("Running migrations in: {}", args.path);
for m in migrations {
println!(" Would apply: {} ({})", m.version, m.name);
if args.show_sql {
println!("{}", print_sql_block("SQL UP", &m.sql_up));
}
}
println!(
"Total: {} migration(s). Note: Actual execution requires database connection (offline mode).",
migrations.len()
);
}
Ok(())
}
async fn execute_migrate_online(
args: &MigrateArgs,
migrations: &[Migration],
url: &str,
db_type: DbType,
) -> Result<(), CliError> {
let mut conn = create_connection(url, db_type).await?;
if args.rollback {
let last = migrations
.last()
.ok_or_else(|| CliError::Migration("No migrations to rollback".to_string()))?;
println!("Rolling back: {} ({})", last.version, last.name);
if args.show_sql {
println!("{}", print_sql_block("SQL DOWN", &last.sql_down));
}
if !last.sql_down.is_empty() {
let sql = prepare_sql_for_db(&last.sql_down, db_type);
conn.execute(&sql)
.await
.map_err(|e| CliError::Migration(format!("Rollback failed: {}", e)))?;
}
delete_migration_record(&mut conn, &last.version, db_type).await?;
println!("Rollback completed: {} ({})", last.version, last.name);
} else {
ensure_migrations_table(&mut conn, db_type).await?;
let applied = fetch_applied_versions(&mut conn, db_type).await?;
let pending: Vec<&Migration> = migrations
.iter()
.filter(|m| !applied.contains(&m.version))
.collect();
if pending.is_empty() {
println!("No pending migrations. Database is up to date.");
return Ok(());
}
println!("Running {} pending migration(s):", pending.len());
let mut applied_count = 0;
for m in &pending {
if args.show_sql {
println!("{}", print_sql_block("SQL UP", &m.sql_up));
}
let sql = prepare_sql_for_db(&m.sql_up, db_type);
conn.execute(&sql).await.map_err(|e| {
CliError::Migration(format!("Migration {} failed: {}", m.version, e))
})?;
insert_migration_record(&mut conn, &m.version, &m.name, db_type).await?;
println!(" Applied: {}", m.version);
applied_count += 1;
}
println!("Migration completed: {} applied.", applied_count);
}
Ok(())
}
pub async fn execute_status(path: &str) -> Result<(), CliError> {
execute_status_full(path, "postgres", false, None).await
}
pub async fn execute_status_with(
path: &str,
db_type_str: &str,
show_sql: bool,
) -> Result<(), CliError> {
execute_status_full(path, db_type_str, show_sql, None).await
}
pub async fn execute_status_full(
path: &str,
db_type_str: &str,
show_sql: bool,
url: Option<&str>,
) -> Result<(), CliError> {
let path_buf = PathBuf::from(path);
if !path_buf.exists() {
return Err(CliError::Migration(format!(
"Migration directory not found: {}",
path_buf.display()
)));
}
let db_type = DbType::from_str(db_type_str)
.ok_or_else(|| CliError::Migration(format!("Unknown database type: {}", db_type_str)))?;
let migrations = resolve_migrations(&path_buf, db_type)?;
if migrations.is_empty() {
println!("No migrations found in: {}", path_buf.display());
return Ok(());
}
let applied_versions = if let Some(url) = url {
let mut conn = create_connection(url, db_type).await?;
ensure_migrations_table(&mut conn, db_type).await?;
fetch_applied_versions(&mut conn, db_type).await?
} else {
std::collections::HashSet::new()
};
println!(
"{:<15} {:<30} {:<20}",
"Version", "Migration Name", "Status"
);
println!("{}", "-".repeat(65));
for m in &migrations {
let status = if applied_versions.contains(&m.version) {
"Applied"
} else if url.is_some() {
"Pending"
} else {
"Pending*"
};
println!("{:<15} {:<30} {:<20}", m.version, m.name, status);
if show_sql {
println!("{}", print_sql_block("SQL UP", &m.sql_up));
println!("{}", print_sql_block("SQL DOWN", &m.sql_down));
}
}
println!();
if url.is_some() {
let applied = migrations
.iter()
.filter(|m| applied_versions.contains(&m.version))
.count();
println!(
"Total: {} migration(s), {} applied, {} pending.",
migrations.len(),
applied,
migrations.len() - applied
);
} else {
println!("* Status cannot be determined without database connection (offline mode).");
}
Ok(())
}
async fn create_connection(url: &str, db_type: DbType) -> Result<Box<dyn Connection>, CliError> {
use std::sync::Arc;
use sz_orm_sqlx::any_driver::AnyPool;
if db_type == DbType::SqlServer {
use sz_orm_mssql::{MssqlConnectionFactory, MssqlPoolHandle};
use sz_rust_core::orm::ConnectionFactory;
let rest = url
.strip_prefix("mssql://")
.or_else(|| url.strip_prefix("sqlserver://"))
.ok_or_else(|| CliError::Migration("Invalid MSSQL DSN".to_string()))?;
let (userinfo, hostinfo) = rest
.split_once('@')
.ok_or_else(|| CliError::Migration("MSSQL DSN missing @".to_string()))?;
let (username, password) = userinfo
.split_once(':')
.ok_or_else(|| CliError::Migration("MSSQL DSN missing password".to_string()))?;
let (host_port, database) = hostinfo
.split_once('/')
.ok_or_else(|| CliError::Migration("MSSQL DSN missing database".to_string()))?;
let (host, port) = host_port.split_once(':').unwrap_or((host_port, "1433"));
let ado = format!(
"Server={host},{port};Database={database};User Id={username};Password={password};\
Encrypt=false;TrustServerCertificate=true;"
);
let pool = MssqlPoolHandle::connect(&ado)
.await
.map_err(|e| CliError::Migration(format!("MSSQL connect failed: {e}")))?;
let factory = MssqlConnectionFactory::new(Arc::new(pool));
let conn = factory
.create()
.await
.map_err(|e| CliError::Migration(format!("MSSQL acquire failed: {e}")))?;
return Ok(conn);
}
if db_type == DbType::Oracle {
use sz_orm_oracle::{OracleConnectionFactory, OraclePoolHandle};
use sz_rust_core::orm::ConnectionFactory;
let rest = url
.strip_prefix("oracle://")
.ok_or_else(|| CliError::Migration("Invalid Oracle DSN".to_string()))?;
let (userinfo, hostinfo) = rest
.split_once('@')
.ok_or_else(|| CliError::Migration("Oracle DSN missing @".to_string()))?;
let (username, password) = userinfo
.split_once(':')
.ok_or_else(|| CliError::Migration("Oracle DSN missing password".to_string()))?;
let pool = OraclePoolHandle::connect(username, password, hostinfo)
.map_err(|e| CliError::Migration(format!("Oracle connect failed: {e}")))?;
let pool_arc = Arc::new(pool);
let factory = OracleConnectionFactory::new(pool_arc.clone());
std::mem::forget(pool_arc);
let conn = factory
.create()
.await
.map_err(|e| CliError::Migration(format!("Oracle acquire failed: {e}")))?;
return Ok(conn);
}
let pool = AnyPool::connect(url)
.await
.map_err(|e| CliError::Migration(format!("{:?} connect failed: {}", db_type, e)))?;
let conn = pool
.create()
.await
.map_err(|e| CliError::Migration(format!("{:?} acquire failed: {}", db_type, e)))?;
Ok(Box::new(conn))
}
async fn ensure_migrations_table(
conn: &mut Box<dyn Connection>,
db_type: DbType,
) -> Result<(), CliError> {
let sql: &str = match db_type {
DbType::PostgreSQL | DbType::Sqlite => {
"CREATE TABLE IF NOT EXISTS __migrations (
version VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
batch INTEGER NOT NULL,
executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)"
}
DbType::MySQL => {
"CREATE TABLE IF NOT EXISTS __migrations (
version VARCHAR(255) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
batch INT NOT NULL,
executed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
)"
}
DbType::Oracle => {
"CREATE TABLE \"__migrations\" (\
version VARCHAR2(255) PRIMARY KEY,\
name VARCHAR2(255) NOT NULL,\
batch NUMBER(10) NOT NULL,\
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\
)"
}
DbType::SqlServer => {
"CREATE TABLE __migrations (\
version NVARCHAR(255) PRIMARY KEY,\
name NVARCHAR(255) NOT NULL,\
batch INT NOT NULL,\
executed_at DATETIME2 NOT NULL DEFAULT CURRENT_TIMESTAMP\
)"
}
_ => {
return Err(CliError::Migration(format!(
"Cannot ensure __migrations table for db_type {:?}",
db_type
)))
}
};
let result = conn.execute(sql).await;
match result {
Ok(_) => Ok(()),
Err(e) if db_type == DbType::SqlServer => {
let err_msg = format!("{}", e);
if err_msg.contains("2714") || err_msg.contains("already exists") {
Ok(())
} else {
Err(CliError::Migration(format!(
"Failed to create __migrations table: {}",
e
)))
}
}
Err(e) if db_type == DbType::Oracle => {
let err_msg = format!("{}", e);
if err_msg.contains("ORA-00955") || err_msg.contains("already exists") {
Ok(())
} else {
Err(CliError::Migration(format!(
"Failed to create __migrations table: {}",
e
)))
}
}
Err(e) => Err(CliError::Migration(format!(
"Failed to create __migrations table: {}",
e
))),
}
}
fn migrations_table_name(db_type: DbType) -> &'static str {
match db_type {
DbType::Oracle => "\"__migrations\"",
_ => "__migrations",
}
}
fn prepare_sql_for_db(sql: &str, db_type: DbType) -> String {
if db_type == DbType::Oracle {
sql.trim_end().trim_end_matches(';').to_string()
} else {
sql.to_string()
}
}
async fn fetch_applied_versions(
conn: &mut Box<dyn Connection>,
db_type: DbType,
) -> Result<std::collections::HashSet<String>, CliError> {
let table = migrations_table_name(db_type);
let sql = format!("SELECT version FROM {}", table);
let rows = conn
.query(&sql)
.await
.map_err(|e| CliError::Migration(format!("Failed to query __migrations: {}", e)))?;
let mut versions = std::collections::HashSet::new();
for row in &rows {
use sz_rust_core::orm::Value;
let val = row.get("version").or_else(|| row.get("VERSION"));
if let Some(val) = val {
match val {
Value::String(s) => versions.insert(s.clone()),
Value::I64(i) => versions.insert(i.to_string()),
Value::I32(i) => versions.insert(i.to_string()),
_ => false,
};
}
}
Ok(versions)
}
async fn insert_migration_record(
conn: &mut Box<dyn Connection>,
version: &str,
name: &str,
db_type: DbType,
) -> Result<(), CliError> {
if !matches!(
db_type,
DbType::PostgreSQL | DbType::Sqlite | DbType::MySQL | DbType::Oracle | DbType::SqlServer
) {
return Ok(());
}
use sz_rust_core::orm::Value;
let table = migrations_table_name(db_type);
let sql = format!(
"INSERT INTO {} (version, name, batch) VALUES (?, ?, 1)",
table
);
conn.execute_with_params(
&sql,
&[
Value::String(version.to_string()),
Value::String(name.to_string()),
],
)
.await
.map_err(|e| CliError::Migration(format!("Failed to insert migration record: {}", e)))?;
conn.commit()
.await
.map_err(|e| CliError::Migration(format!("Failed to commit: {}", e)))?;
Ok(())
}
async fn delete_migration_record(
conn: &mut Box<dyn Connection>,
version: &str,
db_type: DbType,
) -> Result<(), CliError> {
if !matches!(
db_type,
DbType::PostgreSQL | DbType::Sqlite | DbType::MySQL | DbType::Oracle | DbType::SqlServer
) {
return Ok(());
}
use sz_rust_core::orm::Value;
let table = migrations_table_name(db_type);
let sql = format!("DELETE FROM {} WHERE version = ?", table);
conn.execute_with_params(&sql, &[Value::String(version.to_string())])
.await
.map_err(|e| CliError::Migration(format!("Failed to delete migration record: {}", e)))?;
conn.commit()
.await
.map_err(|e| CliError::Migration(format!("Failed to commit: {}", e)))?;
Ok(())
}
fn resolve_migrations(path: &Path, db_type: DbType) -> Result<Vec<Migration>, CliError> {
let resolver = FileMigrationResolver::new(path.to_path_buf());
resolver
.resolve(db_type)
.map_err(|e| CliError::Migration(format!("Failed to resolve migrations: {}", e)))
}
fn print_sql_block(title: &str, sql: &str) -> String {
if sql.is_empty() {
return String::new();
}
let mut out = format!(" --- {} ---\n", title);
for line in sql.lines() {
out.push_str(&format!(" {}\n", line));
}
out.push_str(&format!(" {}\n", "-".repeat(title.len() + 8)));
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
fn create_test_migration(dir: &Path, version: &str, name: &str) {
let up_name = format!("{}_{}_up.sql", version, name);
let down_name = format!("{}_{}_down.sql", version, name);
let up_path = dir.join(up_name);
let down_path = dir.join(down_name);
let mut up_file = fs::File::create(&up_path).unwrap();
writeln!(up_file, "-- {} up", name).unwrap();
let mut down_file = fs::File::create(&down_path).unwrap();
writeln!(down_file, "-- {} down", name).unwrap();
}
#[test]
fn test_resolve_migrations_empty() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
let result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_resolve_migrations_with_files() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
create_test_migration(&path, "001", "create_users");
create_test_migration(&path, "002", "add_index");
let result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].version, "001");
assert_eq!(result[0].name, "create_users");
assert_eq!(result[1].version, "002");
assert_eq!(result[1].name, "add_index");
}
#[test]
fn test_resolve_migrations_returns_sql_content() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
let up_path = path.join("001_init_up.sql");
let down_path = path.join("001_init_down.sql");
fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
fs::write(&down_path, "DROP TABLE users;").unwrap();
let result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
assert_eq!(result.len(), 1);
assert!(result[0].sql_up.contains("CREATE TABLE users"));
assert!(result[0].sql_down.contains("DROP TABLE users"));
}
#[test]
fn test_resolve_migrations_supports_multiple_db_types() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
create_test_migration(&path, "001", "init");
let mysql_result = resolve_migrations(&path, DbType::MySQL).unwrap();
let pg_result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
assert_eq!(mysql_result.len(), 1);
assert_eq!(pg_result.len(), 1);
}
#[tokio::test]
async fn test_execute_status_nonexistent_dir() {
let result = execute_status("/nonexistent/path/migrations").await;
assert!(matches!(result, Err(CliError::Migration(_))));
}
#[tokio::test]
async fn test_execute_status_empty_dir() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_str().unwrap();
let result = execute_status(path).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_status_with_migrations() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
create_test_migration(&path, "001", "create_users");
let path_str = temp.path().to_str().unwrap();
let result = execute_status(path_str).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_status_with_invalid_db_type() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_str().unwrap();
let result = execute_status_with(path, "invalid_db_type", false).await;
assert!(matches!(result, Err(CliError::Migration(_))));
}
#[tokio::test]
async fn test_execute_status_with_show_sql() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
let up_path = path.join("001_init_up.sql");
let down_path = path.join("001_init_down.sql");
fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
fs::write(&down_path, "DROP TABLE users;").unwrap();
let path_str = temp.path().to_str().unwrap();
let result = execute_status_with(path_str, "postgres", true).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_migrate_nonexistent_dir() {
let args = MigrateArgs {
rollback: false,
path: "/nonexistent/migrations".to_string(),
db_type: "postgres".to_string(),
show_sql: false,
url: None,
};
let result = execute_migrate(&args).await;
assert!(matches!(result, Err(CliError::Migration(_))));
}
#[tokio::test]
async fn test_execute_migrate_empty_dir() {
let temp = tempfile::tempdir().unwrap();
let args = MigrateArgs {
rollback: false,
path: temp.path().to_str().unwrap().to_string(),
db_type: "postgres".to_string(),
show_sql: false,
url: None,
};
let result = execute_migrate(&args).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_migrate_with_files_offline() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
create_test_migration(&path, "001", "create_users");
let args = MigrateArgs {
rollback: false,
path: temp.path().to_str().unwrap().to_string(),
db_type: "postgres".to_string(),
show_sql: false,
url: None,
};
let result = execute_migrate(&args).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_migrate_with_show_sql_offline() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
let up_path = path.join("001_init_up.sql");
let down_path = path.join("001_init_down.sql");
fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
fs::write(&down_path, "DROP TABLE users;").unwrap();
let args = MigrateArgs {
rollback: false,
path: temp.path().to_str().unwrap().to_string(),
db_type: "postgres".to_string(),
show_sql: true,
url: None,
};
let result = execute_migrate(&args).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_migrate_with_invalid_db_type() {
let temp = tempfile::tempdir().unwrap();
let args = MigrateArgs {
rollback: false,
path: temp.path().to_str().unwrap().to_string(),
db_type: "invalid_db_type".to_string(),
show_sql: false,
url: None,
};
let result = execute_migrate(&args).await;
assert!(matches!(result, Err(CliError::Migration(_))));
}
#[tokio::test]
async fn test_execute_migrate_rollback_offline() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
create_test_migration(&path, "001", "create_users");
create_test_migration(&path, "002", "add_index");
let args = MigrateArgs {
rollback: true,
path: temp.path().to_str().unwrap().to_string(),
db_type: "postgres".to_string(),
show_sql: false,
url: None,
};
let result = execute_migrate(&args).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_migrate_rollback_with_show_sql_offline() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
let up_path = path.join("001_init_up.sql");
let down_path = path.join("001_init_down.sql");
fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
fs::write(&down_path, "DROP TABLE users;").unwrap();
let args = MigrateArgs {
rollback: true,
path: temp.path().to_str().unwrap().to_string(),
db_type: "postgres".to_string(),
show_sql: true,
url: None,
};
let result = execute_migrate(&args).await;
assert!(result.is_ok());
}
#[test]
fn test_print_sql_block_empty_sql() {
let out = print_sql_block("SQL UP", "");
assert!(out.is_empty(), "空 SQL 不应产生输出,实际: {:?}", out);
}
#[test]
fn test_print_sql_block_with_content() {
let out = print_sql_block("SQL UP", "CREATE TABLE users (id INT);");
assert!(out.contains("--- SQL UP ---"), "应包含标题, 实际: {out}");
assert!(
out.contains("CREATE TABLE users (id INT);"),
"应包含 SQL 内容, 实际: {out}"
);
assert!(
out.contains(&"-".repeat("SQL UP".len() + 8)),
"应以分隔线结尾, 实际: {:?}",
out
);
}
#[tokio::test]
async fn test_execute_status_full_offline_no_url() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
create_test_migration(&path, "001", "init");
let path_str = temp.path().to_str().unwrap();
let result = execute_status_full(path_str, "postgres", false, None).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_status_full_offline_with_show_sql() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
let up_path = path.join("001_init_up.sql");
let down_path = path.join("001_init_down.sql");
fs::write(&up_path, "CREATE TABLE t (id INT);").unwrap();
fs::write(&down_path, "DROP TABLE t;").unwrap();
let path_str = temp.path().to_str().unwrap();
let result = execute_status_full(path_str, "postgres", true, None).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_execute_status_full_invalid_db_type() {
let temp = tempfile::tempdir().unwrap();
let path_str = temp.path().to_str().unwrap();
let result = execute_status_full(path_str, "invalid_db", false, None).await;
assert!(matches!(result, Err(CliError::Migration(_))));
}
#[tokio::test]
async fn test_execute_migrate_online_with_invalid_url_returns_error() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().to_path_buf();
create_test_migration(&path, "001", "init");
let args = MigrateArgs {
rollback: false,
path: temp.path().to_str().unwrap().to_string(),
db_type: "postgres".to_string(),
show_sql: false,
url: Some("postgres://invalid:invalid@127.0.0.1:1/invalid".to_string()),
};
let result = execute_migrate(&args).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_create_connection_oracle_dsn_attempts_connect() {
let result = create_connection(
"oracle://invalid:invalid@127.0.0.1:1/invalid",
DbType::Oracle,
)
.await;
match result {
Err(e) => {
let err = format!("{}", e);
assert!(
!err.contains("not supported"),
"Oracle 应尝试连接而非拒绝: {err}"
);
}
Ok(_) => panic!("Oracle 连接应失败"),
}
}
#[tokio::test]
async fn test_create_connection_mssql_dsn_attempts_connect() {
let result = create_connection(
"mssql://invalid:invalid@127.0.0.1:1/invalid",
DbType::SqlServer,
)
.await;
match result {
Err(e) => {
let err = format!("{}", e);
assert!(
!err.contains("not supported"),
"MSSQL 应尝试连接而非拒绝: {err}"
);
}
Ok(_) => panic!("MSSQL 连接应失败"),
}
}
#[test]
fn test_migrations_table_name_postgres() {
assert_eq!(migrations_table_name(DbType::PostgreSQL), "__migrations");
}
#[test]
fn test_migrations_table_name_mysql() {
assert_eq!(migrations_table_name(DbType::MySQL), "__migrations");
}
#[test]
fn test_migrations_table_name_sqlite() {
assert_eq!(migrations_table_name(DbType::Sqlite), "__migrations");
}
#[test]
fn test_migrations_table_name_oracle() {
assert_eq!(migrations_table_name(DbType::Oracle), "\"__migrations\"");
}
#[test]
fn test_migrations_table_name_mssql() {
assert_eq!(migrations_table_name(DbType::SqlServer), "__migrations");
}
#[test]
fn test_prepare_sql_for_db_postgres() {
let sql = "CREATE TABLE users (id INT);";
assert_eq!(prepare_sql_for_db(sql, DbType::PostgreSQL), sql);
}
#[test]
fn test_prepare_sql_for_db_oracle_strips_semicolon() {
let sql = "CREATE TABLE users (id INT);";
assert_eq!(
prepare_sql_for_db(sql, DbType::Oracle),
"CREATE TABLE users (id INT)"
);
}
#[test]
fn test_prepare_sql_for_db_oracle_no_semicolon() {
let sql = "CREATE TABLE users (id INT)";
assert_eq!(prepare_sql_for_db(sql, DbType::Oracle), sql);
}
#[test]
fn test_prepare_sql_for_db_oracle_trailing_whitespace() {
let sql = "CREATE TABLE users (id INT); \n";
assert_eq!(
prepare_sql_for_db(sql, DbType::Oracle),
"CREATE TABLE users (id INT)"
);
}
#[test]
fn test_prepare_sql_for_db_mysql_no_change() {
let sql = "CREATE TABLE users (id INT);";
assert_eq!(prepare_sql_for_db(sql, DbType::MySQL), sql);
}
}