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