1use std::borrow::Cow;
8use std::fmt;
9
10#[derive(Debug)]
21pub enum BsqlError {
22 Pool(PoolError),
23 Query(QueryError),
24 Decode(DecodeError),
25 Connect(ConnectError),
26}
27
28#[derive(Debug)]
30pub struct PoolError {
31 pub message: Cow<'static, str>,
32 pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
33}
34
35#[derive(Debug)]
37pub struct QueryError {
38 pub message: Cow<'static, str>,
39 pub pg_code: Option<String>,
41 pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
42}
43
44#[derive(Debug)]
46pub struct DecodeError {
47 pub column: String,
48 pub expected: &'static str,
49 pub actual: String,
50}
51
52#[derive(Debug)]
54pub struct ConnectError {
55 pub message: Cow<'static, str>,
56 pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
57}
58
59pub type BsqlResult<T> = Result<T, BsqlError>;
61
62impl fmt::Display for BsqlError {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 match self {
67 Self::Pool(e) => write!(f, "pool error: {e}"),
68 Self::Query(e) => write!(f, "query error: {e}"),
69 Self::Decode(e) => write!(f, "decode error: {e}"),
70 Self::Connect(e) => write!(f, "connect error: {e}"),
71 }
72 }
73}
74
75impl fmt::Display for PoolError {
76 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77 f.write_str(&self.message)
78 }
79}
80
81impl fmt::Display for QueryError {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match &self.pg_code {
84 Some(code) => write!(f, "[{code}] {}", self.message),
85 None => 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 boxed_source(&self.source)
120 }
121}
122
123impl std::error::Error for QueryError {
124 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
125 boxed_source(&self.source)
126 }
127}
128
129impl std::error::Error for DecodeError {}
130
131impl std::error::Error for ConnectError {
132 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
133 boxed_source(&self.source)
134 }
135}
136
137fn boxed_source(
138 src: &Option<Box<dyn std::error::Error + Send + Sync>>,
139) -> Option<&(dyn std::error::Error + 'static)> {
140 src.as_ref()
141 .map(|e| &**e as &(dyn std::error::Error + 'static))
142}
143
144impl From<tokio_postgres::Error> for BsqlError {
147 fn from(e: tokio_postgres::Error) -> Self {
148 let pg_code = e.code().map(|c| c.code().to_owned());
149 let message = Cow::Owned(e.to_string());
150 BsqlError::Query(QueryError {
151 message,
152 pg_code,
153 source: Some(Box::new(e)),
154 })
155 }
156}
157
158impl From<deadpool_postgres::PoolError> for BsqlError {
159 fn from(e: deadpool_postgres::PoolError) -> Self {
160 let message = Cow::Owned(e.to_string());
161 BsqlError::Pool(PoolError {
162 message,
163 source: Some(Box::new(e)),
164 })
165 }
166}
167
168impl PoolError {
171 pub fn exhausted() -> BsqlError {
172 BsqlError::Pool(PoolError {
173 message: Cow::Borrowed("pool exhausted: all connections in use"),
174 source: None,
175 })
176 }
177}
178
179impl ConnectError {
180 pub fn create(msg: impl Into<String>) -> BsqlError {
181 BsqlError::Connect(ConnectError {
182 message: Cow::Owned(msg.into()),
183 source: None,
184 })
185 }
186
187 pub fn with_source(
188 msg: impl Into<String>,
189 source: impl std::error::Error + Send + Sync + 'static,
190 ) -> BsqlError {
191 BsqlError::Connect(ConnectError {
192 message: Cow::Owned(msg.into()),
193 source: Some(Box::new(source)),
194 })
195 }
196}
197
198impl QueryError {
199 pub fn row_count(expected: &str, actual: u64) -> BsqlError {
200 BsqlError::Query(QueryError {
201 message: Cow::Owned(format!("expected {expected}, got {actual} rows")),
202 pg_code: None,
203 source: None,
204 })
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use std::error::Error as _;
212
213 #[test]
214 fn pool_error_display() {
215 let e = PoolError::exhausted();
216 assert_eq!(
217 e.to_string(),
218 "pool error: pool exhausted: all connections in use"
219 );
220 }
221
222 #[test]
223 fn query_error_with_code_display() {
224 let e = BsqlError::Query(QueryError {
225 message: Cow::Borrowed("duplicate key"),
226 pg_code: Some("23505".into()),
227 source: None,
228 });
229 assert_eq!(e.to_string(), "query error: [23505] duplicate key");
230 }
231
232 #[test]
233 fn query_error_without_code_display() {
234 let e = QueryError::row_count("exactly 1 row", 0);
235 assert_eq!(
236 e.to_string(),
237 "query error: expected exactly 1 row, got 0 rows"
238 );
239 }
240
241 #[test]
242 fn decode_error_display() {
243 let e = BsqlError::Decode(DecodeError {
244 column: "age".into(),
245 expected: "i32",
246 actual: "text".into(),
247 });
248 assert_eq!(
249 e.to_string(),
250 "decode error: column \"age\": expected i32, got text"
251 );
252 }
253
254 #[test]
255 fn connect_error_display() {
256 let e = ConnectError::create("connection refused");
257 assert_eq!(e.to_string(), "connect error: connection refused");
258 }
259
260 #[test]
261 fn pool_exhausted_uses_borrowed_cow() {
262 let e = PoolError::exhausted();
263 match e {
264 BsqlError::Pool(ref pe) => {
265 assert!(
266 matches!(pe.message, Cow::Borrowed(_)),
267 "exhausted() should use Cow::Borrowed for zero-alloc"
268 );
269 }
270 _ => panic!("expected Pool variant"),
271 }
272 }
273
274 #[test]
275 fn connect_error_uses_owned_cow() {
276 let e = ConnectError::create("dynamic message");
277 match e {
278 BsqlError::Connect(ref ce) => {
279 assert!(
280 matches!(ce.message, Cow::Owned(_)),
281 "create() with dynamic msg should use Cow::Owned"
282 );
283 }
284 _ => panic!("expected Connect variant"),
285 }
286 }
287
288 #[test]
289 fn query_row_count_uses_owned_cow() {
290 let e = QueryError::row_count("exactly 1 row", 5);
291 match e {
292 BsqlError::Query(ref qe) => {
293 assert!(
294 matches!(qe.message, Cow::Owned(_)),
295 "row_count() with formatted msg should use Cow::Owned"
296 );
297 }
298 _ => panic!("expected Query variant"),
299 }
300 }
301
302 #[test]
303 fn pool_error_source_chain() {
304 let e = PoolError::exhausted();
305 assert!(e.source().is_none());
307 }
308
309 #[test]
310 fn connect_error_with_source_chain() {
311 let inner = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
312 let e = ConnectError::with_source("connection failed", inner);
313 assert!(e.source().is_some());
314 }
315
316 #[test]
317 fn decode_error_has_no_source() {
318 let e = BsqlError::Decode(DecodeError {
319 column: "col".into(),
320 expected: "i32",
321 actual: "text".into(),
322 });
323 assert!(e.source().is_none());
324 }
325}