use std::io::Write as _;
use anstream::stdout;
use anstyle::{AnsiColor, Effects, Style};
use type_bridge_migration::plan::{ExecutionStep, StepKind};
use type_bridge_migration::{MigrationAction, MigrationResult};
use type_bridge_orm::TxType;
const STYLE_SAFE: Style = Style::new()
.fg_color(Some(anstyle::Color::Ansi(AnsiColor::Green)))
.effects(Effects::BOLD);
const STYLE_WARNING: Style = Style::new()
.fg_color(Some(anstyle::Color::Ansi(AnsiColor::Yellow)))
.effects(Effects::BOLD);
const STYLE_BREAKING: Style = Style::new()
.fg_color(Some(anstyle::Color::Ansi(AnsiColor::Red)))
.effects(Effects::BOLD);
const STYLE_DIM: Style = Style::new().effects(Effects::DIMMED);
pub fn print_no_pending() {
let mut out = stdout();
writeln!(out, "No pending migrations.").unwrap_or_default();
}
pub fn print_migration_header(name: &str, action: &str) {
let mut out = stdout();
writeln!(
out,
"\n{STYLE_DIM}Migration:{STYLE_DIM:#} {name} [{action}]"
)
.unwrap_or_default();
}
pub fn print_step(index: usize, step: &ExecutionStep) {
let mut out = stdout();
let kind_label = step_kind_label(step.kind);
let kind_style = step_kind_style(step.kind, step.tx_type);
let preview = truncate_typeql(&step.forward, 80);
writeln!(
out,
" Step {index}: {kind_style}{kind_label}{kind_style:#} tx={tx_type}",
tx_type = tx_type_label(step.tx_type),
)
.unwrap_or_default();
writeln!(out, " {STYLE_DIM}{preview}{STYLE_DIM:#}").unwrap_or_default();
}
pub fn print_sqlmigrate_forward(index: usize, step: &ExecutionStep) {
let mut out = stdout();
writeln!(
out,
"-- Step {index} (forward, tx={tx_type})",
tx_type = tx_type_label(step.tx_type)
)
.unwrap_or_default();
writeln!(out, "{}", step.forward).unwrap_or_default();
}
pub fn print_sqlmigrate_reverse(index: usize, step: &ExecutionStep) {
let mut out = stdout();
match &step.reverse {
Some(reverse_typeql) => {
writeln!(
out,
"-- Step {index} (reverse, tx={tx_type})",
tx_type = tx_type_label(step.tx_type)
)
.unwrap_or_default();
writeln!(out, "{reverse_typeql}").unwrap_or_default();
}
None => {
writeln!(out, "-- Step {index}: no reverse (non-reversible step)").unwrap_or_default();
}
}
}
pub fn print_result(result: &MigrationResult) {
let mut out = stdout();
let action_label = match result.action {
MigrationAction::Apply => "Applied",
MigrationAction::Rollback => "Rolled back",
};
let status_label = if result.success { "OK" } else { "FAILED" };
let status_style = if result.success {
STYLE_SAFE
} else {
STYLE_BREAKING
};
writeln!(
out,
" {action_label}: {name} ... {status_style}{status_label}{status_style:#}",
name = result.name,
)
.unwrap_or_default();
if let Some(error) = &result.error {
writeln!(out, " Error: {error}").unwrap_or_default();
}
if let Some(backfill_steps) = &result.backfill {
for bf in backfill_steps {
writeln!(
out,
" {STYLE_DIM}backfill step {idx}: matched={matched} inserted={inserted} skipped={skipped}{STYLE_DIM:#}",
idx = bf.step_index,
matched = bf.matched,
inserted = bf.inserted,
skipped = bf.skipped,
)
.unwrap_or_default();
}
}
}
pub fn print_app_label(app_label: &str) {
let mut out = stdout();
writeln!(out, "\n{app_label}").unwrap_or_default();
}
pub fn print_migration_status(name: &str, is_applied: bool) {
let mut out = stdout();
if is_applied {
writeln!(out, " {STYLE_SAFE}[X]{STYLE_SAFE:#} {name}").unwrap_or_default();
} else {
writeln!(out, " {STYLE_DIM}[ ]{STYLE_DIM:#} {name}").unwrap_or_default();
}
}
fn step_kind_label(kind: StepKind) -> &'static str {
match kind {
StepKind::Schema => "schema",
StepKind::Write => "write",
StepKind::Backfill => "backfill",
}
}
fn step_kind_style(kind: StepKind, tx_type: TxType) -> Style {
match kind {
StepKind::Schema => STYLE_SAFE,
StepKind::Backfill => STYLE_BREAKING,
StepKind::Write => match tx_type {
TxType::Write => STYLE_WARNING,
TxType::Schema => STYLE_SAFE,
TxType::Read => STYLE_DIM,
},
}
}
fn tx_type_label(tx_type: TxType) -> &'static str {
match tx_type {
TxType::Schema => "schema",
TxType::Write => "write",
TxType::Read => "read",
}
}
fn truncate_typeql(typeql: &str, max_chars: usize) -> String {
let single_line: String = typeql.lines().map(str::trim).collect::<Vec<_>>().join(" ");
if single_line.chars().count() <= max_chars {
single_line
} else {
let truncated: String = single_line.chars().take(max_chars).collect();
format!("{truncated}…")
}
}