use std::path::PathBuf;
use anyhow::Result;
use pgevolve_core::plan::{LintWaiver, Plan, PlannerPolicy, write_plan_dir};
use crate::cli::PlanArgs;
use crate::config::PgevolveConfig;
use crate::connection::{connect, resolve_db};
pub async fn run(args: PlanArgs, cfg: &PgevolveConfig) -> Result<i32> {
let opts = resolve_db(cfg, &args.db, args.url.as_deref())?;
let client = connect(&opts).await?;
let existing_lint_waivers = args
.output
.as_deref()
.map_or_else(Vec::new, load_existing_waivers);
let build_opts = crate::api::BuildPlanOptions {
managed_schemas: opts.managed_schemas.clone(),
ignore_objects: opts.ignore_objects.clone(),
strategy: opts.strategy,
planner_ruleset_version: PlannerPolicy::default().planner_ruleset_version,
existing_lint_waivers,
source_rev: detect_git_rev().ok(),
};
let plan = match crate::api::build_plan(&cfg.project.schema_dir, client, build_opts).await {
Ok(p) => p,
Err(crate::api::BuildPlanError::LintAtPlanRequiresWaiver(msg)) => {
eprintln!("pgevolve plan: refusing to plan due to unwaived LintAtPlan findings:");
eprintln!(" {msg}");
eprintln!();
eprintln!(
"Resolve by correcting the source schema, or add `[[lint_waiver]]` rows to your intent.toml."
);
return Ok(2);
}
Err(e) => return Err(anyhow::anyhow!("{e}")),
};
let out_dir = args.output.unwrap_or_else(|| default_plan_dir(cfg, &plan));
write_plan_dir(&plan, &out_dir)?;
println!(
"Wrote plan {} to {} ({} group(s), {} step(s), {} intent(s))",
plan.id.short(),
out_dir.display(),
plan.groups.len(),
plan.groups.iter().map(|g| g.steps.len()).sum::<usize>(),
plan.intents.len(),
);
for f in &plan.advisory_findings {
eprintln!("pgevolve plan: advisory [{}]: {}", f.rule, f.message);
}
if args.shadow_validate {
let source = pgevolve_core::parse::parse_directory(&cfg.project.schema_dir, &[])
.map_err(|e| anyhow::anyhow!("parse error: {e}"))?;
run_shadow_cross_check(&source, cfg, args.shadow_strict).await?;
}
Ok(0)
}
async fn run_shadow_cross_check(
source: &pgevolve_core::ir::catalog::Catalog,
cfg: &PgevolveConfig,
strict: bool,
) -> anyhow::Result<()> {
let shadow_cfg = cfg.shadow.as_ref().ok_or_else(|| {
anyhow::anyhow!("--shadow-validate requires a [shadow] section in pgevolve.toml")
})?;
let backend = crate::shadow::resolve(shadow_cfg)?;
let major = shadow_cfg
.postgres_version
.as_deref()
.and_then(|v| v.trim().parse::<u32>().ok())
.unwrap_or(17);
let report =
crate::shadow::validate::cross_check(backend.as_ref(), source, major, strict).await?;
let mismatch_count = report.canonical_mismatches.len()
+ report.extra_ast_edges.len()
+ report.missing_ast_edges.len();
if mismatch_count > 0 {
for m in &report.canonical_mismatches {
eprintln!(
" canonical mismatch {}: source={:?} catalog={:?}",
m.view_qname, m.source_canonical, m.catalog_canonical
);
}
for e in &report.extra_ast_edges {
eprintln!(" extra AST edge {}: {}", e.view_qname, e.dep_node);
}
for m in &report.missing_ast_edges {
eprintln!(
" missing AST edge {}: {}.{}",
m.view_qname, m.ref_schema, m.ref_name
);
}
if strict {
anyhow::bail!("shadow-validate --strict: {mismatch_count} mismatch(es)");
}
}
let n_edges = report.structural_edges_checked;
eprintln!(
"shadow-validate: ok ({n_edges} structural edge(s), {mismatch_count} canonical mismatch(es))"
);
Ok(())
}
fn load_existing_waivers(dir: &std::path::Path) -> Vec<LintWaiver> {
let path = dir.join("intent.toml");
let Ok(text) = std::fs::read_to_string(&path) else {
return Vec::new();
};
match pgevolve_core::plan::read_intent_toml(&text) {
Ok(parsed) => parsed.lint_waivers,
Err(_) => Vec::new(),
}
}
fn default_plan_dir(cfg: &PgevolveConfig, plan: &Plan) -> PathBuf {
let date = plan.metadata.created_at.date().to_string(); cfg.project
.plan_dir
.join(format!("{date}-{}", plan.id.short()))
}
fn detect_git_rev() -> std::io::Result<String> {
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.output()?;
if out.status.success() {
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
Ok(format!("git:{s}"))
} else {
Err(std::io::Error::other("git rev-parse failed"))
}
}