Skip to main content

waypoint_core/commands/
validate.rs

1//! Validate applied migrations against local files (checksum and ordering).
2
3use std::collections::HashMap;
4
5use serde::Serialize;
6use tokio_postgres::Client;
7
8use crate::config::WaypointConfig;
9use crate::error::{Result, WaypointError};
10use crate::history;
11use crate::migration::{scan_migrations, ResolvedMigration};
12
13/// Report returned after a validate operation.
14#[derive(Debug, Serialize)]
15pub struct ValidateReport {
16    pub valid: bool,
17    pub issues: Vec<String>,
18    pub warnings: Vec<String>,
19}
20
21/// Execute the validate command.
22///
23/// For each applied (success=TRUE) migration in history:
24/// - Find the corresponding file on disk
25/// - Recalculate the checksum
26/// - Report mismatches
27/// - Warn if file is missing
28pub async fn execute(client: &Client, config: &WaypointConfig) -> Result<ValidateReport> {
29    let schema = &config.migrations.schema;
30    let table = &config.migrations.table;
31
32    if !history::history_table_exists(client, schema, table).await? {
33        return Ok(ValidateReport {
34            valid: true,
35            issues: Vec::new(),
36            warnings: vec!["No history table found — nothing to validate.".to_string()],
37        });
38    }
39
40    let applied = history::get_applied_migrations(client, schema, table).await?;
41    let resolved = scan_migrations(&config.migrations.locations)?;
42
43    // Build lookup maps
44    let resolved_by_version: HashMap<String, &ResolvedMigration> = resolved
45        .iter()
46        .filter(|m| m.is_versioned())
47        .filter_map(|m| m.version().map(|v| (v.raw.clone(), m)))
48        .collect();
49
50    let resolved_by_script: HashMap<String, &ResolvedMigration> = resolved
51        .iter()
52        .filter(|m| !m.is_versioned())
53        .map(|m| (m.script.clone(), m))
54        .collect();
55
56    let mut issues = Vec::new();
57    let mut warnings = Vec::new();
58
59    for am in &applied {
60        if !am.success {
61            continue;
62        }
63        if am.migration_type == "BASELINE" {
64            continue;
65        }
66
67        // Distinguish by version presence for Flyway compatibility
68        if am.version.is_some() {
69            // Versioned migration
70            if let Some(ref version) = am.version {
71                if let Some(resolved) = resolved_by_version.get(version) {
72                    if let Some(expected_checksum) = am.checksum {
73                        if resolved.checksum != expected_checksum {
74                            issues.push(format!(
75                                "Checksum mismatch for version {}: applied={}, resolved={}. \
76                                 Migration file '{}' has been modified after it was applied.",
77                                version, expected_checksum, resolved.checksum, resolved.script
78                            ));
79                        }
80                    }
81                } else {
82                    warnings.push(format!(
83                        "Applied migration version {} (script: {}) not found on disk.",
84                        version, am.script
85                    ));
86                }
87            }
88        } else {
89            // Repeatable (version is NULL) — we only warn on missing, don't fail on checksum diff
90            // (checksum diff is expected and triggers re-apply)
91            if !resolved_by_script.contains_key(&am.script) {
92                warnings.push(format!(
93                    "Applied repeatable migration '{}' not found on disk.",
94                    am.script
95                ));
96            }
97        }
98    }
99
100    let valid = issues.is_empty();
101
102    tracing::info!(
103        valid = valid,
104        issue_count = issues.len(),
105        warning_count = warnings.len(),
106        "Validation completed"
107    );
108
109    if !valid {
110        return Err(WaypointError::ValidationFailed(issues.join("\n")));
111    }
112
113    Ok(ValidateReport {
114        valid,
115        issues,
116        warnings,
117    })
118}