Skip to main content

bsql_core/
error.rs

1//! Error types for bsql.
2//!
3//! [`BsqlError`] is the single error type returned by all bsql operations.
4//! It has four variants matching the four failure modes of a database operation:
5//! pool, query execution, data decoding, and initial connection.
6
7use std::fmt;
8
9/// The error type for all bsql operations.
10///
11/// # Variants
12///
13/// - [`Pool`](BsqlError::Pool) — connection pool exhausted or misconfigured.
14/// - [`Query`](BsqlError::Query) — PostgreSQL rejected the query at runtime
15///   (triggers, RLS policies, constraint violations).
16/// - [`Decode`](BsqlError::Decode) — a column value could not be converted to
17///   the expected Rust type.
18/// - [`Connect`](BsqlError::Connect) — initial connection to PostgreSQL failed.
19#[derive(Debug)]
20pub enum BsqlError {
21    Pool(PoolError),
22    Query(QueryError),
23    Decode(DecodeError),
24    Connect(ConnectError),
25}
26
27/// Connection pool failure.
28#[derive(Debug)]
29pub struct PoolError {
30    pub message: String,
31    pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
32}
33
34/// Query execution failure. Contains the PostgreSQL error code when available.
35#[derive(Debug)]
36pub struct QueryError {
37    pub message: String,
38    /// The five-character SQLSTATE code (e.g. `"23505"` for unique violation).
39    pub pg_code: Option<String>,
40    pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
41}
42
43/// Row/column decoding failure.
44#[derive(Debug)]
45pub struct DecodeError {
46    pub column: String,
47    pub expected: &'static str,
48    pub actual: String,
49}
50
51/// Initial connection failure.
52#[derive(Debug)]
53pub struct ConnectError {
54    pub message: String,
55    pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
56}
57
58/// Convenience alias used throughout bsql.
59pub type BsqlResult<T> = Result<T, BsqlError>;
60
61// --- Display ---
62
63impl fmt::Display for BsqlError {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        match self {
66            Self::Pool(e) => write!(f, "pool error: {e}"),
67            Self::Query(e) => write!(f, "query error: {e}"),
68            Self::Decode(e) => write!(f, "decode error: {e}"),
69            Self::Connect(e) => write!(f, "connect error: {e}"),
70        }
71    }
72}
73
74impl fmt::Display for PoolError {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        f.write_str(&self.message)
77    }
78}
79
80impl fmt::Display for QueryError {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        if let Some(code) = &self.pg_code {
83            write!(f, "[{code}] {}", self.message)
84        } else {
85            f.write_str(&self.message)
86        }
87    }
88}
89
90impl fmt::Display for DecodeError {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        write!(
93            f,
94            "column \"{}\": expected {}, got {}",
95            self.column, self.expected, self.actual
96        )
97    }
98}
99
100impl fmt::Display for ConnectError {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        f.write_str(&self.message)
103    }
104}
105
106impl std::error::Error for BsqlError {
107    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
108        match self {
109            Self::Pool(e) => e.source(),
110            Self::Query(e) => e.source(),
111            Self::Decode(_) => None,
112            Self::Connect(e) => e.source(),
113        }
114    }
115}
116
117impl std::error::Error for PoolError {
118    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
119        self.source
120            .as_ref()
121            .map(|e| &**e as &(dyn std::error::Error + 'static))
122    }
123}
124
125impl std::error::Error for QueryError {
126    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
127        self.source
128            .as_ref()
129            .map(|e| &**e as &(dyn std::error::Error + 'static))
130    }
131}
132
133impl std::error::Error for DecodeError {}
134
135impl std::error::Error for ConnectError {
136    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
137        self.source
138            .as_ref()
139            .map(|e| &**e as &(dyn std::error::Error + 'static))
140    }
141}
142
143// --- From conversions ---
144
145impl From<tokio_postgres::Error> for BsqlError {
146    fn from(e: tokio_postgres::Error) -> Self {
147        let pg_code = e.code().map(|c| c.code().to_owned());
148        let message = e.to_string();
149        BsqlError::Query(QueryError {
150            message,
151            pg_code,
152            source: Some(Box::new(e)),
153        })
154    }
155}
156
157impl From<deadpool_postgres::PoolError> for BsqlError {
158    fn from(e: deadpool_postgres::PoolError) -> Self {
159        let message = e.to_string();
160        BsqlError::Pool(PoolError {
161            message,
162            source: Some(Box::new(e)),
163        })
164    }
165}
166
167// --- Constructor helpers ---
168
169impl PoolError {
170    pub fn exhausted() -> BsqlError {
171        BsqlError::Pool(PoolError {
172            message: "pool exhausted: all connections in use".into(),
173            source: None,
174        })
175    }
176}
177
178impl ConnectError {
179    pub fn create(msg: impl Into<String>) -> BsqlError {
180        BsqlError::Connect(ConnectError {
181            message: msg.into(),
182            source: None,
183        })
184    }
185
186    pub fn with_source(
187        msg: impl Into<String>,
188        source: impl std::error::Error + Send + Sync + 'static,
189    ) -> BsqlError {
190        BsqlError::Connect(ConnectError {
191            message: msg.into(),
192            source: Some(Box::new(source)),
193        })
194    }
195}
196
197impl QueryError {
198    pub fn row_count(expected: &str, actual: u64) -> BsqlError {
199        BsqlError::Query(QueryError {
200            message: format!("expected {expected}, got {actual} rows"),
201            pg_code: None,
202            source: None,
203        })
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn pool_error_display() {
213        let e = PoolError::exhausted();
214        assert_eq!(
215            e.to_string(),
216            "pool error: pool exhausted: all connections in use"
217        );
218    }
219
220    #[test]
221    fn query_error_with_code_display() {
222        let e = BsqlError::Query(QueryError {
223            message: "duplicate key".into(),
224            pg_code: Some("23505".into()),
225            source: None,
226        });
227        assert_eq!(e.to_string(), "query error: [23505] duplicate key");
228    }
229
230    #[test]
231    fn query_error_without_code_display() {
232        let e = QueryError::row_count("exactly 1 row", 0);
233        assert_eq!(
234            e.to_string(),
235            "query error: expected exactly 1 row, got 0 rows"
236        );
237    }
238
239    #[test]
240    fn decode_error_display() {
241        let e = BsqlError::Decode(DecodeError {
242            column: "age".into(),
243            expected: "i32",
244            actual: "text".into(),
245        });
246        assert_eq!(
247            e.to_string(),
248            "decode error: column \"age\": expected i32, got text"
249        );
250    }
251
252    #[test]
253    fn connect_error_display() {
254        let e = ConnectError::create("connection refused");
255        assert_eq!(e.to_string(), "connect error: connection refused");
256    }
257
258    #[test]
259    fn error_is_send_sync() {
260        fn assert_send_sync<T: Send + Sync + 'static>() {}
261        assert_send_sync::<BsqlError>();
262    }
263
264    #[test]
265    fn error_implements_std_error() {
266        fn assert_std_error<T: std::error::Error>() {}
267        assert_std_error::<BsqlError>();
268    }
269
270    #[test]
271    fn from_tokio_postgres_error() {
272        // tokio_postgres::Error is not easily constructable in tests,
273        // but we can verify the From impl exists and the type compiles.
274        fn _accepts_pg_error(e: tokio_postgres::Error) -> BsqlError {
275            e.into()
276        }
277    }
278
279    #[test]
280    fn from_deadpool_error() {
281        fn _accepts_pool_error(e: deadpool_postgres::PoolError) -> BsqlError {
282            e.into()
283        }
284    }
285}