parsql_cli/commands/
migrate.rs

1//! Migration command implementations
2
3use 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            // Standardized format: {timestamp}_{name}
63            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            // Standardized format: {timestamp}_{name} (remove duplicate timestamp)
92            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,               // Migration comment
129                timestamp,          // Version comment  
130                chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), // Created comment
131                timestamp,          // struct name
132                timestamp,          // impl name
133                timestamp,          // version() return value
134                safe_name           // name() return value
135            );
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    // Run migrations based on database type
185    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    // Add migrations
217    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    // Run migrations
227    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    // Print report
233    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    // Add migrations
278    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    // Run migrations
288    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    // Print report
294    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    // Run rollback based on database type
343    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    // Get status based on database type
369    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    // Check for version gaps
397    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            // For now, just show the checksum (we'll add comparison with DB later)
426            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    // Get applied migrations
498    let records = migration_conn.query_migrations(&config.migrations.table_name)
499        .context("Failed to fetch applied migrations")?;
500    
501    // Convert to map for easy lookup
502    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    // Print summary
514    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    // Get applied migrations
589    let records = migration_conn.query_migrations(&config.migrations.table_name)
590        .context("Failed to fetch applied migrations")?;
591    
592    // Convert to map for easy lookup
593    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    // Print summary
605    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
660// Helper structures and functions
661
662fn 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            // Parse SQL migrations
730            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            // TODO: Parse Rust migrations
761        }
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    // Add all migrations
788    for migration in migrations {
789        runner.add_migration(Box::new(migration));
790    }
791    
792    // Perform rollback
793    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    // Print report
799    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    // Add all migrations
845    for migration in migrations {
846        runner.add_migration(Box::new(migration));
847    }
848    
849    // Perform rollback
850    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    // Print report
856    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}