waypoint_core/commands/
validate.rs1use 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#[derive(Debug, Serialize)]
15pub struct ValidateReport {
16 pub valid: bool,
17 pub issues: Vec<String>,
18 pub warnings: Vec<String>,
19}
20
21pub 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 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 if am.version.is_some() {
69 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 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}