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,
18 pub issues: Vec<String>,
20 pub warnings: Vec<String>,
22}
23
24pub async fn execute(client: &Client, config: &WaypointConfig) -> Result<ValidateReport> {
32 let schema = &config.migrations.schema;
33 let table = &config.migrations.table;
34
35 if !history::history_table_exists(client, schema, table).await? {
36 return Ok(ValidateReport {
37 valid: true,
38 issues: Vec::new(),
39 warnings: vec!["No history table found — nothing to validate.".to_string()],
40 });
41 }
42
43 let applied = history::get_applied_migrations(client, schema, table).await?;
44 let resolved = scan_migrations(&config.migrations.locations)?;
45
46 let resolved_by_version: HashMap<String, &ResolvedMigration> = resolved
48 .iter()
49 .filter(|m| m.is_versioned())
50 .filter_map(|m| m.version().map(|v| (v.raw.clone(), m)))
51 .collect();
52
53 let resolved_by_script: HashMap<String, &ResolvedMigration> = resolved
54 .iter()
55 .filter(|m| !m.is_versioned())
56 .map(|m| (m.script.clone(), m))
57 .collect();
58
59 let mut issues = Vec::new();
60 let mut warnings = Vec::new();
61
62 for am in &applied {
63 if !am.success {
64 continue;
65 }
66 if am.migration_type == "BASELINE" {
67 continue;
68 }
69 if am.migration_type == "UNDO_SQL" {
70 continue;
71 }
72
73 if am.version.is_some() {
75 if let Some(ref version) = am.version {
77 if let Some(resolved) = resolved_by_version.get(version) {
78 if let Some(expected_checksum) = am.checksum {
79 if resolved.checksum != expected_checksum {
80 issues.push(format!(
81 "Checksum mismatch for version {}: applied={}, resolved={}. \
82 Migration file '{}' has been modified after it was applied.",
83 version, expected_checksum, resolved.checksum, resolved.script
84 ));
85 }
86 }
87 } else {
88 warnings.push(format!(
89 "Applied migration version {} (script: {}) not found on disk.",
90 version, am.script
91 ));
92 }
93 }
94 } else {
95 if !resolved_by_script.contains_key(&am.script) {
98 warnings.push(format!(
99 "Applied repeatable migration '{}' not found on disk.",
100 am.script
101 ));
102 }
103 }
104 }
105
106 let valid = issues.is_empty();
107
108 log::info!(
109 "Validation completed; valid={}, issue_count={}, warning_count={}",
110 valid,
111 issues.len(),
112 warnings.len()
113 );
114
115 if !valid {
116 return Err(WaypointError::ValidationFailed(issues.join("\n")));
117 }
118
119 Ok(ValidateReport {
120 valid,
121 issues,
122 warnings,
123 })
124}