diff --git a/src/error.rs b/src/error.rs index e470bb6a..125da4f8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -139,3 +139,93 @@ where { DbErr::Json(s.to_string()) } + +/// An error from unsuccessful SQL query +#[derive(Error, Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] +pub enum SqlErr { + /// Error for duplicate record in unique field or primary key field + #[error("Unique Constraint Violated: {0}")] + UniqueConstraintViolation(String), + /// Error for Foreign key constraint + #[error("Foreign Key Constraint Violated: {0}")] + ForeignKeyConstraintViolation(String), +} + +#[allow(dead_code)] +impl DbErr { + /// Convert generic DbErr by sqlx to SqlErr, return none if the error is not any type of SqlErr + pub fn sql_err(&self) -> Option { + #[cfg(any( + feature = "sqlx-mysql", + feature = "sqlx-postgres", + feature = "sqlx-sqlite" + ))] + { + use std::ops::Deref; + if let DbErr::Exec(RuntimeErr::SqlxError(sqlx::Error::Database(e))) + | DbErr::Query(RuntimeErr::SqlxError(sqlx::Error::Database(e))) = self + { + let error_code = e.code().unwrap_or_default(); + let _error_code_expanded = error_code.deref(); + #[cfg(feature = "sqlx-mysql")] + if e.try_downcast_ref::() + .is_some() + { + let error_number = e + .try_downcast_ref::()? + .number(); + match error_number { + // 1022 Can't write; duplicate key in table '%s' + // 1062 Duplicate entry '%s' for key %d + // 1169 Can't write, because of unique constraint, to table '%s' + // 1586 Duplicate entry '%s' for key '%s' + 1022 | 1062 | 1169 | 1586 => { + return Some(SqlErr::UniqueConstraintViolation(e.message().into())) + } + // 1216 Cannot add or update a child row: a foreign key constraint fails + // 1217 Cannot delete or update a parent row: a foreign key constraint fails + // 1451 Cannot delete or update a parent row: a foreign key constraint fails (%s) + // 1452 Cannot add or update a child row: a foreign key constraint fails (%s) + // 1557 Upholding foreign key constraints for table '%s', entry '%s', key %d would lead to a duplicate entry + // 1761 Foreign key constraint for table '%s', record '%s' would lead to a duplicate entry in table '%s', key '%s' + // 1762 Foreign key constraint for table '%s', record '%s' would lead to a duplicate entry in a child table + 1216 | 1217 | 1451 | 1452 | 1557 | 1761 | 1762 => { + return Some(SqlErr::ForeignKeyConstraintViolation(e.message().into())) + } + _ => return None, + } + } + #[cfg(feature = "sqlx-postgres")] + if e.try_downcast_ref::() + .is_some() + { + match _error_code_expanded { + "23505" => { + return Some(SqlErr::UniqueConstraintViolation(e.message().into())) + } + "23503" => { + return Some(SqlErr::ForeignKeyConstraintViolation(e.message().into())) + } + _ => return None, + } + } + #[cfg(feature = "sqlx-sqlite")] + if e.try_downcast_ref::().is_some() { + match _error_code_expanded { + // error code 1555 refers to the primary key's unique constraint violation + // error code 2067 refers to the UNIQUE unique constraint violation + "1555" | "2067" => { + return Some(SqlErr::UniqueConstraintViolation(e.message().into())) + } + "787" => { + return Some(SqlErr::ForeignKeyConstraintViolation(e.message().into())) + } + _ => return None, + } + } + } + } + None + } +} diff --git a/tests/sql_err_tests.rs b/tests/sql_err_tests.rs new file mode 100644 index 00000000..e73b3afe --- /dev/null +++ b/tests/sql_err_tests.rs @@ -0,0 +1,67 @@ +pub mod common; +pub use common::{bakery_chain::*, setup::*, TestContext}; +use rust_decimal_macros::dec; +pub use sea_orm::{ + entity::*, error::DbErr, error::SqlErr, tests_cfg, DatabaseConnection, DbBackend, EntityName, + ExecResult, +}; +use uuid::Uuid; + +#[sea_orm_macros::test] +#[cfg(any( + feature = "sqlx-mysql", + feature = "sqlx-sqlite", + feature = "sqlx-postgres" +))] +async fn main() { + let ctx = TestContext::new("bakery_chain_sql_err_tests").await; + create_tables(&ctx.db).await.unwrap(); + test_error(&ctx.db).await; + ctx.delete().await; +} + +pub async fn test_error(db: &DatabaseConnection) { + let mud_cake = cake::ActiveModel { + name: Set("Moldy Cake".to_owned()), + price: Set(dec!(10.25)), + gluten_free: Set(false), + serial: Set(Uuid::new_v4()), + bakery_id: Set(None), + ..Default::default() + }; + + let cake = mud_cake.save(db).await.expect("could not insert cake"); + + // if compiling without sqlx, this assignment will complain, + // but the whole test is useless in that case anyway. + #[allow(unused_variables)] + let error: DbErr = cake + .into_active_model() + .insert(db) + .await + .expect_err("inserting should fail due to duplicate primary key"); + + assert!(matches!( + error.sql_err(), + Some(SqlErr::UniqueConstraintViolation(_)) + )); + + let fk_cake = cake::ActiveModel { + name: Set("fk error Cake".to_owned()), + price: Set(dec!(10.25)), + gluten_free: Set(false), + serial: Set(Uuid::new_v4()), + bakery_id: Set(Some(1000)), + ..Default::default() + }; + + let fk_error = fk_cake + .insert(db) + .await + .expect_err("create foreign key should fail with non-primary key"); + + assert!(matches!( + fk_error.sql_err(), + Some(SqlErr::ForeignKeyConstraintViolation(_)) + )); +}