Skip to main content

waypoint_core/commands/
migrate.rs

1//! Apply pending migrations to the database.
2//!
3//! This module owns the engine-agnostic public types ([`MigrateReport`],
4//! [`MigrateDetail`]) and a handful of shared helpers used by both
5//! engine-specific implementations. The actual `execute*` entry points
6//! live in [`crate::engines::postgres::migrate`] and
7//! [`crate::engines::mysql::migrate`] and are re-exported here so that
8//! downstream callers (and the library `Waypoint` façade) can keep using
9//! the historical paths under `crate::commands::migrate::*`.
10
11use serde::Serialize;
12
13use crate::directive::MigrationDirectives;
14use crate::error::WaypointError;
15
16// ── Re-exports of the engine-specific entry points ──────────────────────────
17//
18// `multi.rs` and `lib.rs` reference these paths today. Keeping the names
19// where they used to live preserves the public API and back-compat.
20
21#[cfg(feature = "mysql")]
22pub use crate::engines::mysql::migrate::{
23    execute as execute_mysql, execute_with_options as execute_mysql_with_options,
24};
25#[cfg(feature = "postgres")]
26pub use crate::engines::postgres::migrate::{execute, execute_with_options};
27
28// ── Engine-agnostic public types ────────────────────────────────────────────
29
30/// Report returned after a migrate operation.
31#[derive(Debug, Serialize)]
32pub struct MigrateReport {
33    /// Number of migrations that were applied in this run.
34    pub migrations_applied: usize,
35    /// Total execution time of all migrations in milliseconds.
36    pub total_time_ms: i32,
37    /// Per-migration details for each applied migration.
38    pub details: Vec<MigrateDetail>,
39    /// Number of lifecycle hooks that were executed.
40    pub hooks_executed: usize,
41    /// Total execution time of all hooks in milliseconds.
42    pub hooks_time_ms: i32,
43}
44
45/// Details of a single applied migration within a migrate run.
46#[derive(Debug, Serialize)]
47pub struct MigrateDetail {
48    /// Version string, or None for repeatable migrations.
49    pub version: Option<String>,
50    /// Human-readable description from the migration filename.
51    pub description: String,
52    /// Filename of the migration script.
53    pub script: String,
54    /// Execution time of this migration in milliseconds.
55    pub execution_time_ms: i32,
56}
57
58// ── Shared helpers used by both engine paths ────────────────────────────────
59
60/// Result of evaluating require-guard preconditions for a single migration.
61pub(crate) enum GuardAction {
62    /// All preconditions passed; proceed with the migration.
63    Continue,
64    /// A precondition failed with on_require_fail=Skip; skip this migration.
65    Skip,
66    /// A precondition failed fatally; abort with the given error.
67    Error(WaypointError),
68}
69
70/// Check if a migration should run in the current environment.
71///
72/// Returns true if:
73/// - The migration has no env directives (runs everywhere)
74/// - No environment is configured (runs everything)
75/// - The migration's env list includes the current environment
76pub(crate) fn should_run_in_environment(
77    directives: &MigrationDirectives,
78    current_env: Option<&str>,
79) -> bool {
80    if directives.env.is_empty() {
81        return true;
82    }
83    let env = match current_env {
84        Some(e) => e,
85        None => return true,
86    };
87    directives.env.iter().any(|e| e.eq_ignore_ascii_case(env))
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_should_run_in_environment_no_directives() {
96        let directives = MigrationDirectives::default();
97        assert!(should_run_in_environment(&directives, Some("production")));
98        assert!(should_run_in_environment(&directives, None));
99    }
100
101    #[test]
102    fn test_should_run_in_environment_matches() {
103        let directives = MigrationDirectives {
104            env: vec!["production".to_string(), "staging".to_string()],
105            ..Default::default()
106        };
107        assert!(should_run_in_environment(&directives, Some("production")));
108        assert!(should_run_in_environment(&directives, Some("staging")));
109        assert!(!should_run_in_environment(&directives, Some("dev")));
110    }
111
112    #[test]
113    fn test_should_run_in_environment_case_insensitive() {
114        let directives = MigrationDirectives {
115            env: vec!["PROD".to_string()],
116            ..Default::default()
117        };
118        assert!(should_run_in_environment(&directives, Some("prod")));
119        assert!(should_run_in_environment(&directives, Some("PROD")));
120        assert!(should_run_in_environment(&directives, Some("Prod")));
121        assert!(!should_run_in_environment(&directives, Some("dev")));
122    }
123
124    #[test]
125    fn test_should_run_in_environment_no_env_configured() {
126        let directives = MigrationDirectives {
127            env: vec!["prod".to_string()],
128            ..Default::default()
129        };
130        assert!(should_run_in_environment(&directives, None));
131    }
132}