use std::fmt;
#[derive(Debug)]
pub enum BsqlError {
Pool(PoolError),
Query(QueryError),
Decode(DecodeError),
Connect(ConnectError),
}
#[derive(Debug)]
pub struct PoolError {
pub message: String,
pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
#[derive(Debug)]
pub struct QueryError {
pub message: String,
pub pg_code: Option<String>,
pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
#[derive(Debug)]
pub struct DecodeError {
pub column: String,
pub expected: &'static str,
pub actual: String,
}
#[derive(Debug)]
pub struct ConnectError {
pub message: String,
pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
}
pub type BsqlResult<T> = Result<T, BsqlError>;
impl fmt::Display for BsqlError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Pool(e) => write!(f, "pool error: {e}"),
Self::Query(e) => write!(f, "query error: {e}"),
Self::Decode(e) => write!(f, "decode error: {e}"),
Self::Connect(e) => write!(f, "connect error: {e}"),
}
}
}
impl fmt::Display for PoolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl fmt::Display for QueryError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(code) = &self.pg_code {
write!(f, "[{code}] {}", self.message)
} else {
f.write_str(&self.message)
}
}
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"column \"{}\": expected {}, got {}",
self.column, self.expected, self.actual
)
}
}
impl fmt::Display for ConnectError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for BsqlError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Pool(e) => e.source(),
Self::Query(e) => e.source(),
Self::Decode(_) => None,
Self::Connect(e) => e.source(),
}
}
}
impl std::error::Error for PoolError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|e| &**e as &(dyn std::error::Error + 'static))
}
}
impl std::error::Error for QueryError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|e| &**e as &(dyn std::error::Error + 'static))
}
}
impl std::error::Error for DecodeError {}
impl std::error::Error for ConnectError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|e| &**e as &(dyn std::error::Error + 'static))
}
}
impl From<tokio_postgres::Error> for BsqlError {
fn from(e: tokio_postgres::Error) -> Self {
let pg_code = e.code().map(|c| c.code().to_owned());
let message = e.to_string();
BsqlError::Query(QueryError {
message,
pg_code,
source: Some(Box::new(e)),
})
}
}
impl From<deadpool_postgres::PoolError> for BsqlError {
fn from(e: deadpool_postgres::PoolError) -> Self {
let message = e.to_string();
BsqlError::Pool(PoolError {
message,
source: Some(Box::new(e)),
})
}
}
impl PoolError {
pub fn exhausted() -> BsqlError {
BsqlError::Pool(PoolError {
message: "pool exhausted: all connections in use".into(),
source: None,
})
}
}
impl ConnectError {
pub fn create(msg: impl Into<String>) -> BsqlError {
BsqlError::Connect(ConnectError {
message: msg.into(),
source: None,
})
}
pub fn with_source(
msg: impl Into<String>,
source: impl std::error::Error + Send + Sync + 'static,
) -> BsqlError {
BsqlError::Connect(ConnectError {
message: msg.into(),
source: Some(Box::new(source)),
})
}
}
impl QueryError {
pub fn row_count(expected: &str, actual: u64) -> BsqlError {
BsqlError::Query(QueryError {
message: format!("expected {expected}, got {actual} rows"),
pg_code: None,
source: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pool_error_display() {
let e = PoolError::exhausted();
assert_eq!(
e.to_string(),
"pool error: pool exhausted: all connections in use"
);
}
#[test]
fn query_error_with_code_display() {
let e = BsqlError::Query(QueryError {
message: "duplicate key".into(),
pg_code: Some("23505".into()),
source: None,
});
assert_eq!(e.to_string(), "query error: [23505] duplicate key");
}
#[test]
fn query_error_without_code_display() {
let e = QueryError::row_count("exactly 1 row", 0);
assert_eq!(
e.to_string(),
"query error: expected exactly 1 row, got 0 rows"
);
}
#[test]
fn decode_error_display() {
let e = BsqlError::Decode(DecodeError {
column: "age".into(),
expected: "i32",
actual: "text".into(),
});
assert_eq!(
e.to_string(),
"decode error: column \"age\": expected i32, got text"
);
}
#[test]
fn connect_error_display() {
let e = ConnectError::create("connection refused");
assert_eq!(e.to_string(), "connect error: connection refused");
}
#[test]
fn error_is_send_sync() {
fn assert_send_sync<T: Send + Sync + 'static>() {}
assert_send_sync::<BsqlError>();
}
#[test]
fn error_implements_std_error() {
fn assert_std_error<T: std::error::Error>() {}
assert_std_error::<BsqlError>();
}
#[test]
fn from_tokio_postgres_error() {
fn _accepts_pg_error(e: tokio_postgres::Error) -> BsqlError {
e.into()
}
}
#[test]
fn from_deadpool_error() {
fn _accepts_pool_error(e: deadpool_postgres::PoolError) -> BsqlError {
e.into()
}
}
}