1use std::path::PathBuf;
2
3use sqlx::error::ErrorKind;
4#[cfg(feature = "mysql")]
5use sqlx::mysql::MySqlDatabaseError;
6
7#[cfg(feature = "mysql")]
9const ER_LOCK_WAIT_TIMEOUT: u16 = 1205;
10#[cfg(feature = "mysql")]
12const ER_LOCK_DEADLOCK: u16 = 1213;
13#[cfg(feature = "postgres")]
15const PG_DEADLOCK_DETECTED: &str = "40P01";
16#[cfg(feature = "postgres")]
18const PG_LOCK_NOT_AVAILABLE: &str = "55P03";
19
20#[derive(Debug, thiserror::Error)]
23pub enum Error {
24 #[error(transparent)]
26 Database(sqlx::Error),
27
28 #[error("unique index `{}` was violated", .index.as_deref().unwrap_or("unknown"))]
31 UniqueViolation { index: Option<String> },
32
33 #[error("foreign key `{}` was violated", .constraint.as_deref().unwrap_or("unknown"))]
36 ForeignKeyViolation { constraint: Option<String> },
37
38 #[error("deadlock found; the transaction was rolled back")]
40 Deadlock,
41
42 #[error("lock wait timeout; the statement was rolled back")]
44 LockWaitTimeout,
45
46 #[error("could not read {}: {source}", path.display())]
48 Read {
49 path: PathBuf,
50 source: std::io::Error,
51 },
52
53 #[error("migration `{name}` is defined more than once")]
55 DuplicateMigration { name: String },
56
57 #[error("migration `{name}` must start with a 14-digit timestamp, such as 20261108000000_name")]
59 InvalidMigrationName { name: String },
60
61 #[error("migrations `{first}` and `{second}` have the same timestamp")]
63 DuplicateTimestamp { first: String, second: String },
64
65 #[error("another process is running migrations; gave up after {seconds} seconds")]
67 MigrationsLocked { seconds: u64 },
68
69 #[error("migration `{name}` failed, so the run stopped: {source}")]
71 MigrationFailed { name: String, source: sqlx::Error },
72
73 #[error("migration `{name}` failed to roll back: {source}")]
75 RollbackFailed { name: String, source: sqlx::Error },
76
77 #[error(
80 "migration version `{name}` was recorded as applied, but no folder with that version \
81 exists in this migrator, so it cannot be rolled back"
82 )]
83 UnknownMigration { name: String },
84
85 #[error(
88 "invalid identifier `{0}`: names must not be empty or contain a backtick or a double quote"
89 )]
90 InvalidIdentifier(String),
91
92 #[error(
95 "database URL scheme `{scheme}` is not supported by this build; enable its Cargo feature"
96 )]
97 UnsupportedDriver { scheme: String },
98
99 #[error("{operation} is not supported on {driver}")]
102 Unsupported {
103 operation: &'static str,
104 driver: &'static str,
105 },
106
107 #[error("column `{column}` is not fillable on table `{table}`")]
109 NotFillable { table: &'static str, column: String },
110
111 #[error("no query results for table `{table}`")]
113 NotFound { table: &'static str },
114
115 #[error("blocking call on an async thread; run it inside tokio::task::spawn_blocking")]
117 BlockingInRuntime,
118}
119
120impl From<sqlx::Error> for Error {
121 fn from(error: sqlx::Error) -> Self {
124 if let sqlx::Error::Database(db) = &error {
125 match db.kind() {
126 ErrorKind::UniqueViolation => {
127 return Self::UniqueViolation {
128 index: violated_name(db.as_ref(), true),
129 };
130 }
131 ErrorKind::ForeignKeyViolation => {
132 return Self::ForeignKeyViolation {
133 constraint: violated_name(db.as_ref(), false),
134 };
135 }
136 _ => {}
137 }
138 if let Some(kind) = lock_error(db.as_ref()) {
139 return kind;
140 }
141 }
142 Self::Database(error)
143 }
144}
145
146#[cfg_attr(not(feature = "mysql"), allow(unused_variables))]
149fn violated_name(db: &dyn sqlx::error::DatabaseError, unique: bool) -> Option<String> {
150 #[cfg(feature = "mysql")]
151 if db.try_downcast_ref::<MySqlDatabaseError>().is_some() {
152 return if unique {
153 duplicate_key(db.message())
154 } else {
155 foreign_key(db.message())
156 };
157 }
158 db.constraint().map(str::to_owned)
159}
160
161#[cfg_attr(
163 not(any(feature = "mysql", feature = "postgres")),
164 allow(unused_variables)
165)]
166fn lock_error(db: &dyn sqlx::error::DatabaseError) -> Option<Error> {
167 #[cfg(feature = "mysql")]
168 if let Some(mysql) = db.try_downcast_ref::<MySqlDatabaseError>() {
169 match mysql.number() {
170 ER_LOCK_DEADLOCK => return Some(Error::Deadlock),
171 ER_LOCK_WAIT_TIMEOUT => return Some(Error::LockWaitTimeout),
172 _ => {}
173 }
174 }
175 #[cfg(feature = "postgres")]
177 match db.code().as_deref() {
178 Some(PG_DEADLOCK_DETECTED) => return Some(Error::Deadlock),
179 Some(PG_LOCK_NOT_AVAILABLE) => return Some(Error::LockWaitTimeout),
180 _ => {}
181 }
182 None
183}
184
185#[cfg(feature = "mysql")]
188fn duplicate_key(message: &str) -> Option<String> {
189 const MARK: &str = "for key '";
190 let start = message.rfind(MARK)? + MARK.len();
191 let key = message[start..].strip_suffix('\'')?;
192 Some(key.rsplit('.').next().unwrap_or(key).to_owned())
193}
194
195#[cfg(feature = "mysql")]
197fn foreign_key(message: &str) -> Option<String> {
198 const MARK: &str = "CONSTRAINT `";
199 let start = message.find(MARK)? + MARK.len();
200 let rest = &message[start..];
201 Some(rest[..rest.find('`')?].to_owned())
202}
203
204#[cfg(all(test, feature = "mysql"))]
205mod tests {
206 use super::{duplicate_key, foreign_key};
207
208 #[test]
209 fn duplicate_key_reads_the_index_name() {
210 assert_eq!(
211 duplicate_key("Duplicate entry 'a@example.com' for key 'users_email_unique'"),
212 Some("users_email_unique".to_owned())
213 );
214 }
215
216 #[test]
217 fn duplicate_key_drops_the_table_prefix_of_mysql_8() {
218 assert_eq!(
219 duplicate_key("Duplicate entry '1' for key 'users.PRIMARY'"),
220 Some("PRIMARY".to_owned())
221 );
222 }
223
224 #[test]
225 fn duplicate_key_ignores_a_value_that_looks_like_the_marker() {
226 assert_eq!(
227 duplicate_key("Duplicate entry 'x' for key 'y' for key 'real_index'"),
228 Some("real_index".to_owned())
229 );
230 }
231
232 #[test]
233 fn duplicate_key_is_none_without_a_key() {
234 assert_eq!(
235 duplicate_key("Can't write, because of unique constraint 'x'"),
236 None
237 );
238 }
239
240 #[test]
241 fn foreign_key_reads_the_constraint_name() {
242 assert_eq!(
243 foreign_key(
244 "Cannot add or update a child row: a foreign key constraint fails (`db`.`child`, \
245 CONSTRAINT `fk_child_parent` FOREIGN KEY (`parent_id`) REFERENCES `parent` (`id`))"
246 ),
247 Some("fk_child_parent".to_owned())
248 );
249 assert_eq!(foreign_key("something else"), None);
250 }
251}