use std::path::PathBuf;
use sqlx::error::ErrorKind;
#[cfg(feature = "mysql")]
use sqlx::mysql::MySqlDatabaseError;
#[cfg(feature = "mysql")]
const ER_LOCK_WAIT_TIMEOUT: u16 = 1205;
#[cfg(feature = "mysql")]
const ER_LOCK_DEADLOCK: u16 = 1213;
#[cfg(feature = "postgres")]
const PG_DEADLOCK_DETECTED: &str = "40P01";
#[cfg(feature = "postgres")]
const PG_LOCK_NOT_AVAILABLE: &str = "55P03";
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Database(sqlx::Error),
#[error("unique index `{}` was violated", .index.as_deref().unwrap_or("unknown"))]
UniqueViolation { index: Option<String> },
#[error("foreign key `{}` was violated", .constraint.as_deref().unwrap_or("unknown"))]
ForeignKeyViolation { constraint: Option<String> },
#[error("deadlock found; the transaction was rolled back")]
Deadlock,
#[error("lock wait timeout; the statement was rolled back")]
LockWaitTimeout,
#[error("could not read {}: {source}", path.display())]
Read {
path: PathBuf,
source: std::io::Error,
},
#[error("migration `{name}` is defined more than once")]
DuplicateMigration { name: String },
#[error("migration `{name}` must start with a 14-digit timestamp, such as 20261108000000_name")]
InvalidMigrationName { name: String },
#[error("migrations `{first}` and `{second}` have the same timestamp")]
DuplicateTimestamp { first: String, second: String },
#[error("another process is running migrations; gave up after {seconds} seconds")]
MigrationsLocked { seconds: u64 },
#[error("migration `{name}` failed, so the run stopped: {source}")]
MigrationFailed { name: String, source: sqlx::Error },
#[error("migration `{name}` failed to roll back: {source}")]
RollbackFailed { name: String, source: sqlx::Error },
#[error(
"migration version `{name}` was recorded as applied, but no folder with that version \
exists in this migrator, so it cannot be rolled back"
)]
UnknownMigration { name: String },
#[error(
"invalid identifier `{0}`: names must not be empty or contain a backtick or a double quote"
)]
InvalidIdentifier(String),
#[error(
"database URL scheme `{scheme}` is not supported by this build; enable its Cargo feature"
)]
UnsupportedDriver { scheme: String },
#[error("{operation} is not supported on {driver}")]
Unsupported {
operation: &'static str,
driver: &'static str,
},
#[error("column `{column}` is not fillable on table `{table}`")]
NotFillable { table: &'static str, column: String },
#[error("no query results for table `{table}`")]
NotFound { table: &'static str },
#[error("blocking call on an async thread; run it inside tokio::task::spawn_blocking")]
BlockingInRuntime,
}
impl From<sqlx::Error> for Error {
fn from(error: sqlx::Error) -> Self {
if let sqlx::Error::Database(db) = &error {
match db.kind() {
ErrorKind::UniqueViolation => {
return Self::UniqueViolation {
index: violated_name(db.as_ref(), true),
};
}
ErrorKind::ForeignKeyViolation => {
return Self::ForeignKeyViolation {
constraint: violated_name(db.as_ref(), false),
};
}
_ => {}
}
if let Some(kind) = lock_error(db.as_ref()) {
return kind;
}
}
Self::Database(error)
}
}
#[cfg_attr(not(feature = "mysql"), allow(unused_variables))]
fn violated_name(db: &dyn sqlx::error::DatabaseError, unique: bool) -> Option<String> {
#[cfg(feature = "mysql")]
if db.try_downcast_ref::<MySqlDatabaseError>().is_some() {
return if unique {
duplicate_key(db.message())
} else {
foreign_key(db.message())
};
}
db.constraint().map(str::to_owned)
}
#[cfg_attr(
not(any(feature = "mysql", feature = "postgres")),
allow(unused_variables)
)]
fn lock_error(db: &dyn sqlx::error::DatabaseError) -> Option<Error> {
#[cfg(feature = "mysql")]
if let Some(mysql) = db.try_downcast_ref::<MySqlDatabaseError>() {
match mysql.number() {
ER_LOCK_DEADLOCK => return Some(Error::Deadlock),
ER_LOCK_WAIT_TIMEOUT => return Some(Error::LockWaitTimeout),
_ => {}
}
}
#[cfg(feature = "postgres")]
match db.code().as_deref() {
Some(PG_DEADLOCK_DETECTED) => return Some(Error::Deadlock),
Some(PG_LOCK_NOT_AVAILABLE) => return Some(Error::LockWaitTimeout),
_ => {}
}
None
}
#[cfg(feature = "mysql")]
fn duplicate_key(message: &str) -> Option<String> {
const MARK: &str = "for key '";
let start = message.rfind(MARK)? + MARK.len();
let key = message[start..].strip_suffix('\'')?;
Some(key.rsplit('.').next().unwrap_or(key).to_owned())
}
#[cfg(feature = "mysql")]
fn foreign_key(message: &str) -> Option<String> {
const MARK: &str = "CONSTRAINT `";
let start = message.find(MARK)? + MARK.len();
let rest = &message[start..];
Some(rest[..rest.find('`')?].to_owned())
}
#[cfg(all(test, feature = "mysql"))]
mod tests {
use super::{duplicate_key, foreign_key};
#[test]
fn duplicate_key_reads_the_index_name() {
assert_eq!(
duplicate_key("Duplicate entry 'a@example.com' for key 'users_email_unique'"),
Some("users_email_unique".to_owned())
);
}
#[test]
fn duplicate_key_drops_the_table_prefix_of_mysql_8() {
assert_eq!(
duplicate_key("Duplicate entry '1' for key 'users.PRIMARY'"),
Some("PRIMARY".to_owned())
);
}
#[test]
fn duplicate_key_ignores_a_value_that_looks_like_the_marker() {
assert_eq!(
duplicate_key("Duplicate entry 'x' for key 'y' for key 'real_index'"),
Some("real_index".to_owned())
);
}
#[test]
fn duplicate_key_is_none_without_a_key() {
assert_eq!(
duplicate_key("Can't write, because of unique constraint 'x'"),
None
);
}
#[test]
fn foreign_key_reads_the_constraint_name() {
assert_eq!(
foreign_key(
"Cannot add or update a child row: a foreign key constraint fails (`db`.`child`, \
CONSTRAINT `fk_child_parent` FOREIGN KEY (`parent_id`) REFERENCES `parent` (`id`))"
),
Some("fk_child_parent".to_owned())
);
assert_eq!(foreign_key("something else"), None);
}
}