Skip to main content

waypoint_core/commands/
safety.rs

1//! Standalone `waypoint safety` command for analyzing migration files.
2
3use serde::Serialize;
4
5#[cfg(feature = "postgres")]
6use tokio_postgres::Client;
7
8use crate::config::WaypointConfig;
9use crate::db::DbClient;
10use crate::error::Result;
11use crate::safety;
12
13/// Report from the standalone safety analysis command.
14#[derive(Debug, Clone, Serialize)]
15pub struct SafetyCommandReport {
16    /// Per-file safety reports.
17    pub reports: Vec<safety::SafetyReport>,
18    /// Overall verdict across all files.
19    pub overall_verdict: safety::SafetyVerdict,
20}
21
22/// Analyze a single migration file for safety (PostgreSQL legacy entry).
23#[cfg(feature = "postgres")]
24pub async fn execute_file(
25    client: &Client,
26    config: &WaypointConfig,
27    file_path: &str,
28) -> Result<safety::SafetyReport> {
29    let sql = std::fs::read_to_string(file_path)?;
30    let script = filename_from_path(file_path);
31    safety::analyze_migration(
32        client,
33        &config.migrations.schema,
34        &sql,
35        &script,
36        &config.safety,
37    )
38    .await
39}
40
41/// Analyze a single migration file for safety (dialect-aware entry).
42pub async fn execute_file_db(
43    client: &DbClient,
44    config: &WaypointConfig,
45    file_path: &str,
46) -> Result<safety::SafetyReport> {
47    let sql = std::fs::read_to_string(file_path)?;
48    let script = filename_from_path(file_path);
49    let schema = client.resolve_schema(&config.migrations.schema).await?;
50    safety::analyze_migration_db(client, &schema, &sql, &script, &config.safety).await
51}
52
53fn filename_from_path(file_path: &str) -> String {
54    std::path::Path::new(file_path)
55        .file_name()
56        .map(|f| f.to_string_lossy().to_string())
57        .unwrap_or_else(|| file_path.to_string())
58}
59
60/// Analyze all pending migration files for safety (PostgreSQL legacy entry).
61#[cfg(feature = "postgres")]
62#[deprecated(
63    since = "0.6.0",
64    note = "Unused PostgreSQL-only entry point superseded by `execute_db`, which handles both engines. Will be removed in 1.0."
65)]
66pub async fn execute(client: &Client, config: &WaypointConfig) -> Result<SafetyCommandReport> {
67    use crate::history;
68    use crate::migration::scan_migrations;
69
70    let schema = &config.migrations.schema;
71    let table = &config.migrations.table;
72
73    history::create_history_table(client, schema, table).await?;
74    let resolved = scan_migrations(&config.migrations.locations)?;
75    let applied = history::get_applied_migrations(client, schema, table).await?;
76    let effective = history::effective_applied_versions(&applied);
77
78    let mut reports = Vec::new();
79    let mut overall = safety::SafetyVerdict::Safe;
80
81    for migration in &resolved {
82        if migration.is_undo() {
83            continue;
84        }
85        if let Some(version) = migration.version()
86            && effective.contains(&version.raw)
87        {
88            continue;
89        }
90
91        let report = safety::analyze_migration(
92            client,
93            schema,
94            &migration.sql,
95            &migration.script,
96            &config.safety,
97        )
98        .await?;
99
100        if report.overall_verdict > overall {
101            overall = report.overall_verdict;
102        }
103        reports.push(report);
104    }
105
106    Ok(SafetyCommandReport {
107        reports,
108        overall_verdict: overall,
109    })
110}
111
112/// Analyze all pending migration files for safety (dialect-aware entry).
113pub async fn execute_db(client: &DbClient, config: &WaypointConfig) -> Result<SafetyCommandReport> {
114    use crate::history;
115    use crate::migration::scan_migrations;
116
117    let schema = client.resolve_schema(&config.migrations.schema).await?;
118    let table = &config.migrations.table;
119
120    history::create_history_table_db(client, &schema, table).await?;
121    let resolved = scan_migrations(&config.migrations.locations)?;
122    let applied = history::get_applied_migrations_db(client, &schema, table).await?;
123    let effective = history::effective_applied_versions(&applied);
124
125    let mut reports = Vec::new();
126    let mut overall = safety::SafetyVerdict::Safe;
127
128    for migration in &resolved {
129        if migration.is_undo() {
130            continue;
131        }
132        if let Some(version) = migration.version()
133            && effective.contains(&version.raw)
134        {
135            continue;
136        }
137
138        let report = safety::analyze_migration_db(
139            client,
140            &schema,
141            &migration.sql,
142            &migration.script,
143            &config.safety,
144        )
145        .await?;
146
147        if report.overall_verdict > overall {
148            overall = report.overall_verdict;
149        }
150        reports.push(report);
151    }
152
153    Ok(SafetyCommandReport {
154        reports,
155        overall_verdict: overall,
156    })
157}