waypoint_core/commands/
validate.rs1use std::collections::HashMap;
2
3use serde::Serialize;
4use tokio_postgres::Client;
5
6use crate::config::WaypointConfig;
7use crate::error::{Result, WaypointError};
8use crate::history;
9use crate::migration::{scan_migrations, ResolvedMigration};
10
11#[derive(Debug, Serialize)]
13pub struct ValidateReport {
14 pub valid: bool,
15 pub issues: Vec<String>,
16 pub warnings: Vec<String>,
17}
18
19pub async fn execute(client: &Client, config: &WaypointConfig) -> Result<ValidateReport> {
27 let schema = &config.migrations.schema;
28 let table = &config.migrations.table;
29
30 if !history::history_table_exists(client, schema, table).await? {
31 return Ok(ValidateReport {
32 valid: true,
33 issues: Vec::new(),
34 warnings: vec!["No history table found — nothing to validate.".to_string()],
35 });
36 }
37
38 let applied = history::get_applied_migrations(client, schema, table).await?;
39 let resolved = scan_migrations(&config.migrations.locations)?;
40
41 let resolved_by_version: HashMap<String, &ResolvedMigration> = resolved
43 .iter()
44 .filter(|m| m.is_versioned())
45 .filter_map(|m| m.version().map(|v| (v.raw.clone(), m)))
46 .collect();
47
48 let resolved_by_script: HashMap<String, &ResolvedMigration> = resolved
49 .iter()
50 .filter(|m| !m.is_versioned())
51 .map(|m| (m.script.clone(), m))
52 .collect();
53
54 let mut issues = Vec::new();
55 let mut warnings = Vec::new();
56
57 for am in &applied {
58 if !am.success {
59 continue;
60 }
61 if am.migration_type == "BASELINE" {
62 continue;
63 }
64
65 if am.version.is_some() {
67 if let Some(ref version) = am.version {
69 if let Some(resolved) = resolved_by_version.get(version) {
70 if let Some(expected_checksum) = am.checksum {
71 if resolved.checksum != expected_checksum {
72 issues.push(format!(
73 "Checksum mismatch for version {}: applied={}, resolved={}. \
74 Migration file '{}' has been modified after it was applied.",
75 version, expected_checksum, resolved.checksum, resolved.script
76 ));
77 }
78 }
79 } else {
80 warnings.push(format!(
81 "Applied migration version {} (script: {}) not found on disk.",
82 version, am.script
83 ));
84 }
85 }
86 } else {
87 if !resolved_by_script.contains_key(&am.script) {
90 warnings.push(format!(
91 "Applied repeatable migration '{}' not found on disk.",
92 am.script
93 ));
94 }
95 }
96 }
97
98 let valid = issues.is_empty();
99
100 tracing::info!(
101 valid = valid,
102 issue_count = issues.len(),
103 warning_count = warnings.len(),
104 "Validation completed"
105 );
106
107 if !valid {
108 return Err(WaypointError::ValidationFailed(issues.join("\n")));
109 }
110
111 Ok(ValidateReport {
112 valid,
113 issues,
114 warnings,
115 })
116}