1use std::borrow::Cow;
8use std::fmt;
9
10#[derive(Debug)]
38pub enum BsqlError {
39 Pool(PoolError),
40 Query(QueryError),
41 Decode(DecodeError),
42 Connect(ConnectError),
43}
44
45#[derive(Debug)]
47pub struct PoolError {
48 pub message: Cow<'static, str>,
49 pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
50}
51
52#[derive(Debug)]
54pub struct QueryError {
55 pub message: Cow<'static, str>,
56 pub pg_code: Option<Box<str>>,
58 pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
59}
60
61#[derive(Debug)]
63pub struct DecodeError {
64 pub column: Cow<'static, str>,
65 pub expected: &'static str,
66 pub actual: Cow<'static, str>,
67 pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
69}
70
71#[derive(Debug)]
73pub struct ConnectError {
74 pub message: Cow<'static, str>,
75 pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync>>,
76}
77
78pub type BsqlResult<T> = Result<T, BsqlError>;
80
81impl fmt::Display for BsqlError {
84 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
85 match self {
86 Self::Pool(e) => write!(f, "pool error: {e}"),
87 Self::Query(e) => write!(f, "query error: {e}"),
88 Self::Decode(e) => write!(f, "decode error: {e}"),
89 Self::Connect(e) => write!(f, "connect error: {e}"),
90 }
91 }
92}
93
94impl fmt::Display for PoolError {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(&self.message)
97 }
98}
99
100impl fmt::Display for QueryError {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 match &self.pg_code {
103 Some(code) => write!(f, "[{}] {}", &**code, self.message),
104 None => f.write_str(&self.message),
105 }
106 }
107}
108
109impl fmt::Display for DecodeError {
110 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111 write!(
112 f,
113 "column \"{}\": expected {}, got {}",
114 self.column, self.expected, self.actual
115 )
116 }
117}
118
119impl fmt::Display for ConnectError {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 f.write_str(&self.message)
122 }
123}
124
125impl std::error::Error for BsqlError {
126 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
127 match self {
128 Self::Pool(e) => e.source(),
129 Self::Query(e) => e.source(),
130 Self::Decode(e) => e.source(),
131 Self::Connect(e) => e.source(),
132 }
133 }
134}
135
136impl std::error::Error for PoolError {
137 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
138 boxed_source(&self.source)
139 }
140}
141
142impl std::error::Error for QueryError {
143 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
144 boxed_source(&self.source)
145 }
146}
147
148impl std::error::Error for DecodeError {
149 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
150 boxed_source(&self.source)
151 }
152}
153
154impl std::error::Error for ConnectError {
155 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
156 boxed_source(&self.source)
157 }
158}
159
160fn boxed_source(
161 src: &Option<Box<dyn std::error::Error + Send + Sync>>,
162) -> Option<&(dyn std::error::Error + 'static)> {
163 src.as_ref()
164 .map(|e| &**e as &(dyn std::error::Error + 'static))
165}
166
167impl BsqlError {
170 pub fn is_timeout(&self) -> bool {
173 matches!(self, BsqlError::Query(q) if q.pg_code.as_deref() == Some("57014"))
174 }
175
176 pub fn is_serialization_failure(&self) -> bool {
181 matches!(self, BsqlError::Query(q) if q.pg_code.as_deref() == Some("40001"))
182 }
183
184 pub fn is_unique_violation(&self) -> bool {
189 matches!(self, BsqlError::Query(q) if q.pg_code.as_deref() == Some("23505"))
190 }
191
192 pub fn is_foreign_key_violation(&self) -> bool {
197 matches!(self, BsqlError::Query(q) if q.pg_code.as_deref() == Some("23503"))
198 }
199
200 pub fn is_not_null_violation(&self) -> bool {
204 matches!(self, BsqlError::Query(q) if q.pg_code.as_deref() == Some("23502"))
205 }
206
207 pub fn is_check_violation(&self) -> bool {
209 matches!(self, BsqlError::Query(q) if q.pg_code.as_deref() == Some("23514"))
210 }
211
212 pub fn is_deadlock(&self) -> bool {
217 matches!(self, BsqlError::Query(q) if q.pg_code.as_deref() == Some("40P01"))
218 }
219
220 pub fn pg_code(&self) -> Option<&str> {
235 match self {
236 BsqlError::Query(q) => q.pg_code.as_deref(),
237 _ => None,
238 }
239 }
240
241 pub fn from_driver_query(e: bsql_driver_postgres::DriverError) -> Self {
247 match e {
248 bsql_driver_postgres::DriverError::Io(io_err) => BsqlError::Query(QueryError {
249 message: Cow::Owned(format!("I/O error during query: {io_err}")),
250 pg_code: None,
251 source: Some(Box::new(io_err)),
252 }),
253 other => BsqlError::from(other),
254 }
255 }
256}
257
258impl From<bsql_driver_postgres::DriverError> for BsqlError {
261 fn from(e: bsql_driver_postgres::DriverError) -> Self {
262 match e {
263 bsql_driver_postgres::DriverError::Io(io_err) => BsqlError::Connect(ConnectError {
264 message: Cow::Owned(io_err.to_string()),
265 source: Some(Box::new(io_err)),
266 }),
267 bsql_driver_postgres::DriverError::Auth(msg) => BsqlError::Connect(ConnectError {
268 message: Cow::Owned(msg),
269 source: None,
270 }),
271 bsql_driver_postgres::DriverError::Protocol(msg) => BsqlError::Query(QueryError {
272 message: Cow::Owned(msg),
273 pg_code: None,
274 source: None,
275 }),
276 bsql_driver_postgres::DriverError::Server {
277 code,
278 message,
279 detail,
280 hint,
281 position,
282 } => {
283 let msg = {
284 let has_extras = position.is_some() || detail.is_some() || hint.is_some();
285 if has_extras {
286 let mut s = String::from(&*message);
287 if let Some(pos) = position {
288 use std::fmt::Write;
289 let _ = write!(s, " (at position {pos})");
290 }
291 if let Some(d) = &detail {
292 s.push_str("\n detail: ");
293 s.push_str(d);
294 }
295 if let Some(h) = &hint {
296 s.push_str("\n hint: ");
297 s.push_str(h);
298 }
299 Cow::Owned(s)
300 } else {
301 Cow::Owned(String::from(message))
302 }
303 };
304 BsqlError::Query(QueryError {
305 message: msg,
306 pg_code: Some(code),
307 source: None,
308 })
309 }
310 bsql_driver_postgres::DriverError::Pool(msg) => BsqlError::Pool(PoolError {
311 message: Cow::Owned(msg),
312 source: None,
313 }),
314 }
315 }
316}
317
318#[cfg(feature = "sqlite")]
321impl BsqlError {
322 pub fn from_sqlite(e: bsql_driver_sqlite::SqliteError) -> Self {
324 match e {
325 bsql_driver_sqlite::SqliteError::Sqlite { code, message } => {
326 BsqlError::Query(QueryError {
327 message: Cow::Owned(format!("SQLite error [{code}]: {message}")),
328 pg_code: None,
329 source: None,
330 })
331 }
332 bsql_driver_sqlite::SqliteError::Io(io_err) => BsqlError::Connect(ConnectError {
333 message: Cow::Owned(format!("SQLite I/O error: {io_err}")),
334 source: Some(Box::new(io_err)),
335 }),
336 bsql_driver_sqlite::SqliteError::Internal(msg) => BsqlError::Query(QueryError {
337 message: Cow::Owned(format!("SQLite internal error: {msg}")),
338 pg_code: None,
339 source: None,
340 }),
341 bsql_driver_sqlite::SqliteError::Pool(msg) => BsqlError::Pool(PoolError {
342 message: Cow::Owned(format!("SQLite pool error: {msg}")),
343 source: None,
344 }),
345 }
346 }
347}
348
349impl PoolError {
352 pub fn exhausted() -> BsqlError {
353 BsqlError::Pool(PoolError {
354 message: Cow::Borrowed("pool exhausted: all connections in use"),
355 source: None,
356 })
357 }
358}
359
360impl ConnectError {
361 pub fn create(msg: impl Into<String>) -> BsqlError {
362 BsqlError::Connect(ConnectError {
363 message: Cow::Owned(msg.into()),
364 source: None,
365 })
366 }
367
368 pub fn with_source(
369 msg: impl Into<String>,
370 source: impl std::error::Error + Send + Sync + 'static,
371 ) -> BsqlError {
372 BsqlError::Connect(ConnectError {
373 message: Cow::Owned(msg.into()),
374 source: Some(Box::new(source)),
375 })
376 }
377}
378
379impl QueryError {
380 pub fn row_count(expected: &str, actual: u64) -> BsqlError {
381 BsqlError::Query(QueryError {
382 message: Cow::Owned(format!("expected {expected}, got {actual} rows")),
383 pg_code: None,
384 source: None,
385 })
386 }
387}
388
389impl DecodeError {
390 pub fn with_source(
392 column: impl Into<Cow<'static, str>>,
393 expected: &'static str,
394 actual: impl Into<Cow<'static, str>>,
395 source: impl std::error::Error + Send + Sync + 'static,
396 ) -> BsqlError {
397 BsqlError::Decode(DecodeError {
398 column: column.into(),
399 expected,
400 actual: actual.into(),
401 source: Some(Box::new(source)),
402 })
403 }
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409 use std::error::Error as _;
410
411 #[test]
412 fn pool_error_display() {
413 let e = PoolError::exhausted();
414 assert_eq!(
415 e.to_string(),
416 "pool error: pool exhausted: all connections in use"
417 );
418 }
419
420 #[test]
421 fn query_error_with_code_display() {
422 let e = BsqlError::Query(QueryError {
423 message: Cow::Borrowed("duplicate key"),
424 pg_code: Some(Box::from("23505")),
425 source: None,
426 });
427 assert_eq!(e.to_string(), "query error: [23505] duplicate key");
428 }
429
430 #[test]
431 fn query_error_without_code_display() {
432 let e = QueryError::row_count("exactly 1 row", 0);
433 assert_eq!(
434 e.to_string(),
435 "query error: expected exactly 1 row, got 0 rows"
436 );
437 }
438
439 #[test]
440 fn decode_error_display() {
441 let e = BsqlError::Decode(DecodeError {
442 column: Cow::Borrowed("age"),
443 expected: "i32",
444 actual: Cow::Borrowed("text"),
445 source: None,
446 });
447 assert_eq!(
448 e.to_string(),
449 "decode error: column \"age\": expected i32, got text"
450 );
451 }
452
453 #[test]
454 fn connect_error_display() {
455 let e = ConnectError::create("connection refused");
456 assert_eq!(e.to_string(), "connect error: connection refused");
457 }
458
459 #[test]
460 fn pool_exhausted_uses_borrowed_cow() {
461 let e = PoolError::exhausted();
462 match e {
463 BsqlError::Pool(ref pe) => {
464 assert!(
465 matches!(pe.message, Cow::Borrowed(_)),
466 "exhausted() should use Cow::Borrowed for zero-alloc"
467 );
468 }
469 _ => panic!("expected Pool variant"),
470 }
471 }
472
473 #[test]
474 fn connect_error_uses_owned_cow() {
475 let e = ConnectError::create("dynamic message");
476 match e {
477 BsqlError::Connect(ref ce) => {
478 assert!(
479 matches!(ce.message, Cow::Owned(_)),
480 "create() with dynamic msg should use Cow::Owned"
481 );
482 }
483 _ => panic!("expected Connect variant"),
484 }
485 }
486
487 #[test]
488 fn query_row_count_uses_owned_cow() {
489 let e = QueryError::row_count("exactly 1 row", 5);
490 match e {
491 BsqlError::Query(ref qe) => {
492 assert!(
493 matches!(qe.message, Cow::Owned(_)),
494 "row_count() with formatted msg should use Cow::Owned"
495 );
496 }
497 _ => panic!("expected Query variant"),
498 }
499 }
500
501 #[test]
502 fn pool_error_source_chain() {
503 let e = PoolError::exhausted();
504 assert!(e.source().is_none());
506 }
507
508 #[test]
509 fn connect_error_with_source_chain() {
510 let inner = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
511 let e = ConnectError::with_source("connection failed", inner);
512 assert!(e.source().is_some());
513 }
514
515 #[test]
516 fn server_error_preserves_detail_and_hint() {
517 let driver_err = bsql_driver_postgres::DriverError::Server {
518 code: "23505".into(),
519 message: "duplicate key".into(),
520 detail: Some("Key (login)=(alice) already exists.".into()),
521 hint: Some("Use ON CONFLICT to handle duplicates.".into()),
522 position: None,
523 };
524 let e = BsqlError::from(driver_err);
525 let display = e.to_string();
526 assert!(
527 display.contains("duplicate key"),
528 "missing message: {display}"
529 );
530 assert!(
531 display.contains("detail: Key (login)=(alice) already exists."),
532 "missing detail: {display}"
533 );
534 assert!(
535 display.contains("hint: Use ON CONFLICT to handle duplicates."),
536 "missing hint: {display}"
537 );
538 match &e {
540 BsqlError::Query(qe) => assert_eq!(qe.pg_code.as_deref(), Some("23505")),
541 other => panic!("expected Query, got: {other:?}"),
542 }
543 }
544
545 #[test]
546 fn server_error_without_detail_hint() {
547 let driver_err = bsql_driver_postgres::DriverError::Server {
548 code: "42P01".into(),
549 message: "relation does not exist".into(),
550 detail: None,
551 hint: None,
552 position: None,
553 };
554 let e = BsqlError::from(driver_err);
555 let display = e.to_string();
556 assert!(
557 display.contains("relation does not exist"),
558 "missing message: {display}"
559 );
560 assert!(
561 !display.contains("detail:"),
562 "should not contain detail: {display}"
563 );
564 assert!(
565 !display.contains("hint:"),
566 "should not contain hint: {display}"
567 );
568 }
569
570 #[test]
571 fn decode_error_has_no_source() {
572 let e = BsqlError::Decode(DecodeError {
573 column: Cow::Borrowed("col"),
574 expected: "i32",
575 actual: Cow::Borrowed("text"),
576 source: None,
577 });
578 assert!(e.source().is_none());
579 }
580
581 #[test]
582 fn decode_error_with_source_chain() {
583 let inner = std::io::Error::new(std::io::ErrorKind::InvalidData, "bad utf-8");
584 let e = DecodeError::with_source("name", "String", "invalid bytes", inner);
585 assert!(e.source().is_some());
586 match &e {
587 BsqlError::Decode(d) => {
588 assert_eq!(d.column, "name");
589 assert_eq!(d.expected, "String");
590 }
591 other => panic!("expected Decode, got: {other:?}"),
592 }
593 }
594
595 #[test]
596 fn is_timeout_true_for_57014() {
597 let e = BsqlError::Query(QueryError {
598 message: Cow::Borrowed("canceling statement due to statement timeout"),
599 pg_code: Some(Box::from("57014")),
600 source: None,
601 });
602 assert!(e.is_timeout());
603 }
604
605 #[test]
606 fn is_timeout_false_for_other_codes() {
607 let e = BsqlError::Query(QueryError {
608 message: Cow::Borrowed("unique violation"),
609 pg_code: Some(Box::from("23505")),
610 source: None,
611 });
612 assert!(!e.is_timeout());
613 }
614
615 #[test]
616 fn is_timeout_false_for_non_query() {
617 let e = PoolError::exhausted();
618 assert!(!e.is_timeout());
619 }
620
621 #[test]
622 fn is_serialization_failure_true_for_40001() {
623 let e = BsqlError::Query(QueryError {
624 message: Cow::Borrowed("could not serialize access"),
625 pg_code: Some(Box::from("40001")),
626 source: None,
627 });
628 assert!(e.is_serialization_failure());
629 }
630
631 #[test]
632 fn is_serialization_failure_false_for_other_codes() {
633 let e = BsqlError::Query(QueryError {
634 message: Cow::Borrowed("timeout"),
635 pg_code: Some(Box::from("57014")),
636 source: None,
637 });
638 assert!(!e.is_serialization_failure());
639 }
640
641 #[test]
642 fn from_driver_query_maps_io_to_query() {
643 let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe broke");
644 let e = BsqlError::from_driver_query(bsql_driver_postgres::DriverError::Io(io_err));
645 match &e {
646 BsqlError::Query(q) => {
647 assert!(q.message.contains("I/O error during query"));
648 assert!(q.source.is_some());
649 }
650 other => panic!("expected Query, got: {other:?}"),
651 }
652 }
653
654 #[test]
655 fn from_driver_query_non_io_delegates_to_from() {
656 let e =
657 BsqlError::from_driver_query(bsql_driver_postgres::DriverError::Pool("test".into()));
658 assert!(matches!(e, BsqlError::Pool(_)));
659 }
660
661 #[test]
664 fn is_unique_violation_true_for_23505() {
665 let e = BsqlError::Query(QueryError {
666 message: Cow::Borrowed("duplicate key value violates unique constraint"),
667 pg_code: Some(Box::from("23505")),
668 source: None,
669 });
670 assert!(e.is_unique_violation());
671 }
672
673 #[test]
674 fn is_unique_violation_false_for_other_codes() {
675 let e = BsqlError::Query(QueryError {
676 message: Cow::Borrowed("timeout"),
677 pg_code: Some(Box::from("57014")),
678 source: None,
679 });
680 assert!(!e.is_unique_violation());
681 }
682
683 #[test]
684 fn is_unique_violation_false_for_non_query() {
685 let e = PoolError::exhausted();
686 assert!(!e.is_unique_violation());
687 }
688
689 #[test]
692 fn is_foreign_key_violation_true_for_23503() {
693 let e = BsqlError::Query(QueryError {
694 message: Cow::Borrowed("insert or update violates foreign key constraint"),
695 pg_code: Some(Box::from("23503")),
696 source: None,
697 });
698 assert!(e.is_foreign_key_violation());
699 }
700
701 #[test]
702 fn is_foreign_key_violation_false_for_other_codes() {
703 let e = BsqlError::Query(QueryError {
704 message: Cow::Borrowed("unique"),
705 pg_code: Some(Box::from("23505")),
706 source: None,
707 });
708 assert!(!e.is_foreign_key_violation());
709 }
710
711 #[test]
712 fn is_foreign_key_violation_false_for_non_query() {
713 let e = ConnectError::create("down");
714 assert!(!e.is_foreign_key_violation());
715 }
716
717 #[test]
720 fn is_not_null_violation_true_for_23502() {
721 let e = BsqlError::Query(QueryError {
722 message: Cow::Borrowed("null value in column \"name\" violates not-null constraint"),
723 pg_code: Some(Box::from("23502")),
724 source: None,
725 });
726 assert!(e.is_not_null_violation());
727 }
728
729 #[test]
730 fn is_not_null_violation_false_for_other_codes() {
731 let e = BsqlError::Query(QueryError {
732 message: Cow::Borrowed("unique"),
733 pg_code: Some(Box::from("23505")),
734 source: None,
735 });
736 assert!(!e.is_not_null_violation());
737 }
738
739 #[test]
742 fn is_check_violation_true_for_23514() {
743 let e = BsqlError::Query(QueryError {
744 message: Cow::Borrowed("new row violates check constraint"),
745 pg_code: Some(Box::from("23514")),
746 source: None,
747 });
748 assert!(e.is_check_violation());
749 }
750
751 #[test]
752 fn is_check_violation_false_for_other_codes() {
753 let e = BsqlError::Query(QueryError {
754 message: Cow::Borrowed("unique"),
755 pg_code: Some(Box::from("23505")),
756 source: None,
757 });
758 assert!(!e.is_check_violation());
759 }
760
761 #[test]
764 fn is_deadlock_true_for_40p01() {
765 let e = BsqlError::Query(QueryError {
766 message: Cow::Borrowed("deadlock detected"),
767 pg_code: Some(Box::from("40P01")),
768 source: None,
769 });
770 assert!(e.is_deadlock());
771 }
772
773 #[test]
774 fn is_deadlock_false_for_other_codes() {
775 let e = BsqlError::Query(QueryError {
776 message: Cow::Borrowed("serialization"),
777 pg_code: Some(Box::from("40001")),
778 source: None,
779 });
780 assert!(!e.is_deadlock());
781 }
782
783 #[test]
784 fn is_deadlock_false_for_non_query() {
785 let e = PoolError::exhausted();
786 assert!(!e.is_deadlock());
787 }
788
789 #[test]
792 fn pg_code_returns_code_for_query_error() {
793 let e = BsqlError::Query(QueryError {
794 message: Cow::Borrowed("duplicate key"),
795 pg_code: Some(Box::from("23505")),
796 source: None,
797 });
798 assert_eq!(e.pg_code(), Some("23505"));
799 }
800
801 #[test]
802 fn pg_code_returns_none_for_query_without_code() {
803 let e = BsqlError::Query(QueryError {
804 message: Cow::Borrowed("I/O error"),
805 pg_code: None,
806 source: None,
807 });
808 assert_eq!(e.pg_code(), None);
809 }
810
811 #[test]
812 fn pg_code_returns_none_for_pool_error() {
813 let e = PoolError::exhausted();
814 assert_eq!(e.pg_code(), None);
815 }
816
817 #[test]
818 fn pg_code_returns_none_for_connect_error() {
819 let e = ConnectError::create("refused");
820 assert_eq!(e.pg_code(), None);
821 }
822
823 #[test]
824 fn pg_code_returns_none_for_decode_error() {
825 let e = BsqlError::Decode(DecodeError {
826 column: Cow::Borrowed("col"),
827 expected: "i32",
828 actual: Cow::Borrowed("text"),
829 source: None,
830 });
831 assert_eq!(e.pg_code(), None);
832 }
833
834 #[test]
837 fn server_error_with_position_display() {
838 let driver_err = bsql_driver_postgres::DriverError::Server {
839 code: "42601".into(),
840 message: "syntax error".into(),
841 detail: None,
842 hint: None,
843 position: Some(8),
844 };
845 let e = BsqlError::from(driver_err);
846 let display = e.to_string();
847 assert!(
848 display.contains("at position 8"),
849 "should contain position: {display}"
850 );
851 assert!(
852 display.contains("syntax error"),
853 "should contain message: {display}"
854 );
855 }
856
857 #[test]
858 fn server_error_with_all_fields() {
859 let driver_err = bsql_driver_postgres::DriverError::Server {
860 code: "42P01".into(),
861 message: "relation does not exist".into(),
862 detail: Some("table was dropped".into()),
863 hint: Some("recreate the table".into()),
864 position: Some(42),
865 };
866 let e = BsqlError::from(driver_err);
867 let display = e.to_string();
868 assert!(display.contains("at position 42"));
869 assert!(display.contains("detail: table was dropped"));
870 assert!(display.contains("hint: recreate the table"));
871 assert!(display.contains("relation does not exist"));
872 }
873
874 #[test]
877 fn from_driver_query_server_error_delegates() {
878 let e = BsqlError::from_driver_query(bsql_driver_postgres::DriverError::Server {
879 code: "23505".into(),
880 message: "duplicate key".into(),
881 detail: None,
882 hint: None,
883 position: None,
884 });
885 assert!(matches!(e, BsqlError::Query(_)));
886 assert_eq!(e.pg_code(), Some("23505"));
887 }
888
889 #[test]
892 fn from_driver_io_maps_to_connect() {
893 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
894 let e = BsqlError::from(bsql_driver_postgres::DriverError::Io(io_err));
895 assert!(matches!(e, BsqlError::Connect(_)));
896 assert!(e.source().is_some());
897 }
898
899 #[test]
900 fn from_driver_auth_maps_to_connect() {
901 let e = BsqlError::from(bsql_driver_postgres::DriverError::Auth(
902 "wrong password".into(),
903 ));
904 match &e {
905 BsqlError::Connect(ce) => {
906 assert!(ce.message.contains("wrong password"));
907 }
908 other => panic!("expected Connect, got: {other:?}"),
909 }
910 }
911
912 #[test]
913 fn from_driver_protocol_maps_to_query() {
914 let e = BsqlError::from(bsql_driver_postgres::DriverError::Protocol(
915 "unexpected message".into(),
916 ));
917 match &e {
918 BsqlError::Query(qe) => {
919 assert!(qe.message.contains("unexpected message"));
920 assert!(qe.pg_code.is_none());
921 }
922 other => panic!("expected Query, got: {other:?}"),
923 }
924 }
925
926 #[cfg(feature = "sqlite")]
929 mod sqlite_tests {
930 use super::*;
931
932 #[test]
933 fn from_sqlite_sqlite_error() {
934 let e = BsqlError::from_sqlite(bsql_driver_sqlite::SqliteError::Sqlite {
935 code: 19,
936 message: "UNIQUE constraint failed".into(),
937 });
938 match &e {
939 BsqlError::Query(qe) => {
940 assert!(qe.message.contains("SQLite error [19]"));
941 assert!(qe.message.contains("UNIQUE constraint failed"));
942 assert!(qe.pg_code.is_none());
943 }
944 other => panic!("expected Query, got: {other:?}"),
945 }
946 }
947
948 #[test]
949 fn from_sqlite_io_error() {
950 let io_err =
951 std::io::Error::new(std::io::ErrorKind::PermissionDenied, "read-only filesystem");
952 let e = BsqlError::from_sqlite(bsql_driver_sqlite::SqliteError::Io(io_err));
953 match &e {
954 BsqlError::Connect(ce) => {
955 assert!(ce.message.contains("SQLite I/O error"));
956 assert!(ce.message.contains("read-only filesystem"));
957 assert!(ce.source.is_some());
958 }
959 other => panic!("expected Connect, got: {other:?}"),
960 }
961 }
962
963 #[test]
964 fn from_sqlite_internal_error() {
965 let e = BsqlError::from_sqlite(bsql_driver_sqlite::SqliteError::Internal(
966 "corrupted database".into(),
967 ));
968 match &e {
969 BsqlError::Query(qe) => {
970 assert!(qe.message.contains("SQLite internal error"));
971 assert!(qe.message.contains("corrupted database"));
972 }
973 other => panic!("expected Query, got: {other:?}"),
974 }
975 }
976
977 #[test]
978 fn from_sqlite_pool_error() {
979 let e = BsqlError::from_sqlite(bsql_driver_sqlite::SqliteError::Pool(
980 "no readers available".into(),
981 ));
982 match &e {
983 BsqlError::Pool(pe) => {
984 assert!(pe.message.contains("SQLite pool error"));
985 assert!(pe.message.contains("no readers available"));
986 }
987 other => panic!("expected Pool, got: {other:?}"),
988 }
989 }
990 }
991
992 fn _assert_send<T: Send>() {}
995 fn _assert_sync<T: Sync>() {}
996
997 #[test]
998 fn bsql_error_is_send() {
999 _assert_send::<BsqlError>();
1000 }
1001
1002 #[test]
1003 fn bsql_error_is_sync() {
1004 _assert_sync::<BsqlError>();
1005 }
1006
1007 #[test]
1010 fn bsql_error_display_connect_includes_message() {
1011 let e = ConnectError::create("tcp connection refused at 127.0.0.1:5432");
1012 let display = e.to_string();
1013 assert!(
1014 display.contains("connect error:"),
1015 "should start with 'connect error:': {display}"
1016 );
1017 assert!(
1018 display.contains("tcp connection refused at 127.0.0.1:5432"),
1019 "should include the message: {display}"
1020 );
1021 }
1022
1023 #[test]
1026 fn bsql_error_display_query_includes_pg_code() {
1027 let e = BsqlError::Query(QueryError {
1028 message: Cow::Borrowed("relation \"users\" does not exist"),
1029 pg_code: Some(Box::from("42P01")),
1030 source: None,
1031 });
1032 let display = e.to_string();
1033 assert!(
1034 display.contains("42P01"),
1035 "should include pg_code: {display}"
1036 );
1037 assert!(
1038 display.contains("relation \"users\" does not exist"),
1039 "should include message: {display}"
1040 );
1041 }
1042
1043 #[test]
1046 fn bsql_error_display_query_no_code() {
1047 let e = BsqlError::Query(QueryError {
1048 message: Cow::Borrowed("I/O error during query"),
1049 pg_code: None,
1050 source: None,
1051 });
1052 let display = e.to_string();
1053 assert!(
1054 display.contains("I/O error during query"),
1055 "should include message: {display}"
1056 );
1057 assert!(
1058 !display.contains('['),
1059 "should not contain brackets without code: {display}"
1060 );
1061 }
1062
1063 #[test]
1066 fn query_error_row_count_message() {
1067 let e = QueryError::row_count("exactly 1 row", 5);
1068 let display = e.to_string();
1069 assert!(
1070 display.contains("expected exactly 1 row, got 5 rows"),
1071 "row_count message: {display}"
1072 );
1073 }
1074
1075 #[test]
1076 fn query_error_row_count_zero() {
1077 let e = QueryError::row_count("at least 1 row", 0);
1078 let display = e.to_string();
1079 assert!(
1080 display.contains("expected at least 1 row, got 0 rows"),
1081 "row_count zero: {display}"
1082 );
1083 }
1084
1085 #[test]
1088 fn decode_error_with_source_preserves_fields() {
1089 let inner = std::io::Error::new(std::io::ErrorKind::InvalidData, "bad utf-8");
1090 let e = DecodeError::with_source("email", "String", "bytes", inner);
1091 let display = e.to_string();
1092 assert!(
1093 display.contains("email"),
1094 "should contain column: {display}"
1095 );
1096 assert!(
1097 display.contains("String"),
1098 "should contain expected type: {display}"
1099 );
1100 assert!(
1101 display.contains("bytes"),
1102 "should contain actual type: {display}"
1103 );
1104 }
1105
1106 #[test]
1109 fn pool_error_display_custom_message() {
1110 let e = BsqlError::Pool(PoolError {
1111 message: Cow::Owned("all 10 connections in use".to_owned()),
1112 source: None,
1113 });
1114 let display = e.to_string();
1115 assert!(
1116 display.contains("pool error: all 10 connections in use"),
1117 "pool error display: {display}"
1118 );
1119 }
1120
1121 #[test]
1124 fn connect_error_with_source_display() {
1125 let inner = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
1126 let e = ConnectError::with_source("failed to connect to PG", inner);
1127 let display = e.to_string();
1128 assert!(
1129 display.contains("failed to connect to PG"),
1130 "should include msg: {display}"
1131 );
1132 assert!(e.source().is_some());
1133 }
1134}