Skip to main content

waypoint_core/commands/
safety.rs

1//! Standalone `waypoint safety` command for analyzing migration files.
2
3use serde::Serialize;
4use tokio_postgres::Client;
5
6use crate::config::WaypointConfig;
7use crate::error::Result;
8use crate::safety;
9
10/// Report from the standalone safety analysis command.
11#[derive(Debug, Clone, Serialize)]
12pub struct SafetyCommandReport {
13    /// Per-file safety reports.
14    pub reports: Vec<safety::SafetyReport>,
15    /// Overall verdict across all files.
16    pub overall_verdict: safety::SafetyVerdict,
17}
18
19/// Analyze a single migration file for safety.
20pub async fn execute_file(
21    client: &Client,
22    config: &WaypointConfig,
23    file_path: &str,
24) -> Result<safety::SafetyReport> {
25    let sql = std::fs::read_to_string(file_path)?;
26    let script = std::path::Path::new(file_path)
27        .file_name()
28        .map(|f| f.to_string_lossy().to_string())
29        .unwrap_or_else(|| file_path.to_string());
30
31    safety::analyze_migration(
32        client,
33        &config.migrations.schema,
34        &sql,
35        &script,
36        &config.safety,
37    )
38    .await
39}
40
41/// Analyze all pending migration files for safety.
42pub async fn execute(client: &Client, config: &WaypointConfig) -> Result<SafetyCommandReport> {
43    use crate::history;
44    use crate::migration::scan_migrations;
45
46    let schema = &config.migrations.schema;
47    let table = &config.migrations.table;
48
49    history::create_history_table(client, schema, table).await?;
50    let resolved = scan_migrations(&config.migrations.locations)?;
51    let applied = history::get_applied_migrations(client, schema, table).await?;
52    let effective = history::effective_applied_versions(&applied);
53
54    let mut reports = Vec::new();
55    let mut overall = safety::SafetyVerdict::Safe;
56
57    for migration in &resolved {
58        if migration.is_undo() {
59            continue;
60        }
61        if let Some(version) = migration.version() {
62            if effective.contains(&version.raw) {
63                continue; // Already applied
64            }
65        }
66
67        let report = safety::analyze_migration(
68            client,
69            schema,
70            &migration.sql,
71            &migration.script,
72            &config.safety,
73        )
74        .await?;
75
76        if report.overall_verdict > overall {
77            overall = report.overall_verdict;
78        }
79        reports.push(report);
80    }
81
82    Ok(SafetyCommandReport {
83        reports,
84        overall_verdict: overall,
85    })
86}