pgmt 0.6.4

PostgreSQL migration tool that keeps your schema files as the source of truth
Documentation
use console::style;
use std::time::Duration;

pub struct SectionReporter {
    total_sections: usize,
    current_section: usize,
    verbose: bool,
}

impl SectionReporter {
    pub fn new(total_sections: usize, verbose: bool) -> Self {
        Self {
            total_sections,
            current_section: 0,
            verbose,
        }
    }

    pub fn start_section(&mut self, name: &str, _description: Option<&str>) {
        self.current_section += 1;

        // Every executed section announces itself. The start line is marked
        // with an arrow and a trailing ellipsis so it reads distinctly from the
        // check-marked completion line that follows.
        println!(
            "  {} Section {}/{}: {} ...",
            style("→").cyan(),
            self.current_section,
            self.total_sections,
            name
        );
    }

    pub fn skip_section(&self, name: &str) {
        println!(
            "  {} Section '{}' already completed (skipping)",
            style("•").dim(),
            name
        );
    }

    pub fn attempt(&self, current: u32, total: u32) {
        if total > 1 {
            println!("    Attempt {}/{}", current, total);
        }
    }

    pub fn retry(&self, _name: &str, attempt: u32, error: &anyhow::Error, delay: Duration) {
        let delay_str = format_duration(delay);
        println!(
            "    {} Lock timeout on attempt {} (retrying in {}...)",
            style("⚠").yellow(),
            attempt,
            style(&delay_str).yellow()
        );

        if self.verbose {
            println!("      {}", style(error.to_string()).dim());
        }
    }

    pub fn complete_section(&self, name: &str, duration: Duration, rows: Option<usize>) {
        let duration_str = format_duration(duration);
        let rows_str = rows.map(|r| format!(", {} rows", r)).unwrap_or_default();

        println!(
            "  {} Section {}/{}: {} ({}{})",
            style("✓").green(),
            self.current_section,
            self.total_sections,
            name,
            duration_str,
            rows_str
        );
    }

    pub fn complete_section_with_retry(
        &self,
        name: &str,
        duration: Duration,
        rows: Option<usize>,
        attempts: u32,
        total_attempts: u32,
    ) {
        let duration_str = format_duration(duration);
        let rows_str = rows.map(|r| format!(", {} rows", r)).unwrap_or_default();
        let attempts_str = if attempts > 1 {
            format!(", succeeded on attempt {}/{}", attempts, total_attempts)
        } else {
            String::new()
        };

        println!(
            "  {} Section {}/{}: {} ({}{}{})",
            style("✓").green(),
            self.current_section,
            self.total_sections,
            name,
            duration_str,
            rows_str,
            attempts_str
        );
    }

    pub fn fail_section(&self, _name: &str, error: &anyhow::Error) {
        let err_str = error.to_string();
        println!("{} Failed: {}", style("✗").red(), style(&err_str).red());
    }

    pub fn migration_summary(&self, total_duration: Duration, _sections_completed: usize) {
        let duration_str = format_duration(total_duration);
        println!(
            "{} Completed in {}",
            style("✓").green(),
            style(&duration_str).green()
        );
    }
}

fn format_duration(d: Duration) -> String {
    let total_secs = d.as_secs();
    let millis = d.subsec_millis();

    if total_secs == 0 {
        format!("{}ms", millis)
    } else if total_secs < 60 {
        if millis > 0 {
            format!("{}.{}s", total_secs, millis / 100)
        } else {
            format!("{}s", total_secs)
        }
    } else if total_secs < 3600 {
        let mins = total_secs / 60;
        let secs = total_secs % 60;
        if secs > 0 {
            format!("{}m{}s", mins, secs)
        } else {
            format!("{}m", mins)
        }
    } else {
        let hours = total_secs / 3600;
        let mins = (total_secs % 3600) / 60;
        if mins > 0 {
            format!("{}h{}m", hours, mins)
        } else {
            format!("{}h", hours)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_format_duration_milliseconds() {
        assert_eq!(format_duration(Duration::from_millis(5)), "5ms");
        assert_eq!(format_duration(Duration::from_millis(50)), "50ms");
        assert_eq!(format_duration(Duration::from_millis(500)), "500ms");
    }

    #[test]
    fn test_format_duration_seconds() {
        assert_eq!(format_duration(Duration::from_secs(1)), "1s");
        assert_eq!(format_duration(Duration::from_secs(30)), "30s");
        assert_eq!(format_duration(Duration::from_millis(1500)), "1.5s");
        assert_eq!(format_duration(Duration::from_millis(2300)), "2.3s");
    }

    #[test]
    fn test_format_duration_minutes() {
        assert_eq!(format_duration(Duration::from_secs(60)), "1m");
        assert_eq!(format_duration(Duration::from_secs(90)), "1m30s");
        assert_eq!(format_duration(Duration::from_secs(300)), "5m");
        assert_eq!(format_duration(Duration::from_secs(3599)), "59m59s");
    }

    #[test]
    fn test_format_duration_hours() {
        assert_eq!(format_duration(Duration::from_secs(3600)), "1h");
        assert_eq!(format_duration(Duration::from_secs(3660)), "1h1m");
        assert_eq!(format_duration(Duration::from_secs(7200)), "2h");
        assert_eq!(format_duration(Duration::from_secs(5400)), "1h30m");
    }
}