use crate::catalog::Catalog;
use crate::diff::operations::{FunctionOperation, MigrationStep, SqlRenderer, ViewOperation};
use anyhow::Result;
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Default, clap::ValueEnum)]
pub enum DiffFormat {
#[default]
Detailed,
Summary,
Sql,
Json,
}
pub struct DiffContext {
pub from_description: String,
pub to_description: String,
}
impl DiffContext {
pub fn new(from: impl Into<String>, to: impl Into<String>) -> Self {
Self {
from_description: from.into(),
to_description: to.into(),
}
}
}
pub fn output_diff(
steps: &[MigrationStep],
format: &DiffFormat,
context: &DiffContext,
from_catalog: &Catalog,
to_catalog: &Catalog,
output_file: Option<&str>,
) -> Result<()> {
match format {
DiffFormat::Sql => output_sql_format(steps, context, output_file),
DiffFormat::Summary => {
output_summary_format(steps);
Ok(())
}
DiffFormat::Detailed => output_detailed_format(steps, from_catalog, to_catalog),
DiffFormat::Json => output_json_format(steps, context),
}
}
pub fn has_differences(steps: &[MigrationStep]) -> bool {
!steps.is_empty()
}
fn output_sql_format(
steps: &[MigrationStep],
context: &DiffContext,
output_file: Option<&str>,
) -> Result<()> {
let mut output = String::new();
output.push_str(&format!(
"-- SQL to bring {} in sync with {}\n",
context.from_description, context.to_description
));
output.push_str("-- Generated by pgmt\n\n");
for step in steps {
for rendered in step.to_sql() {
output.push_str(&rendered.sql);
output.push_str(";\n\n");
}
}
if let Some(file_path) = output_file {
std::fs::write(file_path, &output)?;
println!("SQL saved to {}", file_path);
} else {
println!("{}", output);
}
Ok(())
}
fn output_summary_format(steps: &[MigrationStep]) {
if steps.is_empty() {
println!("No differences found.");
return;
}
println!("Schema Diff Summary:\n");
let mut counts: HashMap<&str, usize> = HashMap::new();
for step in steps {
let type_name = match step {
MigrationStep::Table(_) => "Tables",
MigrationStep::View(_) => "Views",
MigrationStep::Function(_) => "Functions",
MigrationStep::Aggregate(_) => "Aggregates",
MigrationStep::Index(_) => "Indexes",
MigrationStep::Sequence(_) => "Sequences",
MigrationStep::Schema(_) => "Schemas",
MigrationStep::Extension(_) => "Extensions",
MigrationStep::Trigger(_) => "Triggers",
MigrationStep::Policy(_) => "Policies",
MigrationStep::Type(_) => "Custom Types",
MigrationStep::Domain(_) => "Domains",
MigrationStep::Grant(_) => "Grants",
MigrationStep::Constraint(_) => "Constraints",
};
*counts.entry(type_name).or_insert(0) += 1;
}
for (type_name, count) in counts.iter() {
println!("{:<15} {} changes", format!("{}:", type_name), count);
}
let destructive_count = steps.iter().filter(|s| s.has_destructive_sql()).count();
let safe_count = steps.len() - destructive_count;
println!("\nTotal changes: {}", steps.len());
println!(" Safe: {}", safe_count);
println!(" Destructive: {}", destructive_count);
println!("\nRun with --format detailed to see what changed");
println!("Run with --format sql to generate remediation SQL");
}
fn output_detailed_format(
steps: &[MigrationStep],
from_catalog: &Catalog,
to_catalog: &Catalog,
) -> Result<()> {
if steps.is_empty() {
println!("No differences found.");
return Ok(());
}
println!("Found {} differences:\n", steps.len());
println!("{}", "━".repeat(70));
for (i, step) in steps.iter().enumerate() {
println!("\n{}. {:?}", i + 1, step.id());
println!(
"Status: {}",
if step.has_destructive_sql() {
"DESTRUCTIVE"
} else {
"SAFE"
}
);
match step {
MigrationStep::View(op) => {
if let Some(diff) = get_view_diff(op, from_catalog, to_catalog) {
println!("\n{}", diff);
} else {
print_sql_for_step(step);
}
}
MigrationStep::Function(op) => {
if let Some(diff) = get_function_diff(op, from_catalog, to_catalog) {
println!("\n{}", diff);
} else {
print_sql_for_step(step);
}
}
_ => {
print_sql_for_step(step);
}
}
println!("\n{}", "━".repeat(70));
}
let destructive_count = steps.iter().filter(|s| s.has_destructive_sql()).count();
let safe_count = steps.len() - destructive_count;
println!("\nSummary:");
println!(" Safe operations: {}", safe_count);
println!(" Destructive operations: {}", destructive_count);
Ok(())
}
fn print_sql_for_step(step: &MigrationStep) {
for rendered in step.to_sql() {
println!("\nSQL:");
println!("{}", rendered.sql);
}
}
fn get_view_diff(
op: &ViewOperation,
from_catalog: &Catalog,
to_catalog: &Catalog,
) -> Option<String> {
use similar::{ChangeTag, TextDiff};
match op {
ViewOperation::Replace { schema, name, .. } => {
let from_view = from_catalog
.views
.iter()
.find(|v| v.schema == *schema && v.name == *name)?;
let to_view = to_catalog
.views
.iter()
.find(|v| v.schema == *schema && v.name == *name)?;
let diff = TextDiff::from_lines(&from_view.definition, &to_view.definition);
let mut output = String::new();
output.push_str("Diff:\n");
for change in diff.iter_all_changes() {
let sign = match change.tag() {
ChangeTag::Delete => "-",
ChangeTag::Insert => "+",
ChangeTag::Equal => " ",
};
output.push_str(&format!("{} {}", sign, change));
}
Some(output)
}
_ => None,
}
}
fn get_function_diff(
op: &FunctionOperation,
from_catalog: &Catalog,
to_catalog: &Catalog,
) -> Option<String> {
use similar::{ChangeTag, TextDiff};
match op {
FunctionOperation::Replace {
schema,
name,
parameters,
..
} => {
let from_func = from_catalog.functions.iter().find(|f| {
f.schema == *schema
&& f.name == *name
&& format_parameters(&f.parameters) == *parameters
})?;
let to_func = to_catalog.functions.iter().find(|f| {
f.schema == *schema
&& f.name == *name
&& format_parameters(&f.parameters) == *parameters
})?;
let diff = TextDiff::from_lines(&from_func.definition, &to_func.definition);
let mut output = String::new();
output.push_str("Diff:\n");
for change in diff.iter_all_changes() {
let sign = match change.tag() {
ChangeTag::Delete => "-",
ChangeTag::Insert => "+",
ChangeTag::Equal => " ",
};
output.push_str(&format!("{} {}", sign, change));
}
Some(output)
}
_ => None,
}
}
fn format_parameters(params: &[crate::catalog::function::FunctionParam]) -> String {
params
.iter()
.map(|p| {
let mode = p.mode.as_deref().unwrap_or("");
let name = p.name.as_deref().unwrap_or("");
format!("{} {} {}", mode, name, p.data_type)
.trim()
.to_string()
})
.collect::<Vec<_>>()
.join(", ")
}
fn output_json_format(steps: &[MigrationStep], context: &DiffContext) -> Result<()> {
use serde_json::json;
let changes: Vec<serde_json::Value> = steps
.iter()
.map(|step| {
json!({
"type": format!("{:?}", step.id()),
"destructive": step.has_destructive_sql(),
"sql": step.to_sql().iter().map(|r| &r.sql).collect::<Vec<_>>(),
})
})
.collect();
let output = json!({
"has_differences": !steps.is_empty(),
"from": context.from_description,
"to": context.to_description,
"summary": {
"total_changes": steps.len(),
"destructive_changes": steps.iter().filter(|s| s.has_destructive_sql()).count(),
"safe_changes": steps.iter().filter(|s| !s.has_destructive_sql()).count(),
},
"changes": changes,
});
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}