1use siderite_orm::{BackendCapabilityError, BackendKind, OrmError};
4use thiserror::Error;
5
6#[derive(Debug, Error)]
8#[non_exhaustive]
9pub enum MigrationError {
10 #[error(
12 "migration `{id}` has uncertain RunSQL at operation {operation}; \
13 reconcile its effects before clearing the intent and retrying"
14 )]
15 UncertainSqlStep {
16 id: String,
18 operation: usize,
20 },
21 #[error(
23 "migration `{id}` has uncertain RunRust `{callback}` at operation \
24 {operation}; reconcile its effects before clearing the intent, or \
25 explicitly register it as replay safe"
26 )]
27 UncertainRustStep {
28 id: String,
30 callback: String,
32 operation: usize,
34 },
35 #[error("timed out waiting for the migration lock")]
40 LockTimeout,
41 #[error(
46 "the run finished for {applied:?}, but the migration lock release \
47 could not be confirmed, so lock ownership is uncertain: {source}"
48 )]
49 LockReleaseUncertain {
50 applied: Vec<String>,
53 #[source]
55 source: Box<MigrationError>,
56 },
57 #[error("unknown dependency `{dependency}` of migration `{id}`")]
59 MissingDependency {
60 id: String,
62 dependency: String,
64 },
65 #[error("cycle in migration graph: {}", .nodes.join(" -> "))]
67 Cycle {
68 nodes: Vec<String>,
70 },
71 #[error(
73 "multiple migration heads: {}. Create a merge migration or run squashmigrations",
74 .heads.join(", ")
75 )]
76 MultipleHeads {
77 heads: Vec<String>,
79 },
80 #[error("checksum mismatch for applied migration `{id}` (history {history}, file {file})")]
83 ChecksumMismatch {
84 id: String,
86 history: String,
88 file: String,
90 },
91 #[error("cannot reverse migration `{id}`: {reason}")]
93 Irreversible {
94 id: String,
96 reason: String,
98 },
99 #[error(transparent)]
101 Io(#[from] std::io::Error),
102 #[error("invalid migration JSON: {0}")]
104 Json(#[from] serde_json::Error),
105 #[error(transparent)]
107 Orm(#[from] OrmError),
108 #[error(transparent)]
110 Capability(#[from] BackendCapabilityError),
111 #[error("schema migrations are not supported by {0:?}")]
113 UnsupportedBackend(BackendKind),
114 #[error("{0}")]
116 State(String),
117 #[error("unregistered RunRust operation `{0}`")]
119 UnregisteredRust(String),
120 #[error("{0}")]
122 Usage(String),
123 #[error(
125 "MySQL migration `{id}` failed on statement {index} of {total} after earlier DDL committed (repair the schema by hand, then re-run): {source}"
126 )]
127 MysqlPartial {
128 id: String,
130 index: usize,
132 total: usize,
134 #[source]
136 source: OrmError,
137 },
138 #[error(
140 "MySQL migration `{id}` failed in operation {index} of {total} ({summary}) after earlier statements committed (repair the schema by hand, then re-run): {source}"
141 )]
142 MysqlOpPartial {
143 id: String,
145 index: usize,
147 total: usize,
149 summary: String,
151 #[source]
153 source: Box<MigrationError>,
154 },
155 #[error(
158 "PostgreSQL migration `{id}` (atomic: false) failed on statement {index} of {total} after earlier statements committed; they ran outside a transaction and cannot be rolled back (repair the schema by hand, then re-run `migrate` to resume at the failed statement): {source}"
159 )]
160 PostgresPartial {
161 id: String,
163 index: usize,
165 total: usize,
167 #[source]
169 source: OrmError,
170 },
171 #[error(
174 "PostgreSQL migration `{id}` (atomic: false) failed in operation {index} of {total} ({summary}) after earlier statements committed; they ran outside a transaction and cannot be rolled back (repair the schema by hand, then re-run `migrate` to resume at the failed operation): {source}"
175 )]
176 PostgresOpPartial {
177 id: String,
179 index: usize,
181 total: usize,
183 summary: String,
185 #[source]
187 source: Box<MigrationError>,
188 },
189}
190
191impl MigrationError {
192 pub fn state(msg: impl Into<String>) -> Self {
194 Self::State(msg.into())
195 }
196
197 pub fn usage(msg: impl Into<String>) -> Self {
199 Self::Usage(msg.into())
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use siderite_orm::BackendError;
207
208 #[test]
209 fn mysql_partial_names_the_failed_statement() {
210 let err = MigrationError::MysqlPartial {
211 id: "0002_add".into(),
212 index: 2,
213 total: 3,
214 source: BackendError::Database("table exists".into()).into(),
215 };
216 let msg = err.to_string();
217 assert!(msg.contains("statement 2 of 3"), "{msg}");
218 assert!(msg.contains("0002_add"), "{msg}");
219 assert!(msg.contains("repair the schema by hand"), "{msg}");
220 }
221
222 #[test]
223 fn mysql_op_partial_names_the_failed_operation() {
224 let err = MigrationError::MysqlOpPartial {
225 id: "0003_data".into(),
226 index: 2,
227 total: 2,
228 summary: "RunRust seed".into(),
229 source: Box::new(MigrationError::UnregisteredRust("seed".into())),
230 };
231 let msg = err.to_string();
232 assert!(msg.contains("operation 2 of 2 (RunRust seed)"), "{msg}");
233 assert!(msg.contains("0003_data"), "{msg}");
234 assert!(msg.contains("repair the schema by hand"), "{msg}");
235 }
236
237 #[test]
238 fn postgres_partial_names_the_failed_statement() {
239 let err = MigrationError::PostgresPartial {
240 id: "0004_add".into(),
241 index: 2,
242 total: 3,
243 source: BackendError::Database("relation already exists".into()).into(),
244 };
245 let msg = err.to_string();
246 assert!(msg.contains("statement 2 of 3"), "{msg}");
247 assert!(msg.contains("0004_add"), "{msg}");
248 assert!(msg.contains("atomic: false"), "{msg}");
249 assert!(msg.contains("cannot be rolled back"), "{msg}");
250 assert!(msg.contains("repair the schema by hand"), "{msg}");
251 }
252
253 #[test]
254 fn postgres_op_partial_names_the_failed_operation() {
255 let err = MigrationError::PostgresOpPartial {
256 id: "0005_data".into(),
257 index: 2,
258 total: 2,
259 summary: "RunRust seed".into(),
260 source: Box::new(MigrationError::UnregisteredRust("seed".into())),
261 };
262 let msg = err.to_string();
263 assert!(msg.contains("operation 2 of 2 (RunRust seed)"), "{msg}");
264 assert!(msg.contains("0005_data"), "{msg}");
265 assert!(msg.contains("atomic: false"), "{msg}");
266 assert!(msg.contains("repair the schema by hand"), "{msg}");
267 }
268}