Merge pull request #1002 from SeaQL/better-errors

Better error system
This commit is contained in:
Chris Tsang 2022-10-07 00:24:41 +08:00 committed by GitHub
commit 5d752e60b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 242 additions and 163 deletions

View File

@ -43,6 +43,7 @@ uuid = { version = "^1", features = ["serde", "v4"], optional = true }
ouroboros = "0.15" ouroboros = "0.15"
url = "^2.2" url = "^2.2"
once_cell = "1.8" once_cell = "1.8"
thiserror = "^1"
[dev-dependencies] [dev-dependencies]
smol = { version = "^1.2" } smol = { version = "^1.2" }

View File

@ -26,10 +26,7 @@ macro_rules! impl_try_from_u64_err {
($newtype: ident) => { ($newtype: ident) => {
impl sea_orm::TryFromU64 for $newtype { impl sea_orm::TryFromU64 for $newtype {
fn try_from_u64(_n: u64) -> Result<Self, sea_orm::DbErr> { fn try_from_u64(_n: u64) -> Result<Self, sea_orm::DbErr> {
Err(sea_orm::DbErr::Exec(format!( Err(sea_orm::DbErr::ConvertFromU64(stringify!($newtype)))
"{} cannot be converted from u64",
stringify!($newtype)
)))
} }
} }
}; };

View File

@ -1,5 +1,5 @@
use std::marker::PhantomData;
use sea_orm::entity::prelude::*; use sea_orm::entity::prelude::*;
use std::marker::PhantomData;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "model")] #[sea_orm(table_name = "model")]
@ -31,10 +31,7 @@ impl<T> From<AccountId<T>> for Uuid {
impl<T> sea_orm::TryFromU64 for AccountId<T> { impl<T> sea_orm::TryFromU64 for AccountId<T> {
fn try_from_u64(_n: u64) -> Result<Self, sea_orm::DbErr> { fn try_from_u64(_n: u64) -> Result<Self, sea_orm::DbErr> {
Err(sea_orm::DbErr::Exec(format!( Err(sea_orm::DbErr::ConvertFromU64(stringify!(AccountId<T>)))
"{} cannot be converted from u64",
stringify!(AccountId<T>)
)))
} }
} }

View File

@ -71,7 +71,7 @@ pub fn impl_default_as_str(ident: &Ident, data: &Data) -> syn::Result<TokenStrea
)) ))
} }
/// Implement a column using for an enum using [DeriveColumn](sea_orm::DeriveColumn) /// Implement a column for an enum using [DeriveColumn](sea_orm::DeriveColumn)
pub fn impl_col_from_str(ident: &Ident, data: &Data) -> syn::Result<TokenStream> { pub fn impl_col_from_str(ident: &Ident, data: &Data) -> syn::Result<TokenStream> {
let data_enum = match data { let data_enum = match data {
Data::Enum(data_enum) => data_enum, Data::Enum(data_enum) => data_enum,
@ -99,7 +99,7 @@ pub fn impl_col_from_str(ident: &Ident, data: &Data) -> syn::Result<TokenStream>
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s { match s {
#(#columns),*, #(#columns),*,
_ => Err(sea_orm::ColumnFromStrErr(format!("Failed to parse '{}' as `{}`", s, stringify!(#ident)))), _ => Err(sea_orm::ColumnFromStrErr(s.to_owned())),
} }
} }
} }

View File

@ -116,7 +116,9 @@ impl ConnectionTrait for DatabaseConnection {
DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.execute(stmt).await, DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.execute(stmt).await,
#[cfg(feature = "mock")] #[cfg(feature = "mock")]
DatabaseConnection::MockDatabaseConnection(conn) => conn.execute(stmt), 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, DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.query_one(stmt).await,
#[cfg(feature = "mock")] #[cfg(feature = "mock")]
DatabaseConnection::MockDatabaseConnection(conn) => conn.query_one(stmt), 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, DatabaseConnection::SqlxSqlitePoolConnection(conn) => conn.query_all(stmt).await,
#[cfg(feature = "mock")] #[cfg(feature = "mock")]
DatabaseConnection::MockDatabaseConnection(conn) => conn.query_all(stmt), 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())))
}
} }
} }

View File

@ -102,7 +102,9 @@ impl MockDatabaseTrait for MockDatabase {
result: ExecResultHolder::Mock(std::mem::take(&mut self.exec_results[counter])), result: ExecResultHolder::Mock(std::mem::take(&mut self.exec_results[counter])),
}) })
} else { } 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()) .collect())
} else { } 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 where
T: ValueType, 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 /// An iterator over the keys and values of a mock row

View File

@ -18,7 +18,7 @@ pub use stream::*;
use tracing::instrument; use tracing::instrument;
pub use transaction::*; pub use transaction::*;
use crate::DbErr; use crate::{DbErr, RuntimeErr};
/// Defines a database /// Defines a database
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -75,10 +75,10 @@ impl Database {
if crate::MockDatabaseConnector::accepts(&opt.url) { if crate::MockDatabaseConnector::accepts(&opt.url) {
return crate::MockDatabaseConnector::connect(&opt.url).await; return crate::MockDatabaseConnector::connect(&opt.url).await;
} }
Err(DbErr::Conn(format!( Err(DbErr::Conn(RuntimeErr::Internal(format!(
"The connection string '{}' has no supporting driver.", "The connection string '{}' has no supporting driver.",
opt.url opt.url
))) ))))
} }
} }

View File

@ -238,6 +238,17 @@ impl DatabaseTransaction {
} }
} }
} }
#[cfg(feature = "sqlx-dep")]
fn map_err_ignore_not_found<T: std::fmt::Debug>(
err: Result<Option<T>, sqlx::Error>,
) -> Result<Option<T>, 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 { impl Drop for DatabaseTransaction {
@ -258,13 +269,14 @@ impl ConnectionTrait for DatabaseTransaction {
async fn execute(&self, stmt: Statement) -> Result<ExecResult, DbErr> { async fn execute(&self, stmt: Statement) -> Result<ExecResult, DbErr> {
debug_print!("{}", stmt); debug_print!("{}", stmt);
let _res = match &mut *self.conn.lock().await { match &mut *self.conn.lock().await {
#[cfg(feature = "sqlx-mysql")] #[cfg(feature = "sqlx-mysql")]
InnerConnection::MySql(conn) => { InnerConnection::MySql(conn) => {
let query = crate::driver::sqlx_mysql::sqlx_query(&stmt); let query = crate::driver::sqlx_mysql::sqlx_query(&stmt);
crate::metric::metric!(self.metric_callback, &stmt, { crate::metric::metric!(self.metric_callback, &stmt, {
query.execute(conn).await.map(Into::into) query.execute(conn).await.map(Into::into)
}) })
.map_err(sqlx_error_to_exec_err)
} }
#[cfg(feature = "sqlx-postgres")] #[cfg(feature = "sqlx-postgres")]
InnerConnection::Postgres(conn) => { InnerConnection::Postgres(conn) => {
@ -272,6 +284,7 @@ impl ConnectionTrait for DatabaseTransaction {
crate::metric::metric!(self.metric_callback, &stmt, { crate::metric::metric!(self.metric_callback, &stmt, {
query.execute(conn).await.map(Into::into) query.execute(conn).await.map(Into::into)
}) })
.map_err(sqlx_error_to_exec_err)
} }
#[cfg(feature = "sqlx-sqlite")] #[cfg(feature = "sqlx-sqlite")]
InnerConnection::Sqlite(conn) => { InnerConnection::Sqlite(conn) => {
@ -279,14 +292,13 @@ impl ConnectionTrait for DatabaseTransaction {
crate::metric::metric!(self.metric_callback, &stmt, { crate::metric::metric!(self.metric_callback, &stmt, {
query.execute(conn).await.map(Into::into) query.execute(conn).await.map(Into::into)
}) })
.map_err(sqlx_error_to_exec_err)
} }
#[cfg(feature = "mock")] #[cfg(feature = "mock")]
InnerConnection::Mock(conn) => return conn.execute(stmt), InnerConnection::Mock(conn) => return conn.execute(stmt),
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
_ => unreachable!(), _ => unreachable!(),
}; }
#[cfg(feature = "sqlx-dep")]
_res.map_err(sqlx_error_to_exec_err)
} }
#[instrument(level = "trace")] #[instrument(level = "trace")]
@ -294,32 +306,32 @@ impl ConnectionTrait for DatabaseTransaction {
async fn query_one(&self, stmt: Statement) -> Result<Option<QueryResult>, DbErr> { async fn query_one(&self, stmt: Statement) -> Result<Option<QueryResult>, DbErr> {
debug_print!("{}", stmt); debug_print!("{}", stmt);
let _res = match &mut *self.conn.lock().await { match &mut *self.conn.lock().await {
#[cfg(feature = "sqlx-mysql")] #[cfg(feature = "sqlx-mysql")]
InnerConnection::MySql(conn) => { InnerConnection::MySql(conn) => {
let query = crate::driver::sqlx_mysql::sqlx_query(&stmt); 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")] #[cfg(feature = "sqlx-postgres")]
InnerConnection::Postgres(conn) => { InnerConnection::Postgres(conn) => {
let query = crate::driver::sqlx_postgres::sqlx_query(&stmt); 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")] #[cfg(feature = "sqlx-sqlite")]
InnerConnection::Sqlite(conn) => { InnerConnection::Sqlite(conn) => {
let query = crate::driver::sqlx_sqlite::sqlx_query(&stmt); 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")] #[cfg(feature = "mock")]
InnerConnection::Mock(conn) => return conn.query_one(stmt), InnerConnection::Mock(conn) => return conn.query_one(stmt),
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
_ => unreachable!(), _ => 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<Vec<QueryResult>, DbErr> { async fn query_all(&self, stmt: Statement) -> Result<Vec<QueryResult>, DbErr> {
debug_print!("{}", stmt); debug_print!("{}", stmt);
let _res = match &mut *self.conn.lock().await { match &mut *self.conn.lock().await {
#[cfg(feature = "sqlx-mysql")] #[cfg(feature = "sqlx-mysql")]
InnerConnection::MySql(conn) => { InnerConnection::MySql(conn) => {
let query = crate::driver::sqlx_mysql::sqlx_query(&stmt); let query = crate::driver::sqlx_mysql::sqlx_query(&stmt);
@ -336,6 +348,7 @@ impl ConnectionTrait for DatabaseTransaction {
.fetch_all(conn) .fetch_all(conn)
.await .await
.map(|rows| rows.into_iter().map(|r| r.into()).collect()) .map(|rows| rows.into_iter().map(|r| r.into()).collect())
.map_err(sqlx_error_to_query_err)
} }
#[cfg(feature = "sqlx-postgres")] #[cfg(feature = "sqlx-postgres")]
InnerConnection::Postgres(conn) => { InnerConnection::Postgres(conn) => {
@ -344,6 +357,7 @@ impl ConnectionTrait for DatabaseTransaction {
.fetch_all(conn) .fetch_all(conn)
.await .await
.map(|rows| rows.into_iter().map(|r| r.into()).collect()) .map(|rows| rows.into_iter().map(|r| r.into()).collect())
.map_err(sqlx_error_to_query_err)
} }
#[cfg(feature = "sqlx-sqlite")] #[cfg(feature = "sqlx-sqlite")]
InnerConnection::Sqlite(conn) => { InnerConnection::Sqlite(conn) => {
@ -352,14 +366,13 @@ impl ConnectionTrait for DatabaseTransaction {
.fetch_all(conn) .fetch_all(conn)
.await .await
.map(|rows| rows.into_iter().map(|r| r.into()).collect()) .map(|rows| rows.into_iter().map(|r| r.into()).collect())
.map_err(sqlx_error_to_query_err)
} }
#[cfg(feature = "mock")] #[cfg(feature = "mock")]
InnerConnection::Mock(conn) => return conn.query_all(stmt), InnerConnection::Mock(conn) => return conn.query_all(stmt),
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
_ => unreachable!(), _ => unreachable!(),
}; }
#[cfg(feature = "sqlx-dep")]
_res.map_err(sqlx_error_to_query_err)
} }
} }

View File

@ -1,16 +1,16 @@
use crate::DbErr; use crate::{DbErr, RuntimeErr};
/// Converts an [sqlx::error] execution error to a [DbErr] /// Converts an [sqlx::error] execution error to a [DbErr]
pub fn sqlx_error_to_exec_err(err: sqlx::Error) -> 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] /// Converts an [sqlx::error] query error to a [DbErr]
pub fn sqlx_error_to_query_err(err: sqlx::Error) -> 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] /// Converts an [sqlx::error] connection error to a [DbErr]
pub fn sqlx_error_to_conn_err(err: sqlx::Error) -> DbErr { pub fn sqlx_error_to_conn_err(err: sqlx::Error) -> DbErr {
DbErr::Conn(err.to_string()) DbErr::Conn(RuntimeErr::SqlxError(err))
} }

View File

@ -45,7 +45,7 @@ impl SqlxMySqlConnector {
let mut opt = options let mut opt = options
.url .url
.parse::<MySqlConnectOptions>() .parse::<MySqlConnectOptions>()
.map_err(|e| DbErr::Conn(e.to_string()))?; .map_err(sqlx_error_to_conn_err)?;
use sqlx::ConnectOptions; use sqlx::ConnectOptions;
if !options.sqlx_logging { if !options.sqlx_logging {
opt.disable_statement_logging(); opt.disable_statement_logging();
@ -89,9 +89,7 @@ impl SqlxMySqlPoolConnection {
} }
}) })
} else { } else {
Err(DbErr::Exec( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -107,14 +105,12 @@ impl SqlxMySqlPoolConnection {
Ok(row) => Ok(Some(row.into())), Ok(row) => Ok(Some(row.into())),
Err(err) => match err { Err(err) => match err {
sqlx::Error::RowNotFound => Ok(None), sqlx::Error::RowNotFound => Ok(None),
_ => Err(DbErr::Query(err.to_string())), _ => Err(sqlx_error_to_query_err(err)),
}, },
} }
}) })
} else { } else {
Err(DbErr::Query( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -132,9 +128,7 @@ impl SqlxMySqlPoolConnection {
} }
}) })
} else { } else {
Err(DbErr::Query( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -150,9 +144,7 @@ impl SqlxMySqlPoolConnection {
self.metric_callback.clone(), self.metric_callback.clone(),
))) )))
} else { } else {
Err(DbErr::Query( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -162,9 +154,7 @@ impl SqlxMySqlPoolConnection {
if let Ok(conn) = self.pool.acquire().await { if let Ok(conn) = self.pool.acquire().await {
DatabaseTransaction::new_mysql(conn, self.metric_callback.clone()).await DatabaseTransaction::new_mysql(conn, self.metric_callback.clone()).await
} else { } else {
Err(DbErr::Query( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -185,9 +175,7 @@ impl SqlxMySqlPoolConnection {
.map_err(|e| TransactionError::Connection(e))?; .map_err(|e| TransactionError::Connection(e))?;
transaction.run(callback).await transaction.run(callback).await
} else { } else {
Err(TransactionError::Connection(DbErr::Query( Err(TransactionError::Connection(DbErr::ConnectionAcquire))
"Failed to acquire connection from pool.".to_owned(),
)))
} }
} }

View File

@ -45,7 +45,7 @@ impl SqlxPostgresConnector {
let mut opt = options let mut opt = options
.url .url
.parse::<PgConnectOptions>() .parse::<PgConnectOptions>()
.map_err(|e| DbErr::Conn(e.to_string()))?; .map_err(sqlx_error_to_conn_err)?;
use sqlx::ConnectOptions; use sqlx::ConnectOptions;
if !options.sqlx_logging { if !options.sqlx_logging {
opt.disable_statement_logging(); opt.disable_statement_logging();
@ -89,9 +89,7 @@ impl SqlxPostgresPoolConnection {
} }
}) })
} else { } else {
Err(DbErr::Exec( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -107,14 +105,12 @@ impl SqlxPostgresPoolConnection {
Ok(row) => Ok(Some(row.into())), Ok(row) => Ok(Some(row.into())),
Err(err) => match err { Err(err) => match err {
sqlx::Error::RowNotFound => Ok(None), sqlx::Error::RowNotFound => Ok(None),
_ => Err(DbErr::Query(err.to_string())), _ => Err(sqlx_error_to_query_err(err)),
}, },
} }
}) })
} else { } else {
Err(DbErr::Query( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -132,9 +128,7 @@ impl SqlxPostgresPoolConnection {
} }
}) })
} else { } else {
Err(DbErr::Query( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -150,9 +144,7 @@ impl SqlxPostgresPoolConnection {
self.metric_callback.clone(), self.metric_callback.clone(),
))) )))
} else { } else {
Err(DbErr::Query( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -162,9 +154,7 @@ impl SqlxPostgresPoolConnection {
if let Ok(conn) = self.pool.acquire().await { if let Ok(conn) = self.pool.acquire().await {
DatabaseTransaction::new_postgres(conn, self.metric_callback.clone()).await DatabaseTransaction::new_postgres(conn, self.metric_callback.clone()).await
} else { } else {
Err(DbErr::Query( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -185,9 +175,7 @@ impl SqlxPostgresPoolConnection {
.map_err(|e| TransactionError::Connection(e))?; .map_err(|e| TransactionError::Connection(e))?;
transaction.run(callback).await transaction.run(callback).await
} else { } else {
Err(TransactionError::Connection(DbErr::Query( Err(TransactionError::Connection(DbErr::ConnectionAcquire))
"Failed to acquire connection from pool.".to_owned(),
)))
} }
} }

View File

@ -46,7 +46,7 @@ impl SqlxSqliteConnector {
let mut opt = options let mut opt = options
.url .url
.parse::<SqliteConnectOptions>() .parse::<SqliteConnectOptions>()
.map_err(|e| DbErr::Conn(e.to_string()))?; .map_err(sqlx_error_to_conn_err)?;
if options.sqlcipher_key.is_some() { if options.sqlcipher_key.is_some() {
opt = opt.pragma("key", options.sqlcipher_key.clone().unwrap()); opt = opt.pragma("key", options.sqlcipher_key.clone().unwrap());
} }
@ -96,9 +96,7 @@ impl SqlxSqlitePoolConnection {
} }
}) })
} else { } else {
Err(DbErr::Exec( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -114,14 +112,12 @@ impl SqlxSqlitePoolConnection {
Ok(row) => Ok(Some(row.into())), Ok(row) => Ok(Some(row.into())),
Err(err) => match err { Err(err) => match err {
sqlx::Error::RowNotFound => Ok(None), sqlx::Error::RowNotFound => Ok(None),
_ => Err(DbErr::Query(err.to_string())), _ => Err(sqlx_error_to_query_err(err)),
}, },
} }
}) })
} else { } else {
Err(DbErr::Query( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -139,9 +135,7 @@ impl SqlxSqlitePoolConnection {
} }
}) })
} else { } else {
Err(DbErr::Query( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -157,9 +151,7 @@ impl SqlxSqlitePoolConnection {
self.metric_callback.clone(), self.metric_callback.clone(),
))) )))
} else { } else {
Err(DbErr::Query( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -169,9 +161,7 @@ impl SqlxSqlitePoolConnection {
if let Ok(conn) = self.pool.acquire().await { if let Ok(conn) = self.pool.acquire().await {
DatabaseTransaction::new_sqlite(conn, self.metric_callback.clone()).await DatabaseTransaction::new_sqlite(conn, self.metric_callback.clone()).await
} else { } else {
Err(DbErr::Query( Err(DbErr::ConnectionAcquire)
"Failed to acquire connection from pool.".to_owned(),
))
} }
} }
@ -192,9 +182,7 @@ impl SqlxSqlitePoolConnection {
.map_err(|e| TransactionError::Connection(e))?; .map_err(|e| TransactionError::Connection(e))?;
transaction.run(callback).await transaction.run(callback).await
} else { } else {
Err(TransactionError::Connection(DbErr::Query( Err(TransactionError::Connection(DbErr::ConnectionAcquire))
"Failed to acquire connection from pool.".to_owned(),
)))
} }
} }

View File

@ -1,49 +1,80 @@
#[cfg(feature = "sqlx-dep")]
use sqlx::error::Error as SqlxError;
use thiserror::Error;
/// An error from unsuccessful database operations /// An error from unsuccessful database operations
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Error, Debug)]
pub enum DbErr { 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<dyn std::error::Error + Send + Sync>,
},
/// There was a problem with the database connection /// There was a problem with the database connection
Conn(String), #[error("Connection Error: {0}")]
Conn(#[source] RuntimeErr),
/// An operation did not execute successfully /// An operation did not execute successfully
Exec(String), #[error("Execution Error: {0}")]
Exec(#[source] RuntimeErr),
/// An error occurred while performing a query /// 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 /// The record was not found in the database
#[error("RecordNotFound Error: {0}")]
RecordNotFound(String), RecordNotFound(String),
/// A custom error /// A custom error
#[error("Custom Error: {0}")]
Custom(String), Custom(String),
/// Error occurred while parsing value as target type /// Error occurred while parsing value as target type
#[error("Type Error: {0}")]
Type(String), Type(String),
/// Error occurred while parsing json value as target type /// Error occurred while parsing json value as target type
#[error("Json Error: {0}")]
Json(String), Json(String),
/// A migration error /// A migration error
#[error("Migration Error: {0}")]
Migration(String), 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 { impl PartialEq for DbErr {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn eq(&self, other: &Self) -> bool {
match self { self.to_string() == other.to_string()
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),
}
} }
} }
/// An error from a failed column operation when trying to convert the column to a string impl Eq for DbErr {}
#[derive(Debug, Clone)]
/// Error during `impl FromStr for Entity::Column`
#[derive(Error, Debug)]
#[error("Failed to match \"{0}\" as Column")]
pub struct ColumnFromStrErr(pub String); 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())
}
}

View File

@ -128,7 +128,7 @@ where
Some(value_tuple) => FromValueTuple::from_value_tuple(value_tuple), Some(value_tuple) => FromValueTuple::from_value_tuple(value_tuple),
None => match last_insert_id_opt { None => match last_insert_id_opt {
Some(last_insert_id) => last_insert_id, 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 }) Ok(InsertResult { last_insert_id })
@ -168,6 +168,8 @@ where
}; };
match found { match found {
Some(model) => Ok(model), 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(),
)),
} }
} }

View File

@ -41,7 +41,7 @@ impl From<TryGetError> for DbErr {
match e { match e {
TryGetError::DbErr(e) => e, TryGetError::DbErr(e) => e,
TryGetError::Null(s) => { 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<f64> = row let val: Option<f64> = row
.try_get(column.as_str()) .try_get(column.as_str())
.map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e)))?; .map_err(|e| TryGetError::DbErr(crate::sqlx_error_to_query_err(e)))?;
use rust_decimal::prelude::FromPrimitive;
match val { match val {
Some(v) => Decimal::from_f64(v).ok_or_else(|| { Some(v) => Decimal::try_from(v).map_err(|e| {
TryGetError::DbErr(DbErr::Query( TryGetError::DbErr(DbErr::TryIntoErr {
"Failed to convert f64 into Decimal".to_owned(), from: "f64",
)) into: "Decimal",
source: Box::new(e),
})
}), }),
None => Err(TryGetError::Null(column)), None => Err(TryGetError::Null(column)),
} }
@ -626,7 +627,7 @@ where
fn try_get_many_with_slice_len_of(len: usize, cols: &[String]) -> Result<(), TryGetError> { fn try_get_many_with_slice_len_of(len: usize, cols: &[String]) -> Result<(), TryGetError> {
if cols.len() < len { 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 {}", "Expect {} column names supplied but got slice of length {}",
len, len,
cols.len() cols.len()
@ -708,10 +709,7 @@ macro_rules! try_from_u64_err {
( $type: ty ) => { ( $type: ty ) => {
impl TryFromU64 for $type { impl TryFromU64 for $type {
fn try_from_u64(_: u64) -> Result<Self, DbErr> { fn try_from_u64(_: u64) -> Result<Self, DbErr> {
Err(DbErr::Exec(format!( Err(DbErr::ConvertFromU64(stringify!($type)))
"{} cannot be converted from u64",
stringify!($type)
)))
} }
} }
}; };
@ -722,10 +720,7 @@ macro_rules! try_from_u64_err {
$( $gen_type: TryFromU64, )* $( $gen_type: TryFromU64, )*
{ {
fn try_from_u64(_: u64) -> Result<Self, DbErr> { fn try_from_u64(_: u64) -> Result<Self, DbErr> {
Err(DbErr::Exec(format!( Err(DbErr::ConvertFromU64(stringify!($($gen_type,)*)))
"{} cannot be converted from u64",
stringify!(($($gen_type,)*))
)))
} }
} }
}; };
@ -743,12 +738,10 @@ macro_rules! try_from_u64_numeric {
impl TryFromU64 for $type { impl TryFromU64 for $type {
fn try_from_u64(n: u64) -> Result<Self, DbErr> { fn try_from_u64(n: u64) -> Result<Self, DbErr> {
use std::convert::TryInto; use std::convert::TryInto;
n.try_into().map_err(|_| { n.try_into().map_err(|e| DbErr::TryIntoErr {
DbErr::Exec(format!( from: stringify!(u64),
"fail to convert '{}' into '{}'", into: stringify!($type),
n, source: Box::new(e),
stringify!($type)
))
}) })
} }
} }
@ -823,18 +816,22 @@ try_from_u64_err!(uuid::Uuid);
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::TryGetError; use super::TryGetError;
use crate::error::DbErr; use crate::error::*;
#[test] #[test]
fn from_try_get_error() { fn from_try_get_error() {
// TryGetError::DbErr // TryGetError::DbErr
let expected = DbErr::Query("expected error message".to_owned()); let try_get_error = TryGetError::DbErr(DbErr::Query(RuntimeErr::Internal(
let try_get_error = TryGetError::DbErr(expected.clone()); "expected error message".to_owned(),
assert_eq!(DbErr::from(try_get_error), expected); )));
assert_eq!(
DbErr::from(try_get_error),
DbErr::Query(RuntimeErr::Internal("expected error message".to_owned()))
);
// TryGetError::Null // TryGetError::Null
let try_get_error = TryGetError::Null("column".to_owned()); let try_get_error = TryGetError::Null("column".to_owned());
let expected = "error occurred while decoding column: Null".to_owned(); let expected = "A null value was encountered while decoding column".to_owned();
assert_eq!(DbErr::from(try_get_error), DbErr::Query(expected)); assert_eq!(DbErr::from(try_get_error), DbErr::Type(expected));
} }
} }

View File

@ -116,7 +116,7 @@ where
Updater::new(query).check_record_exists().exec(db).await?; Updater::new(query).check_record_exists().exec(db).await?;
let primary_key_value = match model.get_primary_key_value() { let primary_key_value = match model.get_primary_key_value() {
Some(val) => FromValueTuple::from_value_tuple(val), 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 = <A::Entity as EntityTrait>::find_by_id(primary_key_value) let found = <A::Entity as EntityTrait>::find_by_id(primary_key_value)
.one(db) .one(db)
@ -124,7 +124,9 @@ where
// If we cannot select the updated row from db by the cached primary key // If we cannot select the updated row from db by the cached primary key
match found { match found {
Some(model) => Ok(model), 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(),
)),
} }
} }
} }

56
tests/crud/error.rs Normal file
View File

@ -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"),
}
}

View File

@ -3,6 +3,7 @@ pub mod create_cake;
pub mod create_lineitem; pub mod create_lineitem;
pub mod create_order; pub mod create_order;
pub mod deletes; pub mod deletes;
pub mod error;
pub mod updates; pub mod updates;
pub use create_baker::*; pub use create_baker::*;
@ -10,6 +11,7 @@ pub use create_cake::*;
pub use create_lineitem::*; pub use create_lineitem::*;
pub use create_order::*; pub use create_order::*;
pub use deletes::*; pub use deletes::*;
pub use error::*;
pub use updates::*; pub use updates::*;
pub use super::common::bakery_chain::*; pub use super::common::bakery_chain::*;

View File

@ -35,5 +35,6 @@ pub async fn create_entities(db: &DatabaseConnection) {
test_update_deleted_customer(db).await; test_update_deleted_customer(db).await;
test_delete_cake(db).await; test_delete_cake(db).await;
test_cake_error_sqlx(db).await;
test_delete_bakery(db).await; test_delete_bakery(db).await;
} }

View File

@ -508,7 +508,9 @@ pub async fn transaction_nested() {
assert_eq!(bakeries.len(), 4); assert_eq!(bakeries.len(), 4);
if true { if true {
Err(DbErr::Query("Force Rollback!".to_owned())) Err(DbErr::Query(RuntimeErr::Internal(
"Force Rollback!".to_owned(),
)))
} else { } else {
Ok(()) Ok(())
} }
@ -633,7 +635,9 @@ pub async fn transaction_nested() {
assert_eq!(bakeries.len(), 7); assert_eq!(bakeries.len(), 7);
if true { if true {
Err(DbErr::Query("Force Rollback!".to_owned())) Err(DbErr::Query(RuntimeErr::Internal(
"Force Rollback!".to_owned(),
)))
} else { } else {
Ok(()) Ok(())
} }
@ -652,7 +656,9 @@ pub async fn transaction_nested() {
assert_eq!(bakeries.len(), 6); assert_eq!(bakeries.len(), 6);
if true { if true {
Err(DbErr::Query("Force Rollback!".to_owned())) Err(DbErr::Query(RuntimeErr::Internal(
"Force Rollback!".to_owned(),
)))
} else { } else {
Ok(()) Ok(())
} }