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 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(Box::from(std::str::from_utf8(&code).unwrap_or("?????"))),
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 pub fn column_count(expected: usize, actual: usize) -> BsqlError {
410 BsqlError::Decode(DecodeError {
411 column: Cow::Borrowed("*"),
412 expected: "matching column count",
413 actual: Cow::Owned(format!(
414 "expected {} columns but row has {}",
415 expected, actual
416 )),
417 source: None,
418 })
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use std::error::Error as _;
426
427 #[test]
428 fn pool_error_display() {
429 let e = PoolError::exhausted();
430 assert_eq!(
431 e.to_string(),
432 "pool error: pool exhausted: all connections in use"
433 );
434 }
435
436 #[test]
437 fn query_error_with_code_display() {
438 let e = BsqlError::Query(QueryError {
439 message: Cow::Borrowed("duplicate key"),
440 pg_code: Some(Box::from("23505")),
441 source: None,
442 });
443 assert_eq!(e.to_string(), "query error: [23505] duplicate key");
444 }
445
446 #[test]
447 fn query_error_without_code_display() {
448 let e = QueryError::row_count("exactly 1 row", 0);
449 assert_eq!(
450 e.to_string(),
451 "query error: expected exactly 1 row, got 0 rows"
452 );
453 }
454
455 #[test]
456 fn decode_error_display() {
457 let e = BsqlError::Decode(DecodeError {
458 column: Cow::Borrowed("age"),
459 expected: "i32",
460 actual: Cow::Borrowed("text"),
461 source: None,
462 });
463 assert_eq!(
464 e.to_string(),
465 "decode error: column \"age\": expected i32, got text"
466 );
467 }
468
469 #[test]
470 fn connect_error_display() {
471 let e = ConnectError::create("connection refused");
472 assert_eq!(e.to_string(), "connect error: connection refused");
473 }
474
475 #[test]
476 fn pool_exhausted_uses_borrowed_cow() {
477 let e = PoolError::exhausted();
478 match e {
479 BsqlError::Pool(ref pe) => {
480 assert!(
481 matches!(pe.message, Cow::Borrowed(_)),
482 "exhausted() should use Cow::Borrowed for zero-alloc"
483 );
484 }
485 _ => panic!("expected Pool variant"),
486 }
487 }
488
489 #[test]
490 fn connect_error_uses_owned_cow() {
491 let e = ConnectError::create("dynamic message");
492 match e {
493 BsqlError::Connect(ref ce) => {
494 assert!(
495 matches!(ce.message, Cow::Owned(_)),
496 "create() with dynamic msg should use Cow::Owned"
497 );
498 }
499 _ => panic!("expected Connect variant"),
500 }
501 }
502
503 #[test]
504 fn query_row_count_uses_owned_cow() {
505 let e = QueryError::row_count("exactly 1 row", 5);
506 match e {
507 BsqlError::Query(ref qe) => {
508 assert!(
509 matches!(qe.message, Cow::Owned(_)),
510 "row_count() with formatted msg should use Cow::Owned"
511 );
512 }
513 _ => panic!("expected Query variant"),
514 }
515 }
516
517 #[test]
518 fn pool_error_source_chain() {
519 let e = PoolError::exhausted();
520 assert!(e.source().is_none());
522 }
523
524 #[test]
525 fn connect_error_with_source_chain() {
526 let inner = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
527 let e = ConnectError::with_source("connection failed", inner);
528 assert!(e.source().is_some());
529 }
530
531 #[test]
532 fn server_error_preserves_detail_and_hint() {
533 let driver_err = bsql_driver_postgres::DriverError::Server {
534 code: *b"23505",
535 message: "duplicate key".into(),
536 detail: Some("Key (login)=(alice) already exists.".into()),
537 hint: Some("Use ON CONFLICT to handle duplicates.".into()),
538 position: None,
539 };
540 let e = BsqlError::from(driver_err);
541 let display = e.to_string();
542 assert!(
543 display.contains("duplicate key"),
544 "missing message: {display}"
545 );
546 assert!(
547 display.contains("detail: Key (login)=(alice) already exists."),
548 "missing detail: {display}"
549 );
550 assert!(
551 display.contains("hint: Use ON CONFLICT to handle duplicates."),
552 "missing hint: {display}"
553 );
554 match &e {
556 BsqlError::Query(qe) => assert_eq!(qe.pg_code.as_deref(), Some("23505")),
557 other => panic!("expected Query, got: {other:?}"),
558 }
559 }
560
561 #[test]
562 fn server_error_without_detail_hint() {
563 let driver_err = bsql_driver_postgres::DriverError::Server {
564 code: *b"42P01",
565 message: "relation does not exist".into(),
566 detail: None,
567 hint: None,
568 position: None,
569 };
570 let e = BsqlError::from(driver_err);
571 let display = e.to_string();
572 assert!(
573 display.contains("relation does not exist"),
574 "missing message: {display}"
575 );
576 assert!(
577 !display.contains("detail:"),
578 "should not contain detail: {display}"
579 );
580 assert!(
581 !display.contains("hint:"),
582 "should not contain hint: {display}"
583 );
584 }
585
586 #[test]
587 fn decode_error_has_no_source() {
588 let e = BsqlError::Decode(DecodeError {
589 column: Cow::Borrowed("col"),
590 expected: "i32",
591 actual: Cow::Borrowed("text"),
592 source: None,
593 });
594 assert!(e.source().is_none());
595 }
596
597 #[test]
598 fn decode_error_with_source_chain() {
599 let inner = std::io::Error::new(std::io::ErrorKind::InvalidData, "bad utf-8");
600 let e = DecodeError::with_source("name", "String", "invalid bytes", inner);
601 assert!(e.source().is_some());
602 match &e {
603 BsqlError::Decode(d) => {
604 assert_eq!(d.column, "name");
605 assert_eq!(d.expected, "String");
606 }
607 other => panic!("expected Decode, got: {other:?}"),
608 }
609 }
610
611 #[test]
612 fn is_timeout_true_for_57014() {
613 let e = BsqlError::Query(QueryError {
614 message: Cow::Borrowed("canceling statement due to statement timeout"),
615 pg_code: Some(Box::from("57014")),
616 source: None,
617 });
618 assert!(e.is_timeout());
619 }
620
621 #[test]
622 fn is_timeout_false_for_other_codes() {
623 let e = BsqlError::Query(QueryError {
624 message: Cow::Borrowed("unique violation"),
625 pg_code: Some(Box::from("23505")),
626 source: None,
627 });
628 assert!(!e.is_timeout());
629 }
630
631 #[test]
632 fn is_timeout_false_for_non_query() {
633 let e = PoolError::exhausted();
634 assert!(!e.is_timeout());
635 }
636
637 #[test]
638 fn is_serialization_failure_true_for_40001() {
639 let e = BsqlError::Query(QueryError {
640 message: Cow::Borrowed("could not serialize access"),
641 pg_code: Some(Box::from("40001")),
642 source: None,
643 });
644 assert!(e.is_serialization_failure());
645 }
646
647 #[test]
648 fn is_serialization_failure_false_for_other_codes() {
649 let e = BsqlError::Query(QueryError {
650 message: Cow::Borrowed("timeout"),
651 pg_code: Some(Box::from("57014")),
652 source: None,
653 });
654 assert!(!e.is_serialization_failure());
655 }
656
657 #[test]
658 fn from_driver_query_maps_io_to_query() {
659 let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "pipe broke");
660 let e = BsqlError::from_driver_query(bsql_driver_postgres::DriverError::Io(io_err));
661 match &e {
662 BsqlError::Query(q) => {
663 assert!(q.message.contains("I/O error during query"));
664 assert!(q.source.is_some());
665 }
666 other => panic!("expected Query, got: {other:?}"),
667 }
668 }
669
670 #[test]
671 fn from_driver_query_non_io_delegates_to_from() {
672 let e =
673 BsqlError::from_driver_query(bsql_driver_postgres::DriverError::Pool("test".into()));
674 assert!(matches!(e, BsqlError::Pool(_)));
675 }
676
677 #[test]
680 fn is_unique_violation_true_for_23505() {
681 let e = BsqlError::Query(QueryError {
682 message: Cow::Borrowed("duplicate key value violates unique constraint"),
683 pg_code: Some(Box::from("23505")),
684 source: None,
685 });
686 assert!(e.is_unique_violation());
687 }
688
689 #[test]
690 fn is_unique_violation_false_for_other_codes() {
691 let e = BsqlError::Query(QueryError {
692 message: Cow::Borrowed("timeout"),
693 pg_code: Some(Box::from("57014")),
694 source: None,
695 });
696 assert!(!e.is_unique_violation());
697 }
698
699 #[test]
700 fn is_unique_violation_false_for_non_query() {
701 let e = PoolError::exhausted();
702 assert!(!e.is_unique_violation());
703 }
704
705 #[test]
708 fn is_foreign_key_violation_true_for_23503() {
709 let e = BsqlError::Query(QueryError {
710 message: Cow::Borrowed("insert or update violates foreign key constraint"),
711 pg_code: Some(Box::from("23503")),
712 source: None,
713 });
714 assert!(e.is_foreign_key_violation());
715 }
716
717 #[test]
718 fn is_foreign_key_violation_false_for_other_codes() {
719 let e = BsqlError::Query(QueryError {
720 message: Cow::Borrowed("unique"),
721 pg_code: Some(Box::from("23505")),
722 source: None,
723 });
724 assert!(!e.is_foreign_key_violation());
725 }
726
727 #[test]
728 fn is_foreign_key_violation_false_for_non_query() {
729 let e = ConnectError::create("down");
730 assert!(!e.is_foreign_key_violation());
731 }
732
733 #[test]
736 fn is_not_null_violation_true_for_23502() {
737 let e = BsqlError::Query(QueryError {
738 message: Cow::Borrowed("null value in column \"name\" violates not-null constraint"),
739 pg_code: Some(Box::from("23502")),
740 source: None,
741 });
742 assert!(e.is_not_null_violation());
743 }
744
745 #[test]
746 fn is_not_null_violation_false_for_other_codes() {
747 let e = BsqlError::Query(QueryError {
748 message: Cow::Borrowed("unique"),
749 pg_code: Some(Box::from("23505")),
750 source: None,
751 });
752 assert!(!e.is_not_null_violation());
753 }
754
755 #[test]
758 fn is_check_violation_true_for_23514() {
759 let e = BsqlError::Query(QueryError {
760 message: Cow::Borrowed("new row violates check constraint"),
761 pg_code: Some(Box::from("23514")),
762 source: None,
763 });
764 assert!(e.is_check_violation());
765 }
766
767 #[test]
768 fn is_check_violation_false_for_other_codes() {
769 let e = BsqlError::Query(QueryError {
770 message: Cow::Borrowed("unique"),
771 pg_code: Some(Box::from("23505")),
772 source: None,
773 });
774 assert!(!e.is_check_violation());
775 }
776
777 #[test]
780 fn is_deadlock_true_for_40p01() {
781 let e = BsqlError::Query(QueryError {
782 message: Cow::Borrowed("deadlock detected"),
783 pg_code: Some(Box::from("40P01")),
784 source: None,
785 });
786 assert!(e.is_deadlock());
787 }
788
789 #[test]
790 fn is_deadlock_false_for_other_codes() {
791 let e = BsqlError::Query(QueryError {
792 message: Cow::Borrowed("serialization"),
793 pg_code: Some(Box::from("40001")),
794 source: None,
795 });
796 assert!(!e.is_deadlock());
797 }
798
799 #[test]
800 fn is_deadlock_false_for_non_query() {
801 let e = PoolError::exhausted();
802 assert!(!e.is_deadlock());
803 }
804
805 #[test]
808 fn pg_code_returns_code_for_query_error() {
809 let e = BsqlError::Query(QueryError {
810 message: Cow::Borrowed("duplicate key"),
811 pg_code: Some(Box::from("23505")),
812 source: None,
813 });
814 assert_eq!(e.pg_code(), Some("23505"));
815 }
816
817 #[test]
818 fn pg_code_returns_none_for_query_without_code() {
819 let e = BsqlError::Query(QueryError {
820 message: Cow::Borrowed("I/O error"),
821 pg_code: None,
822 source: None,
823 });
824 assert_eq!(e.pg_code(), None);
825 }
826
827 #[test]
828 fn pg_code_returns_none_for_pool_error() {
829 let e = PoolError::exhausted();
830 assert_eq!(e.pg_code(), None);
831 }
832
833 #[test]
834 fn pg_code_returns_none_for_connect_error() {
835 let e = ConnectError::create("refused");
836 assert_eq!(e.pg_code(), None);
837 }
838
839 #[test]
840 fn pg_code_returns_none_for_decode_error() {
841 let e = BsqlError::Decode(DecodeError {
842 column: Cow::Borrowed("col"),
843 expected: "i32",
844 actual: Cow::Borrowed("text"),
845 source: None,
846 });
847 assert_eq!(e.pg_code(), None);
848 }
849
850 #[test]
853 fn server_error_with_position_display() {
854 let driver_err = bsql_driver_postgres::DriverError::Server {
855 code: *b"42601",
856 message: "syntax error".into(),
857 detail: None,
858 hint: None,
859 position: Some(8),
860 };
861 let e = BsqlError::from(driver_err);
862 let display = e.to_string();
863 assert!(
864 display.contains("at position 8"),
865 "should contain position: {display}"
866 );
867 assert!(
868 display.contains("syntax error"),
869 "should contain message: {display}"
870 );
871 }
872
873 #[test]
874 fn server_error_with_all_fields() {
875 let driver_err = bsql_driver_postgres::DriverError::Server {
876 code: *b"42P01",
877 message: "relation does not exist".into(),
878 detail: Some("table was dropped".into()),
879 hint: Some("recreate the table".into()),
880 position: Some(42),
881 };
882 let e = BsqlError::from(driver_err);
883 let display = e.to_string();
884 assert!(display.contains("at position 42"));
885 assert!(display.contains("detail: table was dropped"));
886 assert!(display.contains("hint: recreate the table"));
887 assert!(display.contains("relation does not exist"));
888 }
889
890 #[test]
893 fn from_driver_query_server_error_delegates() {
894 let e = BsqlError::from_driver_query(bsql_driver_postgres::DriverError::Server {
895 code: *b"23505",
896 message: "duplicate key".into(),
897 detail: None,
898 hint: None,
899 position: None,
900 });
901 assert!(matches!(e, BsqlError::Query(_)));
902 assert_eq!(e.pg_code(), Some("23505"));
903 }
904
905 #[test]
908 fn from_driver_io_maps_to_connect() {
909 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
910 let e = BsqlError::from(bsql_driver_postgres::DriverError::Io(io_err));
911 assert!(matches!(e, BsqlError::Connect(_)));
912 assert!(e.source().is_some());
913 }
914
915 #[test]
916 fn from_driver_auth_maps_to_connect() {
917 let e = BsqlError::from(bsql_driver_postgres::DriverError::Auth(
918 "wrong password".into(),
919 ));
920 match &e {
921 BsqlError::Connect(ce) => {
922 assert!(ce.message.contains("wrong password"));
923 }
924 other => panic!("expected Connect, got: {other:?}"),
925 }
926 }
927
928 #[test]
929 fn from_driver_protocol_maps_to_query() {
930 let e = BsqlError::from(bsql_driver_postgres::DriverError::Protocol(
931 "unexpected message".into(),
932 ));
933 match &e {
934 BsqlError::Query(qe) => {
935 assert!(qe.message.contains("unexpected message"));
936 assert!(qe.pg_code.is_none());
937 }
938 other => panic!("expected Query, got: {other:?}"),
939 }
940 }
941
942 #[cfg(feature = "sqlite")]
945 mod sqlite_tests {
946 use super::*;
947
948 #[test]
949 fn from_sqlite_sqlite_error() {
950 let e = BsqlError::from_sqlite(bsql_driver_sqlite::SqliteError::Sqlite {
951 code: 19,
952 message: "UNIQUE constraint failed".into(),
953 });
954 match &e {
955 BsqlError::Query(qe) => {
956 assert!(qe.message.contains("SQLite error [19]"));
957 assert!(qe.message.contains("UNIQUE constraint failed"));
958 assert!(qe.pg_code.is_none());
959 }
960 other => panic!("expected Query, got: {other:?}"),
961 }
962 }
963
964 #[test]
965 fn from_sqlite_io_error() {
966 let io_err =
967 std::io::Error::new(std::io::ErrorKind::PermissionDenied, "read-only filesystem");
968 let e = BsqlError::from_sqlite(bsql_driver_sqlite::SqliteError::Io(io_err));
969 match &e {
970 BsqlError::Connect(ce) => {
971 assert!(ce.message.contains("SQLite I/O error"));
972 assert!(ce.message.contains("read-only filesystem"));
973 assert!(ce.source.is_some());
974 }
975 other => panic!("expected Connect, got: {other:?}"),
976 }
977 }
978
979 #[test]
980 fn from_sqlite_internal_error() {
981 let e = BsqlError::from_sqlite(bsql_driver_sqlite::SqliteError::Internal(
982 "corrupted database".into(),
983 ));
984 match &e {
985 BsqlError::Query(qe) => {
986 assert!(qe.message.contains("SQLite internal error"));
987 assert!(qe.message.contains("corrupted database"));
988 }
989 other => panic!("expected Query, got: {other:?}"),
990 }
991 }
992
993 #[test]
994 fn from_sqlite_pool_error() {
995 let e = BsqlError::from_sqlite(bsql_driver_sqlite::SqliteError::Pool(
996 "no readers available".into(),
997 ));
998 match &e {
999 BsqlError::Pool(pe) => {
1000 assert!(pe.message.contains("SQLite pool error"));
1001 assert!(pe.message.contains("no readers available"));
1002 }
1003 other => panic!("expected Pool, got: {other:?}"),
1004 }
1005 }
1006 }
1007
1008 fn _assert_send<T: Send>() {}
1011 fn _assert_sync<T: Sync>() {}
1012
1013 #[test]
1014 fn bsql_error_is_send() {
1015 _assert_send::<BsqlError>();
1016 }
1017
1018 #[test]
1019 fn bsql_error_is_sync() {
1020 _assert_sync::<BsqlError>();
1021 }
1022
1023 #[test]
1026 fn bsql_error_display_connect_includes_message() {
1027 let e = ConnectError::create("tcp connection refused at 127.0.0.1:5432");
1028 let display = e.to_string();
1029 assert!(
1030 display.contains("connect error:"),
1031 "should start with 'connect error:': {display}"
1032 );
1033 assert!(
1034 display.contains("tcp connection refused at 127.0.0.1:5432"),
1035 "should include the message: {display}"
1036 );
1037 }
1038
1039 #[test]
1042 fn bsql_error_display_query_includes_pg_code() {
1043 let e = BsqlError::Query(QueryError {
1044 message: Cow::Borrowed("relation \"users\" does not exist"),
1045 pg_code: Some(Box::from("42P01")),
1046 source: None,
1047 });
1048 let display = e.to_string();
1049 assert!(
1050 display.contains("42P01"),
1051 "should include pg_code: {display}"
1052 );
1053 assert!(
1054 display.contains("relation \"users\" does not exist"),
1055 "should include message: {display}"
1056 );
1057 }
1058
1059 #[test]
1062 fn bsql_error_display_query_no_code() {
1063 let e = BsqlError::Query(QueryError {
1064 message: Cow::Borrowed("I/O error during query"),
1065 pg_code: None,
1066 source: None,
1067 });
1068 let display = e.to_string();
1069 assert!(
1070 display.contains("I/O error during query"),
1071 "should include message: {display}"
1072 );
1073 assert!(
1074 !display.contains('['),
1075 "should not contain brackets without code: {display}"
1076 );
1077 }
1078
1079 #[test]
1082 fn query_error_row_count_message() {
1083 let e = QueryError::row_count("exactly 1 row", 5);
1084 let display = e.to_string();
1085 assert!(
1086 display.contains("expected exactly 1 row, got 5 rows"),
1087 "row_count message: {display}"
1088 );
1089 }
1090
1091 #[test]
1092 fn query_error_row_count_zero() {
1093 let e = QueryError::row_count("at least 1 row", 0);
1094 let display = e.to_string();
1095 assert!(
1096 display.contains("expected at least 1 row, got 0 rows"),
1097 "row_count zero: {display}"
1098 );
1099 }
1100
1101 #[test]
1104 fn decode_error_with_source_preserves_fields() {
1105 let inner = std::io::Error::new(std::io::ErrorKind::InvalidData, "bad utf-8");
1106 let e = DecodeError::with_source("email", "String", "bytes", inner);
1107 let display = e.to_string();
1108 assert!(
1109 display.contains("email"),
1110 "should contain column: {display}"
1111 );
1112 assert!(
1113 display.contains("String"),
1114 "should contain expected type: {display}"
1115 );
1116 assert!(
1117 display.contains("bytes"),
1118 "should contain actual type: {display}"
1119 );
1120 }
1121
1122 #[test]
1125 fn pool_error_display_custom_message() {
1126 let e = BsqlError::Pool(PoolError {
1127 message: Cow::Owned("all 10 connections in use".to_owned()),
1128 source: None,
1129 });
1130 let display = e.to_string();
1131 assert!(
1132 display.contains("pool error: all 10 connections in use"),
1133 "pool error display: {display}"
1134 );
1135 }
1136
1137 #[test]
1140 fn connect_error_with_source_display() {
1141 let inner = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
1142 let e = ConnectError::with_source("failed to connect to PG", inner);
1143 let display = e.to_string();
1144 assert!(
1145 display.contains("failed to connect to PG"),
1146 "should include msg: {display}"
1147 );
1148 assert!(e.source().is_some());
1149 }
1150
1151 #[test]
1152 fn decode_error_column_count_mismatch() {
1153 let e: BsqlError = DecodeError::column_count(3, 2);
1155 let display = e.to_string();
1156 assert!(
1157 display.contains("expected 3 columns but row has 2"),
1158 "should describe column count mismatch: {display}"
1159 );
1160 assert!(matches!(e, BsqlError::Decode(_)));
1162 assert!(e.source().is_none());
1164 }
1165
1166 #[test]
1167 fn decode_error_column_count_5_vs_3() {
1168 let e: BsqlError = DecodeError::column_count(5, 3);
1169 let display = e.to_string();
1170 assert!(
1171 display.contains("expected 5 columns but row has 3"),
1172 "should describe 5 vs 3 mismatch: {display}"
1173 );
1174 match &e {
1176 BsqlError::Decode(d) => {
1177 assert_eq!(d.column, "*", "column_count should use '*' for column");
1178 assert_eq!(d.expected, "matching column count");
1179 }
1180 other => panic!("expected Decode, got: {other:?}"),
1181 }
1182 }
1183
1184 #[test]
1185 fn decode_error_column_count_zero_zero() {
1186 let e: BsqlError = DecodeError::column_count(0, 0);
1188 let display = e.to_string();
1189 assert!(
1190 display.contains("expected 0 columns but row has 0"),
1191 "should handle 0/0 edge case: {display}"
1192 );
1193 assert!(matches!(e, BsqlError::Decode(_)));
1194 assert!(e.source().is_none());
1195 }
1196
1197 #[test]
1200 fn is_timeout_false_for_connect() {
1201 let e = ConnectError::create("refused");
1202 assert!(!e.is_timeout());
1203 }
1204
1205 #[test]
1206 fn is_timeout_false_for_decode() {
1207 let e = BsqlError::Decode(DecodeError {
1208 column: Cow::Borrowed("x"),
1209 expected: "i32",
1210 actual: Cow::Borrowed("text"),
1211 source: None,
1212 });
1213 assert!(!e.is_timeout());
1214 }
1215
1216 #[test]
1217 fn is_timeout_false_for_query_without_code() {
1218 let e = BsqlError::Query(QueryError {
1219 message: Cow::Borrowed("some error"),
1220 pg_code: None,
1221 source: None,
1222 });
1223 assert!(!e.is_timeout());
1224 }
1225
1226 #[test]
1229 fn is_serialization_failure_false_for_non_query_pool() {
1230 let e = PoolError::exhausted();
1231 assert!(!e.is_serialization_failure());
1232 }
1233
1234 #[test]
1235 fn is_serialization_failure_false_for_connect() {
1236 let e = ConnectError::create("down");
1237 assert!(!e.is_serialization_failure());
1238 }
1239
1240 #[test]
1241 fn is_serialization_failure_false_for_decode() {
1242 let e = BsqlError::Decode(DecodeError {
1243 column: Cow::Borrowed("x"),
1244 expected: "i32",
1245 actual: Cow::Borrowed("text"),
1246 source: None,
1247 });
1248 assert!(!e.is_serialization_failure());
1249 }
1250
1251 #[test]
1254 fn is_not_null_violation_false_for_pool() {
1255 let e = PoolError::exhausted();
1256 assert!(!e.is_not_null_violation());
1257 }
1258
1259 #[test]
1260 fn is_not_null_violation_false_for_connect() {
1261 let e = ConnectError::create("down");
1262 assert!(!e.is_not_null_violation());
1263 }
1264
1265 #[test]
1266 fn is_not_null_violation_false_for_decode() {
1267 let e = BsqlError::Decode(DecodeError {
1268 column: Cow::Borrowed("x"),
1269 expected: "i32",
1270 actual: Cow::Borrowed("text"),
1271 source: None,
1272 });
1273 assert!(!e.is_not_null_violation());
1274 }
1275
1276 #[test]
1279 fn is_check_violation_false_for_pool() {
1280 let e = PoolError::exhausted();
1281 assert!(!e.is_check_violation());
1282 }
1283
1284 #[test]
1285 fn is_check_violation_false_for_connect() {
1286 let e = ConnectError::create("down");
1287 assert!(!e.is_check_violation());
1288 }
1289
1290 #[test]
1291 fn is_check_violation_false_for_decode() {
1292 let e = BsqlError::Decode(DecodeError {
1293 column: Cow::Borrowed("x"),
1294 expected: "i32",
1295 actual: Cow::Borrowed("text"),
1296 source: None,
1297 });
1298 assert!(!e.is_check_violation());
1299 }
1300
1301 #[test]
1304 fn is_foreign_key_violation_false_for_decode() {
1305 let e = BsqlError::Decode(DecodeError {
1306 column: Cow::Borrowed("x"),
1307 expected: "i32",
1308 actual: Cow::Borrowed("text"),
1309 source: None,
1310 });
1311 assert!(!e.is_foreign_key_violation());
1312 }
1313
1314 #[test]
1317 fn is_deadlock_false_for_connect() {
1318 let e = ConnectError::create("down");
1319 assert!(!e.is_deadlock());
1320 }
1321
1322 #[test]
1323 fn is_deadlock_false_for_decode() {
1324 let e = BsqlError::Decode(DecodeError {
1325 column: Cow::Borrowed("x"),
1326 expected: "i32",
1327 actual: Cow::Borrowed("text"),
1328 source: None,
1329 });
1330 assert!(!e.is_deadlock());
1331 }
1332
1333 #[test]
1336 fn is_unique_violation_false_for_connect() {
1337 let e = ConnectError::create("down");
1338 assert!(!e.is_unique_violation());
1339 }
1340
1341 #[test]
1342 fn is_unique_violation_false_for_decode() {
1343 let e = BsqlError::Decode(DecodeError {
1344 column: Cow::Borrowed("x"),
1345 expected: "i32",
1346 actual: Cow::Borrowed("text"),
1347 source: None,
1348 });
1349 assert!(!e.is_unique_violation());
1350 }
1351
1352 #[test]
1355 fn pg_code_none_when_query_has_no_code() {
1356 let e = BsqlError::Query(QueryError {
1357 message: Cow::Borrowed("io error"),
1358 pg_code: None,
1359 source: None,
1360 });
1361 assert_eq!(e.pg_code(), None);
1362 }
1363
1364 #[test]
1367 fn bsql_error_debug_pool() {
1368 let e = PoolError::exhausted();
1369 let dbg = format!("{e:?}");
1370 assert!(dbg.contains("Pool"), "Pool variant in debug: {dbg}");
1371 }
1372
1373 #[test]
1374 fn bsql_error_debug_query() {
1375 let e = BsqlError::Query(QueryError {
1376 message: Cow::Borrowed("test"),
1377 pg_code: Some(Box::from("23505")),
1378 source: None,
1379 });
1380 let dbg = format!("{e:?}");
1381 assert!(dbg.contains("Query"), "Query variant in debug: {dbg}");
1382 assert!(dbg.contains("23505"), "pg_code in debug: {dbg}");
1383 }
1384
1385 #[test]
1386 fn bsql_error_debug_decode() {
1387 let e = BsqlError::Decode(DecodeError {
1388 column: Cow::Borrowed("col"),
1389 expected: "i32",
1390 actual: Cow::Borrowed("text"),
1391 source: None,
1392 });
1393 let dbg = format!("{e:?}");
1394 assert!(dbg.contains("Decode"), "Decode variant in debug: {dbg}");
1395 }
1396
1397 #[test]
1398 fn bsql_error_debug_connect() {
1399 let e = ConnectError::create("refused");
1400 let dbg = format!("{e:?}");
1401 assert!(dbg.contains("Connect"), "Connect variant in debug: {dbg}");
1402 }
1403
1404 #[test]
1407 fn pool_error_display_starts_with_prefix() {
1408 let e = PoolError::exhausted();
1409 assert!(e.to_string().starts_with("pool error:"));
1410 }
1411
1412 #[test]
1413 fn query_error_display_starts_with_prefix() {
1414 let e = QueryError::row_count("1 row", 0);
1415 assert!(e.to_string().starts_with("query error:"));
1416 }
1417
1418 #[test]
1419 fn decode_error_display_starts_with_prefix() {
1420 let e = BsqlError::Decode(DecodeError {
1421 column: Cow::Borrowed("x"),
1422 expected: "i32",
1423 actual: Cow::Borrowed("text"),
1424 source: None,
1425 });
1426 assert!(e.to_string().starts_with("decode error:"));
1427 }
1428
1429 #[test]
1430 fn connect_error_display_starts_with_prefix() {
1431 let e = ConnectError::create("refused");
1432 assert!(e.to_string().starts_with("connect error:"));
1433 }
1434
1435 #[test]
1438 fn from_driver_query_auth_delegates_to_from() {
1439 let e = BsqlError::from_driver_query(bsql_driver_postgres::DriverError::Auth("bad".into()));
1440 assert!(matches!(e, BsqlError::Connect(_)));
1441 }
1442
1443 #[test]
1446 fn from_driver_query_protocol_delegates_to_from() {
1447 let e = BsqlError::from_driver_query(bsql_driver_postgres::DriverError::Protocol(
1448 "bad msg".into(),
1449 ));
1450 assert!(matches!(e, BsqlError::Query(_)));
1451 }
1452
1453 #[test]
1456 fn query_error_source_is_none_without_source() {
1457 let e = BsqlError::Query(QueryError {
1458 message: Cow::Borrowed("test"),
1459 pg_code: None,
1460 source: None,
1461 });
1462 assert!(e.source().is_none());
1463 }
1464
1465 #[test]
1466 fn connect_error_source_is_none_without_source() {
1467 let e = ConnectError::create("test");
1468 assert!(e.source().is_none());
1469 }
1470
1471 #[test]
1472 fn pool_error_source_is_none_without_source() {
1473 let e = PoolError::exhausted();
1474 assert!(e.source().is_none());
1475 }
1476}