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 currently constructs this variant —
59    /// checksum mismatches surface as `ValidationFailed(String)` from the
60    /// `validate` command (which aggregates one or more mismatches into a
61    /// single human-readable string). Kept for back-compat with external
62    /// matchers; scheduled for removal in 0.4.0.
63    #[deprecated(
64        since = "0.3.4",
65        note = "Never produced — checksum mismatches surface as ValidationFailed. Will be removed in 0.4.0."
66    )]
67    #[error("Checksum mismatch for migration {script}: expected {expected}, found {found}")]
68    ChecksumMismatch {
69        script: String,
70        expected: i32,
71        found: i32,
72    },
73
74    /// One or more validation checks failed before migration could proceed.
75    #[error("Validation failed:\n{0}")]
76    ValidationFailed(String),
77
78    /// A migration script failed to execute against the database.
79    #[error("Migration failed for {script}: {reason}")]
80    MigrationFailed { script: String, reason: String },
81
82    /// Could not acquire the PostgreSQL advisory lock used to prevent concurrent migrations.
83    #[error("Failed to acquire advisory lock: {0}")]
84    LockError(String),
85
86    /// The `clean` command was invoked but clean is not enabled in the configuration.
87    #[error(
88        "Clean is disabled. Pass --allow-clean to enable it or set clean_enabled = true in config."
89    )]
90    CleanDisabled,
91
92    /// A baseline was requested but the schema history table already contains entries.
93    #[error("Baseline already exists. The schema history table is not empty.")]
94    BaselineExists,
95
96    /// A filesystem I/O operation failed (reading migration files, config, etc.).
97    #[error("IO error: {0}")]
98    IoError(#[from] std::io::Error),
99
100    /// A migration version is lower than the highest applied version and out-of-order is disabled.
101    #[error("Out-of-order migration not allowed: version {version} is below the highest applied version {highest}. Enable out_of_order to allow this.")]
102    OutOfOrder { version: String, highest: String },
103
104    /// A `${key}` placeholder in migration SQL has no corresponding value defined.
105    #[error("Placeholder '{key}' not found. Available placeholders: {available}")]
106    PlaceholderNotFound { key: String, available: String },
107
108    /// A SQL callback hook script failed during execution.
109    #[error("Hook failed during {phase} ({script}): {reason}")]
110    HookFailed {
111        phase: String,
112        script: String,
113        reason: String,
114    },
115
116    /// The self-update mechanism encountered an error.
117    #[error("Self-update failed: {0}")]
118    UpdateError(String),
119
120    /// An undo migration script failed to execute against the database.
121    #[error("Undo failed for {script}: {reason}")]
122    UndoFailed { script: String, reason: String },
123
124    /// No undo migration file was found for the requested version.
125    #[error("No undo migration found for version {version}. Expected U{version}__*.sql file.")]
126    UndoMissing { version: String },
127
128    /// Lint analysis found one or more errors in migration SQL.
129    #[error("Lint found {error_count} error(s): {details}")]
130    LintFailed { error_count: usize, details: String },
131
132    /// **Reserved / unused.** No code path currently constructs this variant —
133    /// the `diff` command surfaces failures as `ConfigError(String)` or
134    /// `DatabaseError`. Kept for back-compat with external matchers; scheduled
135    /// for removal in 0.4.0.
136    #[deprecated(
137        since = "0.3.4",
138        note = "Never produced — diff failures surface as ConfigError or DatabaseError. Will be removed in 0.4.0."
139    )]
140    #[error("Diff failed: {reason}")]
141    DiffFailed { reason: String },
142
143    /// The live database schema differs from the expected snapshot.
144    #[error("Schema drift detected: {count} difference(s): {details}")]
145    DriftDetected { count: usize, details: String },
146
147    /// A schema snapshot operation (save, load, or compare) failed.
148    #[error("Snapshot error: {reason}")]
149    SnapshotError { reason: String },
150
151    /// A circular dependency was detected among migration `@depends` directives.
152    #[error("Migration dependency cycle detected: {path}")]
153    DependencyCycle { path: String },
154
155    /// A migration declares a dependency on a version that does not exist on disk.
156    #[error("Migration V{version} depends on V{dependency}, which does not exist")]
157    MissingDependency { version: String, dependency: String },
158
159    /// A migration directive comment is malformed or contains invalid values.
160    #[error("Invalid directive in {script}: {reason}")]
161    InvalidDirective { script: String, reason: String },
162
163    /// A Git operation required for branch conflict detection failed.
164    #[error("Git error: {0}")]
165    GitError(String),
166
167    /// Multiple branches introduced conflicting migration versions.
168    #[error("Migration conflicts detected: {count} conflict(s): {details}")]
169    ConflictsDetected { count: usize, details: String },
170
171    /// A named database referenced in multi-database config was not found.
172    #[error("Database '{name}' not found. Available: {available}")]
173    DatabaseNotFound { name: String, available: String },
174
175    /// A circular dependency was detected among multi-database `depends_on` declarations.
176    #[error("Multi-database dependency cycle: {path}")]
177    MultiDbDependencyCycle { path: String },
178
179    /// A multi-database migration operation failed for a specific named database.
180    #[error("Multi-database error for '{name}': {reason}")]
181    MultiDbError { name: String, reason: String },
182
183    /// One or more pre-flight safety checks failed before migration could proceed.
184    #[error("Pre-flight checks failed: {checks}")]
185    PreflightFailed { checks: String },
186
187    /// A guard precondition or postcondition check failed.
188    #[error("Guard {kind} failed for {script}: {expression}")]
189    GuardFailed {
190        kind: String,
191        script: String,
192        expression: String,
193    },
194
195    /// A migration was blocked by a DANGER safety verdict.
196    #[error("Migration blocked for {script}: {reason}. Use --force to override.")]
197    MigrationBlocked { script: String, reason: String },
198
199    /// A schema advisor analysis encountered an error.
200    #[error("Advisor error: {0}")]
201    AdvisorError(String),
202
203    /// A migration simulation failed.
204    #[error("Simulation failed: {reason}")]
205    SimulationFailed { reason: String },
206
207    /// A migration contains statements that cannot run inside a transaction (e.g. CONCURRENTLY).
208    #[error("Migration {script} contains non-transactional statement: {statement}. Remove --transaction or rewrite the migration.")]
209    NonTransactionalStatement { script: String, statement: String },
210
211    /// The database connection was lost during an operation.
212    #[error("Connection lost during {operation}: {detail}")]
213    ConnectionLost { operation: String, detail: String },
214}
215
216/// Convenience type alias for `Result<T, WaypointError>`.
217pub type Result<T> = std::result::Result<T, WaypointError>;