1use thiserror::Error;
4
5pub type MigrateResult<T> = Result<T, MigrationError>;
7
8#[derive(Debug, Error)]
10pub enum MigrationError {
11 #[error("I/O error: {0}")]
13 Io(#[from] std::io::Error),
14
15 #[error("Database error: {0}")]
17 Database(String),
18
19 #[error("Schema error: {0}")]
21 Schema(String),
22
23 #[error("Invalid migration: {0}")]
25 InvalidMigration(String),
26
27 #[error("Checksum mismatch for migration '{id}': expected {expected}, got {actual}")]
29 ChecksumMismatch {
30 id: String,
32 expected: String,
34 actual: String,
36 },
37
38 #[error("Migration '{0}' has already been applied")]
40 AlreadyApplied(String),
41
42 #[error("Migration '{0}' not found")]
44 NotFound(String),
45
46 #[error("Data loss would occur: {0}")]
48 DataLoss(String),
49
50 #[error("Failed to acquire migration lock: {0}")]
52 LockFailed(String),
53
54 #[error("No schema changes detected")]
56 NoChanges,
57
58 #[error("Cannot rollback: {0}")]
60 RollbackFailed(String),
61
62 #[error("Shadow database error: {0}")]
64 ShadowDatabaseError(String),
65
66 #[error("Resolution file error: {0}")]
68 ResolutionFile(String),
69
70 #[error("Resolution conflict: {0}")]
72 ResolutionConflict(String),
73
74 #[error("Migration conflict: migrations '{0}' and '{1}' conflict")]
76 MigrationConflict(String, String),
77
78 #[error("Migration error: {0}")]
80 Other(String),
81}
82
83impl MigrationError {
84 pub fn database(msg: impl Into<String>) -> Self {
86 Self::Database(msg.into())
87 }
88
89 pub fn schema(msg: impl Into<String>) -> Self {
91 Self::Schema(msg.into())
92 }
93
94 pub fn data_loss(msg: impl Into<String>) -> Self {
96 Self::DataLoss(msg.into())
97 }
98
99 pub fn lock_failed(msg: impl Into<String>) -> Self {
101 Self::LockFailed(msg.into())
102 }
103
104 pub fn other(msg: impl Into<String>) -> Self {
106 Self::Other(msg.into())
107 }
108
109 pub fn shadow_database(msg: impl Into<String>) -> Self {
111 Self::ShadowDatabaseError(msg.into())
112 }
113
114 pub fn resolution_file(msg: impl Into<String>) -> Self {
116 Self::ResolutionFile(msg.into())
117 }
118
119 pub fn resolution_conflict(msg: impl Into<String>) -> Self {
121 Self::ResolutionConflict(msg.into())
122 }
123
124 pub fn migration_conflict(m1: impl Into<String>, m2: impl Into<String>) -> Self {
126 Self::MigrationConflict(m1.into(), m2.into())
127 }
128
129 pub fn migration_file(msg: impl Into<String>) -> Self {
131 Self::InvalidMigration(msg.into())
132 }
133
134 pub fn is_recoverable(&self) -> bool {
136 matches!(
137 self,
138 Self::LockFailed(_) | Self::AlreadyApplied(_) | Self::NoChanges
139 )
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_error_display() {
149 let err = MigrationError::NotFound("20231215_test".to_string());
150 assert!(err.to_string().contains("20231215_test"));
151 }
152
153 #[test]
154 fn test_checksum_mismatch_display() {
155 let err = MigrationError::ChecksumMismatch {
156 id: "test".to_string(),
157 expected: "abc".to_string(),
158 actual: "xyz".to_string(),
159 };
160 let msg = err.to_string();
161 assert!(msg.contains("abc"));
162 assert!(msg.contains("xyz"));
163 }
164
165 #[test]
166 fn test_is_recoverable() {
167 assert!(MigrationError::NoChanges.is_recoverable());
168 assert!(MigrationError::LockFailed("timeout".to_string()).is_recoverable());
169 assert!(!MigrationError::Database("connection".to_string()).is_recoverable());
170 }
171}