1use std::path::{Path, PathBuf};
32
33use clap::Args;
34use sz_orm_core::migration::{FileMigrationResolver, Migration, MigrationResolver};
35use sz_orm_core::DbType;
36
37use crate::error::CliError;
38
39#[derive(Args, Debug)]
43pub struct MigrateArgs {
44 #[arg(long)]
46 pub rollback: bool,
47
48 #[arg(short = 'p', long, default_value = "migrations")]
50 pub path: String,
51
52 #[arg(long, default_value = "postgres")]
58 pub db_type: String,
59
60 #[arg(long)]
62 pub show_sql: bool,
63}
64
65pub fn execute_migrate(args: &MigrateArgs) -> Result<(), CliError> {
75 let path = PathBuf::from(&args.path);
76
77 if !path.exists() {
78 return Err(CliError::Migration(format!(
79 "Migration directory not found: {}",
80 path.display()
81 )));
82 }
83
84 let db_type = DbType::from_str(&args.db_type)
85 .ok_or_else(|| CliError::Migration(format!("Unknown database type: {}", args.db_type)))?;
86
87 let migrations = resolve_migrations(&path, db_type)?;
88
89 if migrations.is_empty() {
90 println!("No migrations found in: {}", path.display());
91 return Ok(());
92 }
93
94 if args.rollback {
95 println!("Rolling back last batch in: {}", path.display());
96 if let Some(last) = migrations.last() {
98 println!(" Would rollback: {} ({})", last.version, last.name);
99 if args.show_sql {
100 print_sql_block("SQL DOWN", &last.sql_down);
101 }
102 }
103 println!("Note: Actual rollback requires database connection (offline mode).");
104 } else {
105 println!("Running migrations in: {}", path.display());
106 for m in &migrations {
107 println!(" Would apply: {} ({})", m.version, m.name);
108 if args.show_sql {
109 print_sql_block("SQL UP", &m.sql_up);
110 }
111 }
112 println!(
113 "Total: {} migration(s). Note: Actual execution requires database connection (offline mode).",
114 migrations.len()
115 );
116 }
117
118 Ok(())
119}
120
121pub fn execute_status(path: &str) -> Result<(), CliError> {
127 execute_status_with(path, "postgres", false)
128}
129
130pub fn execute_status_with(path: &str, db_type_str: &str, show_sql: bool) -> Result<(), CliError> {
138 let path_buf = PathBuf::from(path);
139
140 if !path_buf.exists() {
141 return Err(CliError::Migration(format!(
142 "Migration directory not found: {}",
143 path_buf.display()
144 )));
145 }
146
147 let db_type = DbType::from_str(db_type_str)
148 .ok_or_else(|| CliError::Migration(format!("Unknown database type: {}", db_type_str)))?;
149
150 let migrations = resolve_migrations(&path_buf, db_type)?;
151
152 if migrations.is_empty() {
153 println!("No migrations found in: {}", path_buf.display());
154 return Ok(());
155 }
156
157 println!(
159 "{:<15} {:<30} {:<20}",
160 "Version", "Migration Name", "Status"
161 );
162 println!("{}", "-".repeat(65));
163
164 for m in &migrations {
165 println!("{:<15} {:<30} {:<20}", m.version, m.name, "Pending*");
167 if show_sql {
168 print_sql_block("SQL UP", &m.sql_up);
169 print_sql_block("SQL DOWN", &m.sql_down);
170 }
171 }
172
173 println!();
174 println!("* Status cannot be determined without database connection (offline mode).");
175
176 Ok(())
177}
178
179fn resolve_migrations(path: &Path, db_type: DbType) -> Result<Vec<Migration>, CliError> {
187 let resolver = FileMigrationResolver::new(path.to_path_buf());
188 resolver
189 .resolve(db_type)
190 .map_err(|e| CliError::Migration(format!("Failed to resolve migrations: {}", e)))
191}
192
193fn print_sql_block(title: &str, sql: &str) {
202 if sql.is_empty() {
203 return;
204 }
205 println!(" --- {} ---", title);
206 for line in sql.lines() {
207 println!(" {}", line);
208 }
209 println!(" {}", "-".repeat(title.len() + 8));
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use std::fs;
216 use std::io::Write;
217
218 fn create_test_migration(dir: &Path, version: &str, name: &str) {
220 let up_name = format!("{}_{}_up.sql", version, name);
221 let down_name = format!("{}_{}_down.sql", version, name);
222
223 let up_path = dir.join(up_name);
224 let down_path = dir.join(down_name);
225
226 let mut up_file = fs::File::create(&up_path).unwrap();
227 writeln!(up_file, "-- {} up", name).unwrap();
228
229 let mut down_file = fs::File::create(&down_path).unwrap();
230 writeln!(down_file, "-- {} down", name).unwrap();
231 }
232
233 #[test]
234 fn test_resolve_migrations_empty() {
235 let temp = tempfile::tempdir().unwrap();
236 let path = temp.path().to_path_buf();
237 let result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
238 assert!(result.is_empty());
239 }
240
241 #[test]
242 fn test_resolve_migrations_with_files() {
243 let temp = tempfile::tempdir().unwrap();
244 let path = temp.path().to_path_buf();
245
246 create_test_migration(&path, "001", "create_users");
247 create_test_migration(&path, "002", "add_index");
248
249 let result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
250 assert_eq!(result.len(), 2);
251 assert_eq!(result[0].version, "001");
252 assert_eq!(result[0].name, "create_users");
253 assert_eq!(result[1].version, "002");
254 assert_eq!(result[1].name, "add_index");
255 }
256
257 #[test]
258 fn test_resolve_migrations_returns_sql_content() {
259 let temp = tempfile::tempdir().unwrap();
261 let path = temp.path().to_path_buf();
262
263 let up_path = path.join("001_init_up.sql");
264 let down_path = path.join("001_init_down.sql");
265 fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
266 fs::write(&down_path, "DROP TABLE users;").unwrap();
267
268 let result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
269 assert_eq!(result.len(), 1);
270 assert!(result[0].sql_up.contains("CREATE TABLE users"));
271 assert!(result[0].sql_down.contains("DROP TABLE users"));
272 }
273
274 #[test]
275 fn test_resolve_migrations_supports_multiple_db_types() {
276 let temp = tempfile::tempdir().unwrap();
278 let path = temp.path().to_path_buf();
279 create_test_migration(&path, "001", "init");
280
281 let mysql_result = resolve_migrations(&path, DbType::MySQL).unwrap();
282 let pg_result = resolve_migrations(&path, DbType::PostgreSQL).unwrap();
283
284 assert_eq!(mysql_result.len(), 1);
285 assert_eq!(pg_result.len(), 1);
286 }
287
288 #[test]
289 fn test_execute_status_nonexistent_dir() {
290 let result = execute_status("/nonexistent/path/migrations");
291 assert!(matches!(result, Err(CliError::Migration(_))));
292 }
293
294 #[test]
295 fn test_execute_status_empty_dir() {
296 let temp = tempfile::tempdir().unwrap();
297 let path = temp.path().to_str().unwrap();
298 let result = execute_status(path);
299 assert!(result.is_ok());
300 }
301
302 #[test]
303 fn test_execute_status_with_migrations() {
304 let temp = tempfile::tempdir().unwrap();
305 let path = temp.path().to_path_buf();
306 create_test_migration(&path, "001", "create_users");
307
308 let path_str = temp.path().to_str().unwrap();
309 let result = execute_status(path_str);
310 assert!(result.is_ok());
311 }
312
313 #[test]
314 fn test_execute_status_with_invalid_db_type() {
315 let temp = tempfile::tempdir().unwrap();
316 let path = temp.path().to_str().unwrap();
317 let result = execute_status_with(path, "invalid_db_type", false);
318 assert!(matches!(result, Err(CliError::Migration(_))));
319 }
320
321 #[test]
322 fn test_execute_status_with_show_sql() {
323 let temp = tempfile::tempdir().unwrap();
324 let path = temp.path().to_path_buf();
325
326 let up_path = path.join("001_init_up.sql");
327 let down_path = path.join("001_init_down.sql");
328 fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
329 fs::write(&down_path, "DROP TABLE users;").unwrap();
330
331 let path_str = temp.path().to_str().unwrap();
332 let result = execute_status_with(path_str, "postgres", true);
333 assert!(result.is_ok());
334 }
335
336 #[test]
337 fn test_execute_migrate_nonexistent_dir() {
338 let args = MigrateArgs {
339 rollback: false,
340 path: "/nonexistent/migrations".to_string(),
341 db_type: "postgres".to_string(),
342 show_sql: false,
343 };
344 let result = execute_migrate(&args);
345 assert!(matches!(result, Err(CliError::Migration(_))));
346 }
347
348 #[test]
349 fn test_execute_migrate_empty_dir() {
350 let temp = tempfile::tempdir().unwrap();
351 let args = MigrateArgs {
352 rollback: false,
353 path: temp.path().to_str().unwrap().to_string(),
354 db_type: "postgres".to_string(),
355 show_sql: false,
356 };
357 let result = execute_migrate(&args);
358 assert!(result.is_ok());
359 }
360
361 #[test]
362 fn test_execute_migrate_with_files() {
363 let temp = tempfile::tempdir().unwrap();
364 let path = temp.path().to_path_buf();
365 create_test_migration(&path, "001", "create_users");
366
367 let args = MigrateArgs {
368 rollback: false,
369 path: temp.path().to_str().unwrap().to_string(),
370 db_type: "postgres".to_string(),
371 show_sql: false,
372 };
373 let result = execute_migrate(&args);
374 assert!(result.is_ok());
375 }
376
377 #[test]
378 fn test_execute_migrate_with_show_sql() {
379 let temp = tempfile::tempdir().unwrap();
380 let path = temp.path().to_path_buf();
381
382 let up_path = path.join("001_init_up.sql");
383 let down_path = path.join("001_init_down.sql");
384 fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
385 fs::write(&down_path, "DROP TABLE users;").unwrap();
386
387 let args = MigrateArgs {
388 rollback: false,
389 path: temp.path().to_str().unwrap().to_string(),
390 db_type: "postgres".to_string(),
391 show_sql: true,
392 };
393 let result = execute_migrate(&args);
394 assert!(result.is_ok());
395 }
396
397 #[test]
398 fn test_execute_migrate_with_invalid_db_type() {
399 let temp = tempfile::tempdir().unwrap();
400 let args = MigrateArgs {
401 rollback: false,
402 path: temp.path().to_str().unwrap().to_string(),
403 db_type: "invalid_db_type".to_string(),
404 show_sql: false,
405 };
406 let result = execute_migrate(&args);
407 assert!(matches!(result, Err(CliError::Migration(_))));
408 }
409
410 #[test]
411 fn test_execute_migrate_rollback() {
412 let temp = tempfile::tempdir().unwrap();
413 let path = temp.path().to_path_buf();
414 create_test_migration(&path, "001", "create_users");
415 create_test_migration(&path, "002", "add_index");
416
417 let args = MigrateArgs {
418 rollback: true,
419 path: temp.path().to_str().unwrap().to_string(),
420 db_type: "postgres".to_string(),
421 show_sql: false,
422 };
423 let result = execute_migrate(&args);
424 assert!(result.is_ok());
425 }
426
427 #[test]
428 fn test_execute_migrate_rollback_with_show_sql() {
429 let temp = tempfile::tempdir().unwrap();
430 let path = temp.path().to_path_buf();
431
432 let up_path = path.join("001_init_up.sql");
433 let down_path = path.join("001_init_down.sql");
434 fs::write(&up_path, "CREATE TABLE users (id INT);").unwrap();
435 fs::write(&down_path, "DROP TABLE users;").unwrap();
436
437 let args = MigrateArgs {
438 rollback: true,
439 path: temp.path().to_str().unwrap().to_string(),
440 db_type: "postgres".to_string(),
441 show_sql: true,
442 };
443 let result = execute_migrate(&args);
444 assert!(result.is_ok());
445 }
446
447 #[test]
448 fn test_print_sql_block_empty_sql() {
449 print_sql_block("SQL UP", "");
451 }
452
453 #[test]
454 fn test_print_sql_block_with_content() {
455 print_sql_block("SQL UP", "CREATE TABLE users (id INT);");
457 }
458}