From e08f05b8792aed67c77444a45b0720b46f20f6bd Mon Sep 17 00:00:00 2001 From: Ryan Leckey Date: Sat, 30 May 2020 15:55:04 -0700 Subject: [PATCH] feat: re-introduce error downcasting --- sqlx-core/src/error.rs | 77 ++++++++++++++++++++++ sqlx-core/src/mysql/error.rs | 15 +++++ sqlx-core/src/postgres/error.rs | 19 +++++- sqlx-core/src/postgres/message/response.rs | 2 +- sqlx-core/src/sqlite/error.rs | 15 +++++ tests/docker-compose.yml | 1 + tests/postgres/postgres.rs | 26 ++++++++ tests/x.py | 2 +- 8 files changed, 153 insertions(+), 4 deletions(-) diff --git a/sqlx-core/src/error.rs b/sqlx-core/src/error.rs index e59d20ba..efceab79 100644 --- a/sqlx-core/src/error.rs +++ b/sqlx-core/src/error.rs @@ -89,6 +89,20 @@ pub enum Error { } impl Error { + pub fn into_database_error(self) -> Option> { + match self { + Error::Database(err) => Some(err), + _ => None, + } + } + + pub fn as_database_error(&self) -> Option<&(dyn DatabaseError + 'static)> { + match self { + Error::Database(err) => Some(&**err), + _ => None, + } + } + #[allow(dead_code)] #[inline] pub(crate) fn protocol(err: impl Display) -> Self { @@ -124,6 +138,69 @@ pub trait DatabaseError: 'static + Send + Sync + StdError { fn code(&self) -> Option> { None } + + #[doc(hidden)] + fn as_error(&self) -> &(dyn StdError + Send + Sync + 'static); + + #[doc(hidden)] + fn as_error_mut(&mut self) -> &mut (dyn StdError + Send + Sync + 'static); + + #[doc(hidden)] + fn into_error(self: Box) -> Box; +} + +impl dyn DatabaseError { + /// Downcast a reference to this generic database error to a specific + /// database error type. + /// + /// # Panics + /// + /// Panics if the database error type is not `E`. This is a deliberate contrast from + /// `Error::downcast_ref` which returns `Option<&E>`. In normal usage, you should know the + /// specific error type. In other cases, use `try_downcast_ref`. + /// + pub fn downcast_ref(&self) -> &E { + self.try_downcast_ref().unwrap_or_else(|| { + panic!( + "downcast to wrong DatabaseError type; original error: {}", + self + ) + }) + } + + /// Downcast this generic database error to a specific database error type. + /// + /// # Panics + /// + /// Panics if the database error type is not `E`. This is a deliberate contrast from + /// `Error::downcast` which returns `Option`. In normal usage, you should know the + /// specific error type. In other cases, use `try_downcast`. + /// + pub fn downcast(self: Box) -> Box { + self.try_downcast().unwrap_or_else(|e| { + panic!( + "downcast to wrong DatabaseError type; original error: {}", + e + ) + }) + } + + /// Downcast a reference to this generic database error to a specific + /// database error type. + #[inline] + pub fn try_downcast_ref(&self) -> Option<&E> { + self.as_error().downcast_ref() + } + + /// Downcast this generic database error to a specific database error type. + #[inline] + pub fn try_downcast(self: Box) -> StdResult, Box> { + if self.as_error().is::() { + Ok(self.into_error().downcast().unwrap()) + } else { + Err(self) + } + } } impl From for Error diff --git a/sqlx-core/src/mysql/error.rs b/sqlx-core/src/mysql/error.rs index ae530f1c..ae2c5030 100644 --- a/sqlx-core/src/mysql/error.rs +++ b/sqlx-core/src/mysql/error.rs @@ -61,4 +61,19 @@ impl DatabaseError for MySqlDatabaseError { fn code(&self) -> Option> { self.code().map(Cow::Borrowed) } + + #[doc(hidden)] + fn as_error(&self) -> &(dyn Error + Send + Sync + 'static) { + self + } + + #[doc(hidden)] + fn as_error_mut(&mut self) -> &mut (dyn Error + Send + Sync + 'static) { + self + } + + #[doc(hidden)] + fn into_error(self: Box) -> Box { + self + } } diff --git a/sqlx-core/src/postgres/error.rs b/sqlx-core/src/postgres/error.rs index 91b3f8dc..6416bd58 100644 --- a/sqlx-core/src/postgres/error.rs +++ b/sqlx-core/src/postgres/error.rs @@ -2,10 +2,10 @@ use std::error::Error; use std::fmt::{self, Debug, Display, Formatter}; use atoi::atoi; +use smallvec::alloc::borrow::Cow; use crate::error::DatabaseError; use crate::postgres::message::{Notice, PgSeverity}; -use smallvec::alloc::borrow::Cow; /// An error returned from the PostgreSQL database. pub struct PgDatabaseError(pub(crate) Notice); @@ -115,7 +115,7 @@ impl PgDatabaseError { } } -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] pub enum PgErrorPosition<'a> { /// A position (in characters) into the original query. Original(usize), @@ -169,4 +169,19 @@ impl DatabaseError for PgDatabaseError { fn code(&self) -> Option> { Some(Cow::Borrowed(self.code())) } + + #[doc(hidden)] + fn as_error(&self) -> &(dyn Error + Send + Sync + 'static) { + self + } + + #[doc(hidden)] + fn as_error_mut(&mut self) -> &mut (dyn Error + Send + Sync + 'static) { + self + } + + #[doc(hidden)] + fn into_error(self: Box) -> Box { + self + } } diff --git a/sqlx-core/src/postgres/message/response.rs b/sqlx-core/src/postgres/message/response.rs index 04574985..fc324399 100644 --- a/sqlx-core/src/postgres/message/response.rs +++ b/sqlx-core/src/postgres/message/response.rs @@ -6,7 +6,7 @@ use memchr::memchr; use crate::error::Error; use crate::io::Decode; -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] #[repr(u8)] pub enum PgSeverity { Panic, diff --git a/sqlx-core/src/sqlite/error.rs b/sqlx-core/src/sqlite/error.rs index f5b9b7e9..5fcc3394 100644 --- a/sqlx-core/src/sqlite/error.rs +++ b/sqlx-core/src/sqlite/error.rs @@ -50,4 +50,19 @@ impl DatabaseError for SqliteError { fn message(&self) -> &str { &self.message } + + #[doc(hidden)] + fn as_error(&self) -> &(dyn StdError + Send + Sync + 'static) { + self + } + + #[doc(hidden)] + fn as_error_mut(&mut self) -> &mut (dyn StdError + Send + Sync + 'static) { + self + } + + #[doc(hidden)] + fn into_error(self: Box) -> Box { + self + } } diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 8308b080..4e1c4d5a 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -8,6 +8,7 @@ services: - "$HOME/.cargo/registry:/usr/local/cargo/registry" working_dir: "/home/rust/src" environment: + CARGO_TARGET_DIR: "/home/rust/src/tests/target" CARGO_INCREMENTAL: "0" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort" RUSTDOCFLAGS: "-Cpanic=abort" diff --git a/tests/postgres/postgres.rs b/tests/postgres/postgres.rs index 1ee0fc0a..ec319697 100644 --- a/tests/postgres/postgres.rs +++ b/tests/postgres/postgres.rs @@ -1,6 +1,7 @@ use futures::TryStreamExt; use sqlx::postgres::PgRow; use sqlx::{postgres::Postgres, Executor, Row}; +use sqlx_core::postgres::{PgDatabaseError, PgErrorPosition, PgSeverity}; use sqlx_test::new; #[sqlx_macros::test] @@ -32,6 +33,31 @@ async fn it_maths() -> anyhow::Result<()> { Ok(()) } +#[sqlx_macros::test] +async fn it_can_inspect_errors() -> anyhow::Result<()> { + let mut conn = new::().await?; + + let res: Result = sqlx::query("select f").execute(&mut conn).await; + let err = res.unwrap_err(); + + // can also do [as_database_error] or use `match ..` + let err = err.into_database_error().unwrap(); + + assert_eq!(err.message(), "column \"f\" does not exist"); + assert_eq!(err.code().as_deref(), Some("42703")); + + // can also do [downcast_ref] + let err: Box = err.downcast(); + + assert_eq!(err.severity(), PgSeverity::Error); + assert_eq!(err.message(), "column \"f\" does not exist"); + assert_eq!(err.code(), "42703"); + assert_eq!(err.position(), Some(PgErrorPosition::Original(8))); + assert_eq!(err.routine(), Some("errorMissingColumn")); + + Ok(()) +} + #[sqlx_macros::test] async fn it_executes() -> anyhow::Result<()> { let mut conn = new::().await?; diff --git a/tests/x.py b/tests/x.py index f2ee55d2..d076cc36 100755 --- a/tests/x.py +++ b/tests/x.py @@ -85,7 +85,7 @@ def run(command, comment=None, env=None, service=None, tag=None, args=None): # before we start, we clean previous profile data # keeping these around can cause weird errors -for path in glob(os.path.join(os.path.dirname(__file__), "../target/**/*.gc*"), recursive=True): +for path in glob(os.path.join(os.path.dirname(__file__), "target/**/*.gc*"), recursive=True): os.remove(path) #