Skip to main content

waypoint_core/
error.rs

1//! Error types for Waypoint operations.
2
3use thiserror::Error;
4
5/// Extract the full error message from a tokio_postgres::Error,
6/// including the underlying DbError details that Display hides.
7#[cfg(feature = "postgres")]
8pub fn format_db_error(e: &tokio_postgres::Error) -> String {
9    // The source chain contains the actual DbError with message/detail/hint
10    if let Some(db_err) = e.as_db_error() {
11        let mut msg = db_err.message().to_string();
12        if let Some(detail) = db_err.detail() {
13            msg.push_str(&format!("\n  Detail: {}", detail));
14        }
15        if let Some(hint) = db_err.hint() {
16            msg.push_str(&format!("\n  Hint: {}", hint));
17        }
18        if let Some(position) = db_err.position() {
19            msg.push_str(&format!("\n  Position: {:?}", position));
20        }
21        return msg;
22    }
23    // Fallback: walk the source chain
24    let mut msg = e.to_string();
25    let mut source = std::error::Error::source(e);
26    while let Some(s) = source {
27        msg.push_str(&format!(": {}", s));
28        source = s.source();
29    }
30    // Append connection-loss context when the connection is closed
31    if e.is_closed() {
32        msg.push_str("\n  Note: The database connection was closed unexpectedly. This may indicate a network issue or server restart.");
33    }
34    msg
35}
36
37/// All error types that Waypoint operations can produce.
38#[derive(Error, Debug)]
39pub enum WaypointError {
40    /// Invalid or missing configuration (TOML parse errors, missing required fields, etc.).
41    #[error("Configuration error: {0}")]
42    ConfigError(String),
43
44    /// A database query or connection operation failed (PostgreSQL).
45    #[cfg(feature = "postgres")]
46    #[error("Database error: {}", format_db_error(.0))]
47    DatabaseError(#[from] tokio_postgres::Error),
48
49    /// A database query or connection operation failed (MySQL).
50    #[cfg(feature = "mysql")]
51    #[error("Database error: {0}")]
52    MysqlError(#[from] mysql_async::Error),
53
54    /// A migration filename could not be parsed into a valid migration.
55    #[error("Migration parse error: {0}")]
56    MigrationParseError(String),
57
58    /// **Reserved / unused.** No code path constructs this variant — checksum
59    /// mismatches surface as `ValidationFailed(String)` from the `validate`
60    /// command (which aggregates one or more mismatches into a single
61    /// human-readable string). Kept because removing an enum variant is a
62    /// breaking change; it goes in the 1.0 cleanup along with the other
63    /// deprecated surface.
64    #[deprecated(
65        since = "0.3.4",
66        note = "Never produced — checksum mismatches surface as ValidationFailed. Will be removed in 1.0."
67    )]
68    #[error("Checksum mismatch for migration {script}: expected {expected}, found {found}")]
69    ChecksumMismatch {
70        script: String,
71        expected: i32,
72        found: i32,
73    },
74
75    /// One or more validation checks failed before migration could proceed.
76    #[error("Validation failed:\n{0}")]
77    ValidationFailed(String),
78
79    /// A migration script failed to execute against the database.
80    #[error("Migration failed for {script}: {reason}")]
81    MigrationFailed { script: String, reason: String },
82
83    /// Could not acquire the PostgreSQL advisory lock used to prevent concurrent migrations.
84    #[error("Failed to acquire advisory lock: {0}")]
85    LockError(String),
86
87    /// The `clean` command was invoked but clean is not enabled in the configuration.
88    #[error(
89        "Clean is disabled. Pass --allow-clean to enable it or set clean_enabled = true in config."
90    )]
91    CleanDisabled,
92
93    /// A baseline was requested but the schema history table already contains entries.
94    #[error("Baseline already exists. The schema history table is not empty.")]
95    BaselineExists,
96
97    /// A filesystem I/O operation failed (reading migration files, config, etc.).
98    #[error("IO error: {0}")]
99    IoError(#[from] std::io::Error),
100
101    /// A migration version is lower than the highest applied version and out-of-order is disabled.
102    #[error(
103        "Out-of-order migration not allowed: version {version} is below the highest applied version {highest}. Enable out_of_order to allow this."
104    )]
105    OutOfOrder { version: String, highest: String },
106
107    /// A `${key}` placeholder in migration SQL has no corresponding value defined.
108    #[error("Placeholder '{key}' not found. Available placeholders: {available}")]
109    PlaceholderNotFound { key: String, available: String },
110
111    /// A SQL callback hook script failed during execution.
112    #[error("Hook failed during {phase} ({script}): {reason}")]
113    HookFailed {
114        phase: String,
115        script: String,
116        reason: String,
117    },
118
119    /// The self-update mechanism encountered an error.
120    #[error("Self-update failed: {0}")]
121    UpdateError(String),
122
123    /// An undo migration script failed to execute against the database.
124    #[error("Undo failed for {script}: {reason}")]
125    UndoFailed { script: String, reason: String },
126
127    /// No undo migration file was found for the requested version.
128    #[error("No undo migration found for version {version}. Expected U{version}__*.sql file.")]
129    UndoMissing { version: String },
130
131    /// Lint analysis found one or more errors in migration SQL.
132    #[error("Lint found {error_count} error(s): {details}")]
133    LintFailed { error_count: usize, details: String },
134
135    /// **Reserved / unused.** No code path constructs this variant — the
136    /// `diff` command surfaces failures as `ConfigError(String)` or
137    /// `DatabaseError`. Kept because removing an enum variant is a breaking
138    /// change; it goes in the 1.0 cleanup along with the other deprecated
139    /// surface.
140    #[deprecated(
141        since = "0.3.4",
142        note = "Never produced — diff failures surface as ConfigError or DatabaseError. Will be removed in 1.0."
143    )]
144    #[error("Diff failed: {reason}")]
145    DiffFailed { reason: String },
146
147    /// The live database schema differs from the expected snapshot.
148    #[error("Schema drift detected: {count} difference(s): {details}")]
149    DriftDetected { count: usize, details: String },
150
151    /// A schema snapshot operation (save, load, or compare) failed.
152    #[error("Snapshot error: {reason}")]
153    SnapshotError { reason: String },
154
155    /// A circular dependency was detected among migration `@depends` directives.
156    #[error("Migration dependency cycle detected: {path}")]
157    DependencyCycle { path: String },
158
159    /// A migration declares a dependency on a version that does not exist on disk.
160    #[error("Migration V{version} depends on V{dependency}, which does not exist")]
161    MissingDependency { version: String, dependency: String },
162
163    /// A migration directive comment is malformed or contains invalid values.
164    #[error("Invalid directive in {script}: {reason}")]
165    InvalidDirective { script: String, reason: String },
166
167    /// A Git operation required for branch conflict detection failed.
168    #[error("Git error: {0}")]
169    GitError(String),
170
171    /// Multiple branches introduced conflicting migration versions.
172    #[error("Migration conflicts detected: {count} conflict(s): {details}")]
173    ConflictsDetected { count: usize, details: String },
174
175    /// A named database referenced in multi-database config was not found.
176    #[error("Database '{name}' not found. Available: {available}")]
177    DatabaseNotFound { name: String, available: String },
178
179    /// A circular dependency was detected among multi-database `depends_on` declarations.
180    #[error("Multi-database dependency cycle: {path}")]
181    MultiDbDependencyCycle { path: String },
182
183    /// A multi-database migration operation failed for a specific named database.
184    #[error("Multi-database error for '{name}': {reason}")]
185    MultiDbError { name: String, reason: String },
186
187    /// One or more pre-flight safety checks failed before migration could proceed.
188    #[error("Pre-flight checks failed: {checks}")]
189    PreflightFailed { checks: String },
190
191    /// A guard precondition or postcondition check failed.
192    #[error("Guard {kind} failed for {script}: {expression}")]
193    GuardFailed {
194        kind: String,
195        script: String,
196        expression: String,
197    },
198
199    /// A migration was blocked by a DANGER safety verdict.
200    #[error("Migration blocked for {script}: {reason}. Use --force to override.")]
201    MigrationBlocked { script: String, reason: String },
202
203    /// A schema advisor analysis encountered an error.
204    #[error("Advisor error: {0}")]
205    AdvisorError(String),
206
207    /// A migration simulation failed.
208    #[error("Simulation failed: {reason}")]
209    SimulationFailed { reason: String },
210
211    /// A migration contains statements that cannot run inside a transaction (e.g. CONCURRENTLY).
212    #[error(
213        "Migration {script} contains non-transactional statement: {statement}. Remove --transaction or rewrite the migration."
214    )]
215    NonTransactionalStatement { script: String, statement: String },
216
217    /// The database connection was lost during an operation.
218    #[error("Connection lost during {operation}: {detail}")]
219    ConnectionLost { operation: String, detail: String },
220}
221
222/// Convenience type alias for `Result<T, WaypointError>`.
223pub type Result<T> = std::result::Result<T, WaypointError>;