pub mod cluster;
pub use cluster::{ClusterPlan, ClusterPlanError, build_cluster_plan};
use std::path::Path;
use tokio_postgres::Client;
use pgevolve_core::catalog::{CatalogFilter, read_catalog};
use pgevolve_core::diff::diff;
use pgevolve_core::identifier::Identifier;
use pgevolve_core::lint::Severity;
use pgevolve_core::lint::universal::{check_changeset, run_drift_lints};
use pgevolve_core::plan::{
LintWaiver, Plan, PlannerPolicy, RecordedFinding, Strategy, group_steps, order,
rewrite_with_source,
};
use crate::pg_querier::PgCatalogQuerier;
use crate::target_identity::compute_target_identity;
#[derive(Debug, Clone)]
pub struct BuildPlanOptions {
pub managed_schemas: Vec<Identifier>,
pub ignore_objects: Vec<String>,
pub strategy: Strategy,
pub planner_ruleset_version: u32,
pub existing_lint_waivers: Vec<LintWaiver>,
pub source_rev: Option<String>,
}
impl Default for BuildPlanOptions {
fn default() -> Self {
Self {
managed_schemas: Vec::new(),
ignore_objects: Vec::new(),
strategy: Strategy::Online,
planner_ruleset_version: PlannerPolicy::default().planner_ruleset_version,
existing_lint_waivers: Vec::new(),
source_rev: None,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum BuildPlanError {
#[error("parse error: {0}")]
Parse(String),
#[error("catalog read error: {0}")]
CatalogRead(String),
#[error("planner error: {0}")]
Planner(String),
#[error("unwaived LintAtPlan findings: {0}")]
LintAtPlanRequiresWaiver(String),
#[error("connection error: {0}")]
Connection(String),
}
pub async fn build_plan(
schema_dir: &Path,
client: Client,
opts: BuildPlanOptions,
) -> Result<Plan, BuildPlanError> {
let source = pgevolve_core::parse::parse_directory(schema_dir, &[])
.map_err(|e| BuildPlanError::Parse(e.to_string()))?;
let target_identity = compute_target_identity(&client)
.await
.map_err(|e| BuildPlanError::Connection(e.to_string()))?;
let filter = CatalogFilter::new(opts.managed_schemas.clone(), opts.ignore_objects.clone())
.map_err(|e| BuildPlanError::CatalogRead(e.to_string()))?;
let querier =
PgCatalogQuerier::new(client).map_err(|e| BuildPlanError::Connection(e.to_string()))?;
let (target, drift) = tokio::task::spawn_blocking(move || read_catalog(&querier, &filter))
.await
.map_err(|e| BuildPlanError::Connection(format!("join error: {e}")))?
.map_err(|e| BuildPlanError::CatalogRead(e.to_string()))?;
let changes = diff(&target, &source, &drift);
let changeset_findings = check_changeset(&changes);
let policy = PlannerPolicy {
strategy: opts.strategy,
online: PlannerPolicy::default().online,
..PlannerPolicy::default()
};
let ordered = order(&target, &source, changes, &policy)
.map_err(|e| BuildPlanError::Planner(e.to_string()))?;
let steps = rewrite_with_source(ordered, &target, &source, &policy);
let groups = group_steps(steps);
let mut plan = Plan::from_grouped(
groups,
&source,
&target,
target_identity,
opts.source_rev.clone(),
pgevolve_core::VERSION,
opts.planner_ruleset_version,
)
.map_err(|e| BuildPlanError::Planner(e.to_string()))?;
plan.advisory_findings = changeset_findings
.into_iter()
.map(|f| {
let target_str = f.message.split(':').next().unwrap_or("").trim().to_string();
RecordedFinding {
rule: f.rule.to_string(),
target: target_str,
message: f.message,
}
})
.collect();
let drift_findings = run_drift_lints(&source, &target);
let lint_at_plan: Vec<_> = drift_findings
.iter()
.filter(|f| matches!(f.severity, Severity::LintAtPlan))
.collect();
if !lint_at_plan.is_empty() {
let unwaived: Vec<_> = lint_at_plan
.iter()
.filter(|f| !waiver_matches(f, &opts.existing_lint_waivers))
.collect();
if !unwaived.is_empty() {
let msg = unwaived
.iter()
.map(|f| format!("[{}] ({}): {}", f.rule, f.severity, f.message))
.collect::<Vec<_>>()
.join("; ");
return Err(BuildPlanError::LintAtPlanRequiresWaiver(msg));
}
plan.metadata.lint_at_plan_findings = lint_at_plan
.iter()
.map(|f| {
let target_str = f.message.split(':').next().unwrap_or("").trim().to_string();
RecordedFinding {
rule: f.rule.to_string(),
target: target_str,
message: f.message.clone(),
}
})
.collect();
}
Ok(plan)
}
fn waiver_matches(finding: &pgevolve_core::lint::Finding, waivers: &[LintWaiver]) -> bool {
waivers
.iter()
.any(|w| w.rule == finding.rule && finding.message.contains(&w.target))
}