diff --git a/Cargo.toml b/Cargo.toml index e7215b0f..bb9013b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ uuid = { version = "^1", features = ["serde", "v4"], optional = true } ouroboros = "0.15" url = "^2.2" once_cell = "1.8" +thiserror = "^1" [dev-dependencies] smol = { version = "^1.2" } diff --git a/issues/324/src/model.rs b/issues/324/src/model.rs index 9ec35a07..8b335c25 100644 --- a/issues/324/src/model.rs +++ b/issues/324/src/model.rs @@ -26,10 +26,7 @@ macro_rules! impl_try_from_u64_err { ($newtype: ident) => { impl sea_orm::TryFromU64 for $newtype { fn try_from_u64(_n: u64) -> Result { - Err(sea_orm::DbErr::Exec(format!( - "{} cannot be converted from u64", - stringify!($newtype) - ))) + Err(sea_orm::DbErr::ConvertFromU64(stringify!($newtype))) } } }; diff --git a/issues/400/src/model.rs b/issues/400/src/model.rs index a53eeb74..28a44d64 100644 --- a/issues/400/src/model.rs +++ b/issues/400/src/model.rs @@ -1,5 +1,5 @@ -use std::marker::PhantomData; use sea_orm::entity::prelude::*; +use std::marker::PhantomData; #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[sea_orm(table_name = "model")] @@ -31,10 +31,7 @@ impl From> for Uuid { impl sea_orm::TryFromU64 for AccountId { fn try_from_u64(_n: u64) -> Result { - Err(sea_orm::DbErr::Exec(format!( - "{} cannot be converted from u64", - stringify!(AccountId) - ))) + Err(sea_orm::DbErr::ConvertFromU64(stringify!(AccountId))) } } diff --git a/sea-orm-macros/src/derives/column.rs b/sea-orm-macros/src/derives/column.rs index 42e1dfe1..3a0f9314 100644 --- a/sea-orm-macros/src/derives/column.rs +++ b/sea-orm-macros/src/derives/column.rs @@ -71,7 +71,7 @@ pub fn impl_default_as_str(ident: &Ident, data: &Data) -> syn::Result syn::Result { let data_enum = match data { Data::Enum(data_enum) => data_enum, @@ -99,7 +99,7 @@ pub fn impl_col_from_str(ident: &Ident, data: &Data) -> syn::Result fn from_str(s: &str) -> std::result::Result { match s { #(#columns),*, - _ => Err(sea_orm::ColumnFromStrErr(format!("Failed to parse '{}' as `{}`", s, stringify!(#ident)))), + _ => Err(sea_orm::ColumnFromStrErr(s.to_owned())), } } } diff --git a/src/database/db_connection.rs b/src/database/db_connection.rs index ac4fd7e6..3fa48ec0 100644 --- a/src/database/db_connection.rs +++ b/src/database/db_connection.rs @@ -116,7 +116,9 @@ impl ConnectionTrait for DatabaseConnection { DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.execute(stmt).await, #[cfg(feature = "mock")] DatabaseConnection::MockDatabaseConnection(conn) => conn.execute(stmt), - DatabaseConnection::Disconnected => Err(DbErr::Conn("Disconnected".to_owned())), + DatabaseConnection::Disconnected => { + Err(DbErr::Conn(RuntimeErr::Internal("Disconnected".to_owned()))) + } } } @@ -132,7 +134,9 @@ impl ConnectionTrait for DatabaseConnection { DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.query_one(stmt).await, #[cfg(feature = "mock")] DatabaseConnection::MockDatabaseConnection(conn) => conn.query_one(stmt), - DatabaseConnection::Disconnected => Err(DbErr::Conn("Disconnected".to_owned())), + DatabaseConnection::Disconnected => { + Err(DbErr::Conn(RuntimeErr::Internal("Disconnected".to_owned()))) + } } } @@ -148,7 +152,9 @@ impl ConnectionTrait for DatabaseConnection { DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.query_all(stmt).await, #[cfg(feature = "mock")] DatabaseConnection::MockDatabaseConnection(conn) => conn.query_all(stmt), - DatabaseConnection::Disconnected => Err(DbErr::Conn("Disconnected".to_owned())), + DatabaseConnection::Disconnected => { + Err(DbErr::Conn(RuntimeErr::Internal("Disconnected".to_owned()))) + } } } diff --git a/src/database/mock.rs b/src/database/mock.rs index 7af7cdb5..1677938f 100644 --- a/src/database/mock.rs +++ b/src/database/mock.rs @@ -102,7 +102,9 @@ impl MockDatabaseTrait for MockDatabase { result: ExecResultHolder::Mock(std::mem::take(&mut self.exec_results[counter])), }) } else { - Err(DbErr::Exec("`exec_results` buffer is empty.".to_owned())) + Err(DbErr::Exec(RuntimeErr::Internal( + "`exec_results` buffer is empty.".to_owned(), + ))) } } @@ -121,7 +123,9 @@ impl MockDatabaseTrait for MockDatabase { }) .collect()) } else { - Err(DbErr::Query("`query_results` buffer is empty.".to_owned())) + Err(DbErr::Query(RuntimeErr::Internal( + "`query_results` buffer is empty.".to_owned(), + ))) } } @@ -176,7 +180,7 @@ impl MockRow { where T: ValueType, { - T::try_from(self.values.get(col).unwrap().clone()).map_err(|e| DbErr::Query(e.to_string())) + T::try_from(self.values.get(col).unwrap().clone()).map_err(|e| DbErr::Type(e.to_string())) } /// An iterator over the keys and values of a mock row diff --git a/src/database/mod.rs b/src/database/mod.rs index dda7dd02..1a1a399e 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -18,7 +18,7 @@ pub use stream::*; use tracing::instrument; pub use transaction::*; -use crate::DbErr; +use crate::{DbErr, RuntimeErr}; /// Defines a database #[derive(Debug, Default)] @@ -75,10 +75,10 @@ impl Database { if crate::MockDatabaseConnector::accepts(&opt.url) { return crate::MockDatabaseConnector::connect(&opt.url).await; } - Err(DbErr::Conn(format!( + Err(DbErr::Conn(RuntimeErr::Internal(format!( "The connection string '{}' has no supporting driver.", opt.url - ))) + )))) } } diff --git a/src/database/transaction.rs b/src/database/transaction.rs index cb1ad122..66ae25e5 100644 --- a/src/database/transaction.rs +++ b/src/database/transaction.rs @@ -238,6 +238,17 @@ impl DatabaseTransaction { } } } + + #[cfg(feature = "sqlx-dep")] + fn map_err_ignore_not_found( + err: Result, sqlx::Error>, + ) -> Result, DbErr> { + if let Err(sqlx::Error::RowNotFound) = err { + Ok(None) + } else { + err.map_err(|e| sqlx_error_to_query_err(e)) + } + } } impl Drop for DatabaseTransaction { @@ -258,13 +269,14 @@ impl ConnectionTrait for DatabaseTransaction { async fn execute(&self, stmt: Statement) -> Result { debug_print!("{}", stmt); - let _res = match &mut *self.conn.lock().await { + match &mut *self.conn.lock().await { #[cfg(feature = "sqlx-mysql")] InnerConnection::MySql(conn) => { let query = crate::driver::sqlx_mysql::sqlx_query(&stmt); crate::metric::metric!(self.metric_callback, &stmt, { query.execute(conn).await.map(Into::into) }) + .map_err(sqlx_error_to_exec_err) } #[cfg(feature = "sqlx-postgres")] InnerConnection::Postgres(conn) => { @@ -272,6 +284,7 @@ impl ConnectionTrait for DatabaseTransaction { crate::metric::metric!(self.metric_callback, &stmt, { query.execute(conn).await.map(Into::into) }) + .map_err(sqlx_error_to_exec_err) } #[cfg(feature = "sqlx-sqlite")] InnerConnection::Sqlite(conn) => { @@ -279,14 +292,13 @@ impl ConnectionTrait for DatabaseTransaction { crate::metric::metric!(self.metric_callback, &stmt, { query.execute(conn).await.map(Into::into) }) + .map_err(sqlx_error_to_exec_err) } #[cfg(feature = "mock")] InnerConnection::Mock(conn) => return conn.execute(stmt), #[allow(unreachable_patterns)] _ => unreachable!(), - }; - #[cfg(feature = "sqlx-dep")] - _res.map_err(sqlx_error_to_exec_err) + } } #[instrument(level = "trace")] @@ -294,32 +306,32 @@ impl ConnectionTrait for DatabaseTransaction { async fn query_one(&self, stmt: Statement) -> Result, DbErr> { debug_print!("{}", stmt); - let _res = match &mut *self.conn.lock().await { + match &mut *self.conn.lock().await { #[cfg(feature = "sqlx-mysql")] InnerConnection::MySql(conn) => { let query = crate::driver::sqlx_mysql::sqlx_query(&stmt); - query.fetch_one(conn).await.map(|row| Some(row.into())) + Self::map_err_ignore_not_found( + query.fetch_one(conn).await.map(|row| Some(row.into())), + ) } #[cfg(feature = "sqlx-postgres")] InnerConnection::Postgres(conn) => { let query = crate::driver::sqlx_postgres::sqlx_query(&stmt); - query.fetch_one(conn).await.map(|row| Some(row.into())) + Self::map_err_ignore_not_found( + query.fetch_one(conn).await.map(|row| Some(row.into())), + ) } #[cfg(feature = "sqlx-sqlite")] InnerConnection::Sqlite(conn) => { let query = crate::driver::sqlx_sqlite::sqlx_query(&stmt); - query.fetch_one(conn).await.map(|row| Some(row.into())) + Self::map_err_ignore_not_found( + query.fetch_one(conn).await.map(|row| Some(row.into())), + ) } #[cfg(feature = "mock")] InnerConnection::Mock(conn) => return conn.query_one(stmt), #[allow(unreachable_patterns)] _ => unreachable!(), - }; - #[cfg(feature = "sqlx-dep")] - if let Err(sqlx::Error::RowNotFound) = _res { - Ok(None) - } else { - _res.map_err(sqlx_error_to_query_err) } } @@ -328,7 +340,7 @@ impl ConnectionTrait for DatabaseTransaction { async fn query_all(&self, stmt: Statement) -> Result, DbErr> { debug_print!("{}", stmt); - let _res = match &mut *self.conn.lock().await { + match &mut *self.conn.lock().await { #[cfg(feature = "sqlx-mysql")] InnerConnection::MySql(conn) => { let query = crate::driver::sqlx_mysql::sqlx_query(&stmt); @@ -336,6 +348,7 @@ impl ConnectionTrait for DatabaseTransaction { .fetch_all(conn) .await .map(|rows| rows.into_iter().map(|r| r.into()).collect()) + .map_err(sqlx_error_to_query_err) } #[cfg(feature = "sqlx-postgres")] InnerConnection::Postgres(conn) => { @@ -344,6 +357,7 @@ impl ConnectionTrait for DatabaseTransaction { .fetch_all(conn) .await .map(|rows| rows.into_iter().map(|r| r.into()).collect()) + .map_err(sqlx_error_to_query_err) } #[cfg(feature = "sqlx-sqlite")] InnerConnection::Sqlite(conn) => { @@ -352,14 +366,13 @@ impl ConnectionTrait for DatabaseTransaction { .fetch_all(conn) .await .map(|rows| rows.into_iter().map(|r| r.into()).collect()) + .map_err(sqlx_error_to_query_err) } #[cfg(feature = "mock")] InnerConnection::Mock(conn) => return conn.query_all(stmt), #[allow(unreachable_patterns)] _ => unreachable!(), - }; - #[cfg(feature = "sqlx-dep")] - _res.map_err(sqlx_error_to_query_err) + } } } diff --git a/src/driver/sqlx_common.rs b/src/driver/sqlx_common.rs index 18ca059d..37a250ed 100644 --- a/src/driver/sqlx_common.rs +++ b/src/driver/sqlx_common.rs @@ -1,16 +1,16 @@ -use crate::DbErr; +use crate::{DbErr, RuntimeErr}; /// Converts an [sqlx::error] execution error to a [DbErr] pub fn sqlx_error_to_exec_err(err: sqlx::Error) -> DbErr { - DbErr::Exec(err.to_string()) + DbErr::Exec(RuntimeErr::SqlxError(err)) } /// Converts an [sqlx::error] query error to a [DbErr] pub fn sqlx_error_to_query_err(err: sqlx::Error) -> DbErr { - DbErr::Query(err.to_string()) + DbErr::Query(RuntimeErr::SqlxError(err)) } /// Converts an [sqlx::error] connection error to a [DbErr] pub fn sqlx_error_to_conn_err(err: sqlx::Error) -> DbErr { - DbErr::Conn(err.to_string()) + DbErr::Conn(RuntimeErr::SqlxError(err)) } diff --git a/src/driver/sqlx_mysql.rs b/src/driver/sqlx_mysql.rs index 01b840dd..e3d0ddbf 100644 --- a/src/driver/sqlx_mysql.rs +++ b/src/driver/sqlx_mysql.rs @@ -45,7 +45,7 @@ impl SqlxMySqlConnector { let mut opt = options .url .parse::() - .map_err(|e| DbErr::Conn(e.to_string()))?; + .map_err(sqlx_error_to_conn_err)?; use sqlx::ConnectOptions; if !options.sqlx_logging { opt.disable_statement_logging(); @@ -89,9 +89,7 @@ impl SqlxMySqlPoolConnection { } }) } else { - Err(DbErr::Exec( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -107,14 +105,12 @@ impl SqlxMySqlPoolConnection { Ok(row) => Ok(Some(row.into())), Err(err) => match err { sqlx::Error::RowNotFound => Ok(None), - _ => Err(DbErr::Query(err.to_string())), + _ => Err(sqlx_error_to_query_err(err)), }, } }) } else { - Err(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -132,9 +128,7 @@ impl SqlxMySqlPoolConnection { } }) } else { - Err(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -150,9 +144,7 @@ impl SqlxMySqlPoolConnection { self.metric_callback.clone(), ))) } else { - Err(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -162,9 +154,7 @@ impl SqlxMySqlPoolConnection { if let Ok(conn) = self.pool.acquire().await { DatabaseTransaction::new_mysql(conn, self.metric_callback.clone()).await } else { - Err(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -185,9 +175,7 @@ impl SqlxMySqlPoolConnection { .map_err(|e| TransactionError::Connection(e))?; transaction.run(callback).await } else { - Err(TransactionError::Connection(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - ))) + Err(TransactionError::Connection(DbErr::ConnectionAcquire)) } } diff --git a/src/driver/sqlx_postgres.rs b/src/driver/sqlx_postgres.rs index bd561148..085abaa8 100644 --- a/src/driver/sqlx_postgres.rs +++ b/src/driver/sqlx_postgres.rs @@ -45,7 +45,7 @@ impl SqlxPostgresConnector { let mut opt = options .url .parse::() - .map_err(|e| DbErr::Conn(e.to_string()))?; + .map_err(sqlx_error_to_conn_err)?; use sqlx::ConnectOptions; if !options.sqlx_logging { opt.disable_statement_logging(); @@ -89,9 +89,7 @@ impl SqlxPostgresPoolConnection { } }) } else { - Err(DbErr::Exec( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -107,14 +105,12 @@ impl SqlxPostgresPoolConnection { Ok(row) => Ok(Some(row.into())), Err(err) => match err { sqlx::Error::RowNotFound => Ok(None), - _ => Err(DbErr::Query(err.to_string())), + _ => Err(sqlx_error_to_query_err(err)), }, } }) } else { - Err(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -132,9 +128,7 @@ impl SqlxPostgresPoolConnection { } }) } else { - Err(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -150,9 +144,7 @@ impl SqlxPostgresPoolConnection { self.metric_callback.clone(), ))) } else { - Err(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -162,9 +154,7 @@ impl SqlxPostgresPoolConnection { if let Ok(conn) = self.pool.acquire().await { DatabaseTransaction::new_postgres(conn, self.metric_callback.clone()).await } else { - Err(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -185,9 +175,7 @@ impl SqlxPostgresPoolConnection { .map_err(|e| TransactionError::Connection(e))?; transaction.run(callback).await } else { - Err(TransactionError::Connection(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - ))) + Err(TransactionError::Connection(DbErr::ConnectionAcquire)) } } diff --git a/src/driver/sqlx_sqlite.rs b/src/driver/sqlx_sqlite.rs index 8bf56214..36e9dfa4 100644 --- a/src/driver/sqlx_sqlite.rs +++ b/src/driver/sqlx_sqlite.rs @@ -46,7 +46,7 @@ impl SqlxSqliteConnector { let mut opt = options .url .parse::() - .map_err(|e| DbErr::Conn(e.to_string()))?; + .map_err(sqlx_error_to_conn_err)?; if options.sqlcipher_key.is_some() { opt = opt.pragma("key", options.sqlcipher_key.clone().unwrap()); } @@ -96,9 +96,7 @@ impl SqlxSqlitePoolConnection { } }) } else { - Err(DbErr::Exec( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -114,14 +112,12 @@ impl SqlxSqlitePoolConnection { Ok(row) => Ok(Some(row.into())), Err(err) => match err { sqlx::Error::RowNotFound => Ok(None), - _ => Err(DbErr::Query(err.to_string())), + _ => Err(sqlx_error_to_query_err(err)), }, } }) } else { - Err(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -139,9 +135,7 @@ impl SqlxSqlitePoolConnection { } }) } else { - Err(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -157,9 +151,7 @@ impl SqlxSqlitePoolConnection { self.metric_callback.clone(), ))) } else { - Err(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -169,9 +161,7 @@ impl SqlxSqlitePoolConnection { if let Ok(conn) = self.pool.acquire().await { DatabaseTransaction::new_sqlite(conn, self.metric_callback.clone()).await } else { - Err(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - )) + Err(DbErr::ConnectionAcquire) } } @@ -192,9 +182,7 @@ impl SqlxSqlitePoolConnection { .map_err(|e| TransactionError::Connection(e))?; transaction.run(callback).await } else { - Err(TransactionError::Connection(DbErr::Query( - "Failed to acquire connection from pool.".to_owned(), - ))) + Err(TransactionError::Connection(DbErr::ConnectionAcquire)) } } diff --git a/src/error.rs b/src/error.rs index aecc3f52..01c4f21f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,49 +1,80 @@ +#[cfg(feature = "sqlx-dep")] +use sqlx::error::Error as SqlxError; +use thiserror::Error; + /// An error from unsuccessful database operations -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Error, Debug)] pub enum DbErr { + /// This error can happen when the connection pool is fully-utilized + #[error("Failed to acquire connection from pool")] + ConnectionAcquire, + /// Runtime type conversion error + #[error("Error converting `{from}` into `{into}`: {source}")] + TryIntoErr { + /// From type + from: &'static str, + /// Into type + into: &'static str, + /// TryError + source: Box, + }, /// There was a problem with the database connection - Conn(String), + #[error("Connection Error: {0}")] + Conn(#[source] RuntimeErr), /// An operation did not execute successfully - Exec(String), + #[error("Execution Error: {0}")] + Exec(#[source] RuntimeErr), /// An error occurred while performing a query - Query(String), + #[error("Query Error: {0}")] + Query(#[source] RuntimeErr), + /// Type error: the specified type cannot be converted from u64. This is not a runtime error. + #[error("Type '{0}' cannot be converted from u64")] + ConvertFromU64(&'static str), + /// After an insert statement it was impossible to retrieve the last_insert_id + #[error("Failed to unpack last_insert_id")] + UnpackInsertId, + /// When updating, a model should know it's primary key to check + /// if the record has been correctly updated, otherwise this error will occur + #[error("Failed to get primary key from model")] + UpdateGetPrimeryKey, /// The record was not found in the database + #[error("RecordNotFound Error: {0}")] RecordNotFound(String), /// A custom error + #[error("Custom Error: {0}")] Custom(String), /// Error occurred while parsing value as target type + #[error("Type Error: {0}")] Type(String), /// Error occurred while parsing json value as target type + #[error("Json Error: {0}")] Json(String), /// A migration error + #[error("Migration Error: {0}")] Migration(String), } -impl std::error::Error for DbErr {} +/// Runtime error +#[derive(Error, Debug)] +pub enum RuntimeErr { + /// SQLx Error + #[cfg(feature = "sqlx-dep")] + #[error("{0}")] + SqlxError(SqlxError), + /// Error generated from within SeaORM + #[error("{0}")] + Internal(String), +} -impl std::fmt::Display for DbErr { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::Conn(s) => write!(f, "Connection Error: {}", s), - Self::Exec(s) => write!(f, "Execution Error: {}", s), - Self::Query(s) => write!(f, "Query Error: {}", s), - Self::RecordNotFound(s) => write!(f, "RecordNotFound Error: {}", s), - Self::Custom(s) => write!(f, "Custom Error: {}", s), - Self::Type(s) => write!(f, "Type Error: {}", s), - Self::Json(s) => write!(f, "Json Error: {}", s), - Self::Migration(s) => write!(f, "Migration Error: {}", s), - } +impl PartialEq for DbErr { + fn eq(&self, other: &Self) -> bool { + self.to_string() == other.to_string() } } -/// An error from a failed column operation when trying to convert the column to a string -#[derive(Debug, Clone)] +impl Eq for DbErr {} + +/// Error during `impl FromStr for Entity::Column` +#[derive(Error, Debug)] +#[error("Failed to match \"{0}\" as Column")] pub struct ColumnFromStrErr(pub String); - -impl std::error::Error for ColumnFromStrErr {} - -impl std::fmt::Display for ColumnFromStrErr { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.0.as_str()) - } -} diff --git a/src/executor/insert.rs b/src/executor/insert.rs index 3c7b69c8..51b82623 100644 --- a/src/executor/insert.rs +++ b/src/executor/insert.rs @@ -128,7 +128,7 @@ where Some(value_tuple) => FromValueTuple::from_value_tuple(value_tuple), None => match last_insert_id_opt { Some(last_insert_id) => last_insert_id, - None => return Err(DbErr::Exec("Fail to unpack last_insert_id".to_owned())), + None => return Err(DbErr::UnpackInsertId), }, }; Ok(InsertResult { last_insert_id }) @@ -168,6 +168,8 @@ where }; match found { Some(model) => Ok(model), - None => Err(DbErr::Exec("Failed to find inserted item".to_owned())), + None => Err(DbErr::RecordNotFound( + "Failed to find inserted item".to_owned(), + )), } } diff --git a/src/executor/query.rs b/src/executor/query.rs index 06361a45..3b53542b 100644 --- a/src/executor/query.rs +++ b/src/executor/query.rs @@ -41,7 +41,7 @@ impl From for DbErr { match e { TryGetError::DbErr(e) => e, TryGetError::Null(s) => { - DbErr::Query(format!("error occurred while decoding {}: Null", s)) + DbErr::Type(format!("A null value was encountered while decoding {}", s)) } } } @@ -376,12 +376,13 @@ impl TryGetable for Decimal { let val: Option = row .try_get(column.as_str()) .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e)))?; - use rust_decimal::prelude::FromPrimitive; match val { - Some(v) => Decimal::from_f64(v).ok_or_else(|| { - TryGetError::DbErr(DbErr::Query( - "Failed to convert f64 into Decimal".to_owned(), - )) + Some(v) => Decimal::try_from(v).map_err(|e| { + TryGetError::DbErr(DbErr::TryIntoErr { + from: "f64", + into: "Decimal", + source: Box::new(e), + }) }), None => Err(TryGetError::Null(column)), } @@ -626,7 +627,7 @@ where fn try_get_many_with_slice_len_of(len: usize, cols: &[String]) -> Result<(), TryGetError> { if cols.len() < len { - Err(TryGetError::DbErr(DbErr::Query(format!( + Err(TryGetError::DbErr(DbErr::Type(format!( "Expect {} column names supplied but got slice of length {}", len, cols.len() @@ -708,10 +709,7 @@ macro_rules! try_from_u64_err { ( $type: ty ) => { impl TryFromU64 for $type { fn try_from_u64(_: u64) -> Result { - Err(DbErr::Exec(format!( - "{} cannot be converted from u64", - stringify!($type) - ))) + Err(DbErr::ConvertFromU64(stringify!($type))) } } }; @@ -722,10 +720,7 @@ macro_rules! try_from_u64_err { $( $gen_type: TryFromU64, )* { fn try_from_u64(_: u64) -> Result { - Err(DbErr::Exec(format!( - "{} cannot be converted from u64", - stringify!(($($gen_type,)*)) - ))) + Err(DbErr::ConvertFromU64(stringify!($($gen_type,)*))) } } }; @@ -743,12 +738,10 @@ macro_rules! try_from_u64_numeric { impl TryFromU64 for $type { fn try_from_u64(n: u64) -> Result { use std::convert::TryInto; - n.try_into().map_err(|_| { - DbErr::Exec(format!( - "fail to convert '{}' into '{}'", - n, - stringify!($type) - )) + n.try_into().map_err(|e| DbErr::TryIntoErr { + from: stringify!(u64), + into: stringify!($type), + source: Box::new(e), }) } } @@ -823,18 +816,22 @@ try_from_u64_err!(uuid::Uuid); #[cfg(test)] mod tests { use super::TryGetError; - use crate::error::DbErr; + use crate::error::*; #[test] fn from_try_get_error() { // TryGetError::DbErr - let expected = DbErr::Query("expected error message".to_owned()); - let try_get_error = TryGetError::DbErr(expected.clone()); - assert_eq!(DbErr::from(try_get_error), expected); + let try_get_error = TryGetError::DbErr(DbErr::Query(RuntimeErr::Internal( + "expected error message".to_owned(), + ))); + assert_eq!( + DbErr::from(try_get_error), + DbErr::Query(RuntimeErr::Internal("expected error message".to_owned())) + ); // TryGetError::Null let try_get_error = TryGetError::Null("column".to_owned()); - let expected = "error occurred while decoding column: Null".to_owned(); - assert_eq!(DbErr::from(try_get_error), DbErr::Query(expected)); + let expected = "A null value was encountered while decoding column".to_owned(); + assert_eq!(DbErr::from(try_get_error), DbErr::Type(expected)); } } diff --git a/src/executor/update.rs b/src/executor/update.rs index 5531afa3..c06606dc 100644 --- a/src/executor/update.rs +++ b/src/executor/update.rs @@ -116,7 +116,7 @@ where Updater::new(query).check_record_exists().exec(db).await?; let primary_key_value = match model.get_primary_key_value() { Some(val) => FromValueTuple::from_value_tuple(val), - None => return Err(DbErr::Exec("Fail to get primary key from model".to_owned())), + None => return Err(DbErr::UpdateGetPrimeryKey), }; let found = ::find_by_id(primary_key_value) .one(db) @@ -124,7 +124,9 @@ where // If we cannot select the updated row from db by the cached primary key match found { Some(model) => Ok(model), - None => Err(DbErr::Exec("Failed to find inserted item".to_owned())), + None => Err(DbErr::RecordNotFound( + "Failed to find updated item".to_owned(), + )), } } } diff --git a/tests/crud/error.rs b/tests/crud/error.rs new file mode 100644 index 00000000..4828d9cd --- /dev/null +++ b/tests/crud/error.rs @@ -0,0 +1,56 @@ +pub use super::*; +use rust_decimal_macros::dec; +use sea_orm::error::*; +#[cfg(any( + feature = "sqlx-mysql", + feature = "sqlx-sqlite", + feature = "sqlx-postgres" +))] +use sqlx::Error; +use uuid::Uuid; + +pub async fn test_cake_error_sqlx(db: &DbConn) { + 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"); + + #[cfg(any(feature = "sqlx-mysql", feature = "sqlx-sqlite"))] + match error { + DbErr::Exec(RuntimeErr::SqlxError(error)) => match error { + Error::Database(e) => { + #[cfg(feature = "sqlx-mysql")] + assert_eq!(e.code().unwrap(), "23000"); + #[cfg(feature = "sqlx-sqlite")] + assert_eq!(e.code().unwrap(), "1555"); + } + _ => panic!("Unexpected sqlx-error kind"), + }, + _ => panic!("Unexpected Error kind"), + } + #[cfg(feature = "sqlx-postgres")] + match error { + DbErr::Query(RuntimeErr::SqlxError(error)) => match error { + Error::Database(e) => { + assert_eq!(e.code().unwrap(), "23505"); + } + _ => panic!("Unexpected sqlx-error kind"), + }, + _ => panic!("Unexpected Error kind"), + } +} diff --git a/tests/crud/mod.rs b/tests/crud/mod.rs index 916878c8..3a629d7a 100644 --- a/tests/crud/mod.rs +++ b/tests/crud/mod.rs @@ -3,6 +3,7 @@ pub mod create_cake; pub mod create_lineitem; pub mod create_order; pub mod deletes; +pub mod error; pub mod updates; pub use create_baker::*; @@ -10,6 +11,7 @@ pub use create_cake::*; pub use create_lineitem::*; pub use create_order::*; pub use deletes::*; +pub use error::*; pub use updates::*; pub use super::common::bakery_chain::*; diff --git a/tests/crud_tests.rs b/tests/crud_tests.rs index bc2a3c97..9f7ea404 100644 --- a/tests/crud_tests.rs +++ b/tests/crud_tests.rs @@ -35,5 +35,6 @@ pub async fn create_entities(db: &DatabaseConnection) { test_update_deleted_customer(db).await; test_delete_cake(db).await; + test_cake_error_sqlx(db).await; test_delete_bakery(db).await; } diff --git a/tests/transaction_tests.rs b/tests/transaction_tests.rs index 1059da8e..9faab72a 100644 --- a/tests/transaction_tests.rs +++ b/tests/transaction_tests.rs @@ -508,7 +508,9 @@ pub async fn transaction_nested() { assert_eq!(bakeries.len(), 4); if true { - Err(DbErr::Query("Force Rollback!".to_owned())) + Err(DbErr::Query(RuntimeErr::Internal( + "Force Rollback!".to_owned(), + ))) } else { Ok(()) } @@ -633,7 +635,9 @@ pub async fn transaction_nested() { assert_eq!(bakeries.len(), 7); if true { - Err(DbErr::Query("Force Rollback!".to_owned())) + Err(DbErr::Query(RuntimeErr::Internal( + "Force Rollback!".to_owned(), + ))) } else { Ok(()) } @@ -652,7 +656,9 @@ pub async fn transaction_nested() { assert_eq!(bakeries.len(), 6); if true { - Err(DbErr::Query("Force Rollback!".to_owned())) + Err(DbErr::Query(RuntimeErr::Internal( + "Force Rollback!".to_owned(), + ))) } else { Ok(()) }