1use crate::config::Config;
4use crate::utils::{self, DatabaseType, Progress};
5use crate::MigrateCommands;
6use anyhow::{Context, Result};
7use colored::Colorize;
8use parsql_migrations::prelude::*;
9use sha2::{Sha256, Digest};
10use std::fs;
11use std::path::Path;
12
13pub fn handle_command(
14 command: MigrateCommands,
15 database_url: &str,
16 config: &Config,
17 verbose: bool,
18) -> Result<()> {
19 match command {
20 MigrateCommands::Create { name, migration_type } => {
21 create_migration(&name, &migration_type, &config.migrations.directory)?;
22 }
23
24 MigrateCommands::Run { database_url: cmd_url, dry_run, target } => {
25 let url = cmd_url.as_deref().unwrap_or(database_url);
26 run_migrations(url, config, dry_run, target, verbose)?;
27 }
28
29 MigrateCommands::Rollback { to, database_url: cmd_url, dry_run } => {
30 let url = cmd_url.as_deref().unwrap_or(database_url);
31 rollback_migrations(url, config, to, dry_run, verbose)?;
32 }
33
34 MigrateCommands::Status { database_url: cmd_url, detailed } => {
35 let url = cmd_url.as_deref().unwrap_or(database_url);
36 show_status(url, config, detailed)?;
37 }
38
39 MigrateCommands::Validate { check_gaps, verify_checksums } => {
40 validate_migrations(&config.migrations.directory, check_gaps, verify_checksums, verbose)?;
41 }
42
43 MigrateCommands::List { pending, applied } => {
44 list_migrations(&config.migrations.directory, pending, applied)?;
45 }
46 }
47
48 Ok(())
49}
50
51fn create_migration(name: &str, migration_type: &str, directory: &str) -> Result<()> {
52 let timestamp = utils::get_timestamp();
53
54 let dir_path = Path::new(directory);
55 fs::create_dir_all(dir_path)
56 .context("Failed to create migrations directory")?;
57
58 let safe_name = name.replace(' ', "_").to_lowercase();
59
60 match migration_type {
61 "sql" => {
62 let up_file = dir_path.join(format!("{}_{}.up.sql", timestamp, safe_name));
64 let down_file = dir_path.join(format!("{}_{}.down.sql", timestamp, safe_name));
65
66 let up_content = format!(
67 "-- Migration: {}\n-- Version: {}\n-- Created: {}\n\n-- Add your UP migration SQL here\n",
68 name,
69 timestamp,
70 chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
71 );
72
73 let down_content = format!(
74 "-- Migration: {} (rollback)\n-- Version: {}\n-- Created: {}\n\n-- Add your DOWN migration SQL here\n",
75 name,
76 timestamp,
77 chrono::Local::now().format("%Y-%m-%d %H:%M:%S")
78 );
79
80 fs::write(&up_file, up_content)
81 .context("Failed to create up migration file")?;
82 fs::write(&down_file, down_content)
83 .context("Failed to create down migration file")?;
84
85 utils::print_success(&format!("Created SQL migration: {}", safe_name));
86 println!(" {}: {}", "UP".green(), up_file.display());
87 println!(" {}: {}", "DOWN".red(), down_file.display());
88 }
89
90 "rust" => {
91 let rust_file = dir_path.join(format!("{}_{}.rs", timestamp, safe_name));
93
94 let rust_content = format!(
95 r#"//! Migration: {}
96//! Version: {}
97//! Created: {}
98
99use parsql_migrations::prelude::*;
100
101pub struct Migration{};
102
103impl Migration for Migration{} {{
104 fn version(&self) -> i64 {{
105 {}
106 }}
107
108 fn name(&self) -> &str {{
109 "{}"
110 }}
111
112 fn up(&self, conn: &mut dyn MigrationConnection) -> Result<(), MigrationError> {{
113 // Add your UP migration logic here
114 conn.execute(
115 "CREATE TABLE example (
116 id SERIAL PRIMARY KEY,
117 name VARCHAR(255) NOT NULL
118 )"
119 )
120 }}
121
122 fn down(&self, conn: &mut dyn MigrationConnection) -> Result<(), MigrationError> {{
123 // Add your DOWN migration logic here
124 conn.execute("DROP TABLE IF EXISTS example")
125 }}
126}}
127"#,
128 name, timestamp, chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), timestamp, timestamp, timestamp, safe_name );
136
137 fs::write(&rust_file, rust_content)
138 .context("Failed to create Rust migration file")?;
139
140 utils::print_success(&format!("Created Rust migration: {}", safe_name));
141 println!(" {}: {}", "File".cyan(), rust_file.display());
142
143 utils::print_info("Remember to add this migration to your build.rs or mod.rs");
144 }
145
146 _ => anyhow::bail!("Unknown migration type: {}", migration_type),
147 }
148
149 Ok(())
150}
151
152fn run_migrations(
153 database_url: &str,
154 config: &Config,
155 dry_run: bool,
156 target: Option<i64>,
157 verbose: bool,
158) -> Result<()> {
159 let db_type = utils::parse_database_url(database_url)?;
160
161 if verbose {
162 utils::print_info(&format!("Database: {} ({})", database_url, db_type.name()));
163 }
164
165 let progress = Progress::new("Loading migrations");
166 let migrations = load_migrations_from_directory(&config.migrations.directory)?;
167 progress.finish_with_message(&format!("{} migrations found", migrations.len()));
168
169 if migrations.is_empty() {
170 utils::print_warning("No migrations found");
171 return Ok(());
172 }
173
174 if dry_run {
175 utils::print_info("DRY RUN - No changes will be applied");
176
177 for migration in &migrations {
178 println!("Would run: {} - {}", migration.version, migration.name);
179 }
180
181 return Ok(());
182 }
183
184 match db_type {
186 DatabaseType::PostgreSQL => {
187 run_postgres_migrations(database_url, config, migrations, target)?;
188 }
189 DatabaseType::SQLite => {
190 run_sqlite_migrations(database_url, config, migrations, target)?;
191 }
192 }
193
194 Ok(())
195}
196
197#[cfg(feature = "postgres")]
198fn run_postgres_migrations(
199 database_url: &str,
200 config: &Config,
201 migrations: Vec<FileMigration>,
202 target: Option<i64>,
203) -> Result<()> {
204 use postgres::{Client, NoTls};
205 use parsql_migrations::postgres_simple::PostgresMigrationConnection;
206
207 let progress = Progress::new("Connecting to PostgreSQL");
208 let mut client = Client::connect(database_url, NoTls)
209 .context("Failed to connect to PostgreSQL")?;
210 progress.finish();
211
212 let mut migration_conn = PostgresMigrationConnection::new(&mut client);
213 let migration_config = config.to_parsql_migration_config();
214 let mut runner = MigrationRunner::with_config(migration_config);
215
216 for migration in migrations {
218 if let Some(target) = target {
219 if migration.version > target {
220 continue;
221 }
222 }
223 runner.add_migration(Box::new(migration));
224 }
225
226 let progress = Progress::new("Running migrations");
228 let report = runner.run(&mut migration_conn)
229 .context("Failed to run migrations")?;
230 progress.finish();
231
232 if report.successful_count() > 0 {
234 utils::print_success(&format!("Applied {} migration(s)", report.successful_count()));
235 }
236
237 if !report.skipped.is_empty() {
238 utils::print_info(&format!("Skipped {} migration(s) (already applied)", report.skipped.len()));
239 }
240
241 if report.failed_count() > 0 {
242 utils::print_error(&format!("Failed {} migration(s)", report.failed_count()));
243 for result in &report.failed {
244 println!(" {} Version {}: {}",
245 "✗".red(),
246 result.version,
247 result.error.as_ref().unwrap_or(&"Unknown error".to_string())
248 );
249 }
250 anyhow::bail!("Some migrations failed");
251 }
252
253 Ok(())
254}
255
256#[cfg(feature = "sqlite")]
257fn run_sqlite_migrations(
258 database_url: &str,
259 config: &Config,
260 migrations: Vec<FileMigration>,
261 target: Option<i64>,
262) -> Result<()> {
263 use rusqlite::Connection;
264 use parsql_migrations::sqlite_simple::SqliteMigrationConnection;
265
266 let db_path = database_url.strip_prefix("sqlite:").unwrap_or(database_url);
267
268 let progress = Progress::new("Opening SQLite database");
269 let mut conn = Connection::open(db_path)
270 .context("Failed to open SQLite database")?;
271 progress.finish();
272
273 let mut migration_conn = SqliteMigrationConnection::new(&mut conn);
274 let migration_config = config.to_parsql_migration_config();
275 let mut runner = MigrationRunner::with_config(migration_config);
276
277 for migration in migrations {
279 if let Some(target) = target {
280 if migration.version > target {
281 continue;
282 }
283 }
284 runner.add_migration(Box::new(migration));
285 }
286
287 let progress = Progress::new("Running migrations");
289 let report = runner.run(&mut migration_conn)
290 .context("Failed to run migrations")?;
291 progress.finish();
292
293 if report.successful_count() > 0 {
295 utils::print_success(&format!("Applied {} migration(s)", report.successful_count()));
296 }
297
298 if !report.skipped.is_empty() {
299 utils::print_info(&format!("Skipped {} migration(s) (already applied)", report.skipped.len()));
300 }
301
302 if report.failed_count() > 0 {
303 utils::print_error(&format!("Failed {} migration(s)", report.failed_count()));
304 for result in &report.failed {
305 println!(" {} Version {}: {}",
306 "✗".red(),
307 result.version,
308 result.error.as_ref().unwrap_or(&"Unknown error".to_string())
309 );
310 }
311 anyhow::bail!("Some migrations failed");
312 }
313
314 Ok(())
315}
316
317fn rollback_migrations(
318 database_url: &str,
319 config: &Config,
320 target_version: i64,
321 dry_run: bool,
322 verbose: bool,
323) -> Result<()> {
324 let db_type = utils::parse_database_url(database_url)?;
325
326 if verbose {
327 utils::print_info(&format!("Database: {} ({})", database_url, db_type.name()));
328 }
329
330 utils::print_info(&format!("Rolling back to version: {}", target_version));
331
332 let progress = Progress::new("Loading migrations");
333 let migrations = load_migrations_from_directory(&config.migrations.directory)?;
334 progress.finish_with_message(&format!("{} migrations found", migrations.len()));
335
336 if dry_run {
337 utils::print_info("DRY RUN - No changes will be applied");
338 utils::print_warning("Note: Cannot determine which migrations would be rolled back without database connection");
339 return Ok(());
340 }
341
342 match db_type {
344 DatabaseType::PostgreSQL => {
345 rollback_postgres_migrations(database_url, config, migrations, target_version)?;
346 }
347 DatabaseType::SQLite => {
348 rollback_sqlite_migrations(database_url, config, migrations, target_version)?;
349 }
350 }
351
352 Ok(())
353}
354
355fn show_status(
356 database_url: &str,
357 config: &Config,
358 detailed: bool,
359) -> Result<()> {
360 let db_type = utils::parse_database_url(database_url)?;
361
362 utils::print_info(&format!("Database: {} ({})", database_url, db_type.name()));
363
364 let progress = Progress::new("Loading migrations");
365 let migrations = load_migrations_from_directory(&config.migrations.directory)?;
366 progress.finish_with_message(&format!("{} migrations found", migrations.len()));
367
368 match db_type {
370 DatabaseType::PostgreSQL => {
371 show_postgres_status(database_url, config, migrations, detailed)?;
372 }
373 DatabaseType::SQLite => {
374 show_sqlite_status(database_url, config, migrations, detailed)?;
375 }
376 }
377
378 Ok(())
379}
380
381fn validate_migrations(
382 directory: &str,
383 check_gaps: bool,
384 verify_checksums: bool,
385 verbose: bool,
386) -> Result<()> {
387 let migrations = load_migrations_from_directory(directory)?;
388
389 if migrations.is_empty() {
390 utils::print_warning("No migrations found");
391 return Ok(());
392 }
393
394 utils::print_info(&format!("Found {} migration(s)", migrations.len()));
395
396 if check_gaps {
398 let mut versions: Vec<i64> = migrations.iter().map(|m| m.version).collect();
399 versions.sort();
400
401 let mut has_gaps = false;
402 for i in 1..versions.len() {
403 if versions[i] - versions[i-1] > 1 {
404 utils::print_warning(&format!(
405 "Gap detected between versions {} and {}",
406 versions[i-1],
407 versions[i]
408 ));
409 has_gaps = true;
410 }
411 }
412
413 if !has_gaps {
414 utils::print_success("No version gaps found");
415 }
416 }
417
418 if verify_checksums {
419 utils::print_info("Verifying migration checksums...");
420
421 let checksum_errors = 0;
422 for migration in &migrations {
423 let calculated_checksum = calculate_migration_checksum(migration);
424
425 if verbose {
427 println!(" {} - {}: {}",
428 migration.version,
429 migration.name,
430 &calculated_checksum[..8]
431 );
432 }
433 }
434
435 if checksum_errors == 0 {
436 utils::print_success("All checksums verified");
437 } else {
438 utils::print_error(&format!("{} checksum error(s) found", checksum_errors));
439 }
440 }
441
442 Ok(())
443}
444
445fn list_migrations(
446 directory: &str,
447 pending_only: bool,
448 applied_only: bool,
449) -> Result<()> {
450 let migrations = load_migrations_from_directory(directory)?;
451
452 if migrations.is_empty() {
453 utils::print_warning("No migrations found");
454 return Ok(());
455 }
456
457 println!("{}", "Available Migrations:".bold());
458 println!();
459
460 let headers = vec!["Version", "Name", "Type"];
461 let mut rows = Vec::new();
462
463 for migration in migrations {
464 rows.push(vec![
465 migration.version.to_string(),
466 migration.name.clone(),
467 migration.migration_type.clone(),
468 ]);
469 }
470
471 print!("{}", utils::format_table(headers, rows));
472
473 if pending_only || applied_only {
474 utils::print_info("Filtering by status requires database connection (not yet implemented)");
475 }
476
477 Ok(())
478}
479
480#[cfg(feature = "postgres")]
481fn show_postgres_status(
482 database_url: &str,
483 config: &Config,
484 migrations: Vec<FileMigration>,
485 detailed: bool,
486) -> Result<()> {
487 use postgres::{Client, NoTls};
488 use parsql_migrations::postgres_simple::PostgresMigrationConnection;
489
490 let progress = Progress::new("Connecting to PostgreSQL");
491 let mut client = Client::connect(database_url, NoTls)
492 .context("Failed to connect to PostgreSQL")?;
493 progress.finish();
494
495 let mut migration_conn = PostgresMigrationConnection::new(&mut client);
496
497 let records = migration_conn.query_migrations(&config.migrations.table_name)
499 .context("Failed to fetch applied migrations")?;
500
501 let mut applied = std::collections::HashMap::new();
503 for record in records {
504 applied.insert(record.version, record);
505 }
506
507 let total_count = migrations.len();
508 let applied_count = applied.len();
509 let pending_count = migrations.iter()
510 .filter(|m| !applied.contains_key(&m.version))
511 .count();
512
513 println!();
515 println!("{}", "Migration Status:".bold());
516 println!(" {} migrations", utils::colorize_number(total_count, "Total"));
517 println!(" {} migrations", utils::colorize_number(applied_count, "Applied").green());
518 println!(" {} migrations", utils::colorize_number(pending_count, "Pending").yellow());
519
520 if detailed {
521 println!();
522 println!("{}", "Detailed Status:".bold());
523
524 let headers = vec!["Version", "Name", "Status", "Applied At", "Checksum"];
525 let mut rows = Vec::new();
526
527 for migration in migrations {
528 let status;
529 let applied_at;
530
531 if let Some(record) = applied.get(&migration.version) {
532 status = "Applied".green().to_string();
533 applied_at = record.applied_at.format("%Y-%m-%d %H:%M:%S").to_string();
534 } else {
535 status = "Pending".yellow().to_string();
536 applied_at = "-".to_string();
537 }
538
539 let checksum_status = if let Some(record) = applied.get(&migration.version) {
540 let calculated_checksum = calculate_migration_checksum(&migration);
541 if let Some(ref stored_checksum) = record.checksum {
542 if stored_checksum == &calculated_checksum {
543 "✓".green().to_string()
544 } else {
545 format!("✗ Mismatch").red().to_string()
546 }
547 } else {
548 "No checksum".dimmed().to_string()
549 }
550 } else {
551 "-".to_string()
552 };
553
554 rows.push(vec![
555 migration.version.to_string(),
556 migration.name.clone(),
557 status,
558 applied_at,
559 checksum_status,
560 ]);
561 }
562
563 print!("{}", utils::format_table(headers, rows));
564 }
565
566 Ok(())
567}
568
569#[cfg(feature = "sqlite")]
570fn show_sqlite_status(
571 database_url: &str,
572 config: &Config,
573 migrations: Vec<FileMigration>,
574 detailed: bool,
575) -> Result<()> {
576 use rusqlite::Connection;
577 use parsql_migrations::sqlite_simple::SqliteMigrationConnection;
578
579 let db_path = database_url.strip_prefix("sqlite:").unwrap_or(database_url);
580
581 let progress = Progress::new("Opening SQLite database");
582 let mut conn = Connection::open(db_path)
583 .context("Failed to open SQLite database")?;
584 progress.finish();
585
586 let mut migration_conn = SqliteMigrationConnection::new(&mut conn);
587
588 let records = migration_conn.query_migrations(&config.migrations.table_name)
590 .context("Failed to fetch applied migrations")?;
591
592 let mut applied = std::collections::HashMap::new();
594 for record in records {
595 applied.insert(record.version, record);
596 }
597
598 let total_count = migrations.len();
599 let applied_count = applied.len();
600 let pending_count = migrations.iter()
601 .filter(|m| !applied.contains_key(&m.version))
602 .count();
603
604 println!();
606 println!("{}", "Migration Status:".bold());
607 println!(" {} migrations", utils::colorize_number(total_count, "Total"));
608 println!(" {} migrations", utils::colorize_number(applied_count, "Applied").green());
609 println!(" {} migrations", utils::colorize_number(pending_count, "Pending").yellow());
610
611 if detailed {
612 println!();
613 println!("{}", "Detailed Status:".bold());
614
615 let headers = vec!["Version", "Name", "Status", "Applied At", "Checksum"];
616 let mut rows = Vec::new();
617
618 for migration in migrations {
619 let status;
620 let applied_at;
621
622 if let Some(record) = applied.get(&migration.version) {
623 status = "Applied".green().to_string();
624 applied_at = record.applied_at.format("%Y-%m-%d %H:%M:%S").to_string();
625 } else {
626 status = "Pending".yellow().to_string();
627 applied_at = "-".to_string();
628 }
629
630 let checksum_status = if let Some(record) = applied.get(&migration.version) {
631 let calculated_checksum = calculate_migration_checksum(&migration);
632 if let Some(ref stored_checksum) = record.checksum {
633 if stored_checksum == &calculated_checksum {
634 "✓".green().to_string()
635 } else {
636 format!("✗ Mismatch").red().to_string()
637 }
638 } else {
639 "No checksum".dimmed().to_string()
640 }
641 } else {
642 "-".to_string()
643 };
644
645 rows.push(vec![
646 migration.version.to_string(),
647 migration.name.clone(),
648 status,
649 applied_at,
650 checksum_status,
651 ]);
652 }
653
654 print!("{}", utils::format_table(headers, rows));
655 }
656
657 Ok(())
658}
659
660fn calculate_migration_checksum(migration: &FileMigration) -> String {
663 let mut hasher = Sha256::new();
664 hasher.update(migration.version.to_string());
665 hasher.update(&migration.name);
666
667 if let Some(ref up_sql) = migration.up_sql {
668 hasher.update(up_sql);
669 }
670 if let Some(ref down_sql) = migration.down_sql {
671 hasher.update(down_sql);
672 }
673
674 format!("{:x}", hasher.finalize())
675}
676
677struct FileMigration {
678 version: i64,
679 name: String,
680 migration_type: String,
681 up_sql: Option<String>,
682 down_sql: Option<String>,
683}
684
685impl Migration for FileMigration {
686 fn version(&self) -> i64 {
687 self.version
688 }
689
690 fn name(&self) -> &str {
691 &self.name
692 }
693
694 fn up(&self, conn: &mut dyn MigrationConnection) -> Result<(), MigrationError> {
695 if let Some(ref sql) = self.up_sql {
696 conn.execute(sql)?;
697 }
698 Ok(())
699 }
700
701 fn down(&self, conn: &mut dyn MigrationConnection) -> Result<(), MigrationError> {
702 if let Some(ref sql) = self.down_sql {
703 conn.execute(sql)?;
704 }
705 Ok(())
706 }
707
708 fn checksum(&self) -> String {
709 calculate_migration_checksum(self)
710 }
711}
712
713fn load_migrations_from_directory(directory: &str) -> Result<Vec<FileMigration>> {
714 let dir_path = Path::new(directory);
715
716 if !dir_path.exists() {
717 return Ok(Vec::new());
718 }
719
720 let mut migrations = Vec::new();
721
722 for entry in fs::read_dir(dir_path)? {
723 let entry = entry?;
724 let path = entry.path();
725
726 if let Some(file_name) = path.file_name() {
727 let file_name_str = file_name.to_string_lossy();
728
729 if file_name_str.ends_with(".up.sql") {
731 let base_name = file_name_str.trim_end_matches(".up.sql");
732 let parts: Vec<&str> = base_name.splitn(3, '_').collect();
733
734 if parts.len() >= 3 {
735 let version = parts[0].parse::<i64>()
736 .context("Failed to parse migration version")?;
737 let name = parts[2].to_string();
738
739 let up_sql = fs::read_to_string(&path)
740 .context("Failed to read up migration file")?;
741
742 let down_path = path.with_file_name(format!("{}.down.sql", base_name));
743 let down_sql = if down_path.exists() {
744 Some(fs::read_to_string(&down_path)
745 .context("Failed to read down migration file")?)
746 } else {
747 None
748 };
749
750 migrations.push(FileMigration {
751 version,
752 name,
753 migration_type: "SQL".to_string(),
754 up_sql: Some(up_sql),
755 down_sql,
756 });
757 }
758 }
759
760 }
762 }
763
764 migrations.sort_by_key(|m| m.version);
765 Ok(migrations)
766}
767
768#[cfg(feature = "postgres")]
769fn rollback_postgres_migrations(
770 database_url: &str,
771 config: &Config,
772 migrations: Vec<FileMigration>,
773 target_version: i64,
774) -> Result<()> {
775 use postgres::{Client, NoTls};
776 use parsql_migrations::postgres_simple::PostgresMigrationConnection;
777
778 let progress = Progress::new("Connecting to PostgreSQL");
779 let mut client = Client::connect(database_url, NoTls)
780 .context("Failed to connect to PostgreSQL")?;
781 progress.finish();
782
783 let mut migration_conn = PostgresMigrationConnection::new(&mut client);
784 let migration_config = config.to_parsql_migration_config();
785 let mut runner = MigrationRunner::with_config(migration_config);
786
787 for migration in migrations {
789 runner.add_migration(Box::new(migration));
790 }
791
792 let progress = Progress::new("Rolling back migrations");
794 let report = runner.rollback(&mut migration_conn, target_version)
795 .context("Failed to rollback migrations")?;
796 progress.finish();
797
798 if report.successful_count() > 0 {
800 utils::print_success(&format!("Rolled back {} migration(s)", report.successful_count()));
801 for result in &report.successful {
802 println!(" {} Version {} - {}", "↩".cyan(), result.version, result.name);
803 }
804 } else {
805 utils::print_info("No migrations to roll back");
806 }
807
808 if report.failed_count() > 0 {
809 utils::print_error(&format!("Failed to rollback {} migration(s)", report.failed_count()));
810 for result in &report.failed {
811 println!(" {} Version {}: {}",
812 "✗".red(),
813 result.version,
814 result.error.as_ref().unwrap_or(&"Unknown error".to_string())
815 );
816 }
817 anyhow::bail!("Some rollbacks failed");
818 }
819
820 Ok(())
821}
822
823#[cfg(feature = "sqlite")]
824fn rollback_sqlite_migrations(
825 database_url: &str,
826 config: &Config,
827 migrations: Vec<FileMigration>,
828 target_version: i64,
829) -> Result<()> {
830 use rusqlite::Connection;
831 use parsql_migrations::sqlite_simple::SqliteMigrationConnection;
832
833 let db_path = database_url.strip_prefix("sqlite:").unwrap_or(database_url);
834
835 let progress = Progress::new("Opening SQLite database");
836 let mut conn = Connection::open(db_path)
837 .context("Failed to open SQLite database")?;
838 progress.finish();
839
840 let mut migration_conn = SqliteMigrationConnection::new(&mut conn);
841 let migration_config = config.to_parsql_migration_config();
842 let mut runner = MigrationRunner::with_config(migration_config);
843
844 for migration in migrations {
846 runner.add_migration(Box::new(migration));
847 }
848
849 let progress = Progress::new("Rolling back migrations");
851 let report = runner.rollback(&mut migration_conn, target_version)
852 .context("Failed to rollback migrations")?;
853 progress.finish();
854
855 if report.successful_count() > 0 {
857 utils::print_success(&format!("Rolled back {} migration(s)", report.successful_count()));
858 for result in &report.successful {
859 println!(" {} Version {} - {}", "↩".cyan(), result.version, result.name);
860 }
861 } else {
862 utils::print_info("No migrations to roll back");
863 }
864
865 if report.failed_count() > 0 {
866 utils::print_error(&format!("Failed to rollback {} migration(s)", report.failed_count()));
867 for result in &report.failed {
868 println!(" {} Version {}: {}",
869 "✗".red(),
870 result.version,
871 result.error.as_ref().unwrap_or(&"Unknown error".to_string())
872 );
873 }
874 anyhow::bail!("Some rollbacks failed");
875 }
876
877 Ok(())
878}