use crate::application::monitoring::PerfReport;
use crate::domain::changeset::Changeset;
use crate::domain::conflict::ConflictReport;
use colored::*;
use tabled::settings::{object::Columns, Alignment, Modify, Style};
use tabled::{Table, Tabled};
#[derive(Tabled)]
struct TableRow {
table: String,
inserts: String,
updates: String,
deletes: String,
}
#[derive(Tabled)]
struct SummaryRow {
metric: String,
value: String,
}
pub fn print_summary(changeset: &Changeset) {
println!();
println!("{}", "DIFFLY DIFF SUMMARY".bold().cyan());
println!(
"{} → {}",
changeset.source_schema.blue(),
changeset.target_schema.green()
);
println!("Changeset: {}", changeset.changeset_id.bright_yellow());
println!();
if changeset.summary.total_changes == 0 {
println!("{}", "No changes detected.".italic());
return;
}
let rows: Vec<TableRow> = changeset
.tables
.iter()
.filter(|t| !t.is_empty())
.map(|t| TableRow {
table: t.table_name.bold().to_string(),
inserts: t.inserts.len().to_string().green().to_string(),
updates: t.updates.len().to_string().yellow().to_string(),
deletes: t.deletes.len().to_string().red().to_string(),
})
.collect();
let table = Table::new(rows)
.with(Style::rounded())
.with(Modify::new(Columns::new(1..=5)).with(Alignment::right()))
.to_string();
println!("{table}");
let s = &changeset.summary;
let summary_rows = vec![
SummaryRow {
metric: "Total inserts".into(),
value: s.total_inserts.to_string().green().to_string(),
},
SummaryRow {
metric: "Total updates".into(),
value: s.total_updates.to_string().yellow().to_string(),
},
SummaryRow {
metric: "Total deletes".into(),
value: s.total_deletes.to_string().red().to_string(),
},
SummaryRow {
metric: "Total changes".into(),
value: s.total_changes.to_string().bold().to_string(),
},
];
let summary_table = Table::new(summary_rows)
.with(Style::rounded())
.with(Modify::new(Columns::new(1..=1)).with(Alignment::right()))
.to_string();
println!();
println!("{summary_table}");
println!();
}
#[derive(Tabled)]
struct ConflictRow {
table: String,
pk: String,
column: String,
base: String,
yours: String,
theirs: String,
}
pub fn print_conflicts(conflicts: &[ConflictReport]) -> bool {
if conflicts.is_empty() {
println!("{}", "✓ No conflicts — changeset is clean.".bold().green());
return false;
}
println!();
println!("{}", "CONFLICTS DETECTED".bold().red());
println!(
"{} conflict(s) must be resolved before deploying.",
conflicts.len().to_string().bold()
);
println!();
let rows: Vec<ConflictRow> = conflicts
.iter()
.map(|c| {
let pk_str =
c.pk.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(", ");
ConflictRow {
table: c.table_name.bold().to_string(),
pk: pk_str,
column: c.column.yellow().to_string(),
base: c.base_value.to_string().dimmed().to_string(),
yours: c.source_value.to_string().cyan().to_string(),
theirs: c.target_value.to_string().red().to_string(),
}
})
.collect();
let table = Table::new(rows)
.with(Style::rounded())
.with(Modify::new(Columns::new(0..=0)).with(Alignment::left()))
.to_string();
println!("{table}");
println!();
println!(" {} base value at clone time", "base →".dimmed());
println!(" {} your source change", "yours →".cyan());
println!(" {} concurrent target change", "theirs→".red());
println!();
true
}
#[derive(Tabled)]
struct PerfRow {
operation: String,
table: String,
#[tabled(rename = "rows")]
rows: String,
#[tabled(rename = "time (ms)")]
duration_ms: String,
}
pub fn print_perf_summary(report: &PerfReport) {
if report.timings.is_empty() {
return;
}
println!("{}", "PERFORMANCE".bold().cyan());
let rows: Vec<PerfRow> = report
.timings
.iter()
.map(|t| PerfRow {
operation: t.operation.dimmed().to_string(),
table: t.table.bold().to_string(),
rows: t.rows.to_string(),
duration_ms: format_duration(t.duration_ms),
})
.collect();
let table = Table::new(rows)
.with(Style::rounded())
.with(Modify::new(Columns::new(2..=3)).with(Alignment::right()))
.to_string();
println!("{table}");
println!(
" Total: {} row(s) fetched · {} ms elapsed",
report.total_rows_fetched.to_string().bold(),
format_duration(report.total_ms),
);
println!();
}
fn format_duration(ms: u128) -> String {
if ms >= 1_000 {
format!("{:.1}s", ms as f64 / 1_000.0).yellow().to_string()
} else if ms >= 100 {
ms.to_string().yellow().to_string()
} else {
ms.to_string().green().to_string()
}
}