1use std::fmt;
8
9#[derive(Debug)]
20pub enum BsqlError {
21 Pool(PoolError),
22 Query(QueryError),
23 Decode(DecodeError),
24 Connect(ConnectError),
25}
26
27#[derive(Debug)]
29pub struct PoolError {
30 pub message: String,
31 pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
32}
33
34#[derive(Debug)]
36pub struct QueryError {
37 pub message: String,
38 pub pg_code: Option<String>,
40 pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
41}
42
43#[derive(Debug)]
45pub struct DecodeError {
46 pub column: String,
47 pub expected: &'static str,
48 pub actual: String,
49}
50
51#[derive(Debug)]
53pub struct ConnectError {
54 pub message: String,
55 pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
56}
57
58pub type BsqlResult<T> = Result<T, BsqlError>;
60
61impl 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
143impl 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
167impl 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 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}