1use std::fmt;
4
5#[derive(Debug)]
7pub enum Error {
8 Connection(ConnectionError),
10 Query(QueryError),
12 Type(TypeError),
14 Transaction(TransactionError),
16 Protocol(ProtocolError),
18 Pool(PoolError),
20 Schema(SchemaError),
22 Config(ConfigError),
24 Validation(ValidationError),
26 Io(std::io::Error),
28 Timeout,
30 Cancelled,
32 Serde(String),
34 Custom(String),
36}
37
38#[derive(Debug)]
39pub struct ConnectionError {
40 pub kind: ConnectionErrorKind,
41 pub message: String,
42 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ConnectionErrorKind {
47 Connect,
49 Authentication,
51 Disconnected,
53 Ssl,
55 DnsResolution,
57 Refused,
59 PoolExhausted,
61}
62
63#[derive(Debug)]
64pub struct QueryError {
65 pub kind: QueryErrorKind,
66 pub sql: Option<String>,
67 pub sqlstate: Option<String>,
68 pub message: String,
69 pub detail: Option<String>,
70 pub hint: Option<String>,
71 pub position: Option<usize>,
72 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum QueryErrorKind {
77 Syntax,
79 Constraint,
81 NotFound,
83 Permission,
85 DataTruncation,
87 Deadlock,
89 Serialization,
91 Timeout,
93 Cancelled,
95 Database,
97}
98
99#[derive(Debug)]
100pub struct TypeError {
101 pub expected: &'static str,
102 pub actual: String,
103 pub column: Option<String>,
104 pub rust_type: Option<&'static str>,
105}
106
107#[derive(Debug)]
108pub struct TransactionError {
109 pub kind: TransactionErrorKind,
110 pub message: String,
111}
112
113#[derive(Debug, Clone, Copy)]
114pub enum TransactionErrorKind {
115 AlreadyCommitted,
117 AlreadyRolledBack,
119 SavepointNotFound,
121 NestedNotSupported,
123}
124
125#[derive(Debug)]
126pub struct ProtocolError {
127 pub message: String,
128 pub raw_data: Option<Vec<u8>>,
129 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
130}
131
132#[derive(Debug)]
133pub struct PoolError {
134 pub kind: PoolErrorKind,
135 pub message: String,
136 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
137}
138
139impl PoolError {
140 pub fn poisoned(operation: &str) -> Self {
145 Self {
146 kind: PoolErrorKind::Poisoned,
147 message: format!(
148 "pool mutex poisoned during {operation}; a thread panicked while holding the lock"
149 ),
150 source: None,
151 }
152 }
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum PoolErrorKind {
157 Exhausted,
159 Timeout,
161 Closed,
163 Config,
165 Poisoned,
170}
171
172#[derive(Debug)]
173pub struct SchemaError {
174 pub kind: SchemaErrorKind,
175 pub message: String,
176 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
177}
178
179#[derive(Debug, Clone, Copy)]
180pub enum SchemaErrorKind {
181 TableExists,
183 TableNotFound,
185 ColumnExists,
187 ColumnNotFound,
189 Invalid,
191 Migration,
193}
194
195#[derive(Debug)]
196pub struct ConfigError {
197 pub message: String,
198 pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
199}
200
201#[derive(Debug, Clone)]
203pub struct ValidationError {
204 pub errors: Vec<FieldValidationError>,
206}
207
208#[derive(Debug, Clone)]
210pub struct FieldValidationError {
211 pub field: String,
213 pub kind: ValidationErrorKind,
215 pub message: String,
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum ValidationErrorKind {
222 Min,
224 Max,
226 MinLength,
228 MaxLength,
230 Pattern,
232 Required,
234 Custom,
236 Model,
238 MultipleOf,
240 MinItems,
242 MaxItems,
244 UniqueItems,
246 CreditCard,
248}
249
250impl ValidationError {
251 pub fn new() -> Self {
253 Self { errors: Vec::new() }
254 }
255
256 pub fn is_empty(&self) -> bool {
258 self.errors.is_empty()
259 }
260
261 pub fn add(
263 &mut self,
264 field: impl Into<String>,
265 kind: ValidationErrorKind,
266 message: impl Into<String>,
267 ) {
268 self.errors.push(FieldValidationError {
269 field: field.into(),
270 kind,
271 message: message.into(),
272 });
273 }
274
275 pub fn add_min(
277 &mut self,
278 field: impl Into<String>,
279 min: impl std::fmt::Display,
280 actual: impl std::fmt::Display,
281 ) {
282 self.add(
283 field,
284 ValidationErrorKind::Min,
285 format!("must be at least {min}, got {actual}"),
286 );
287 }
288
289 pub fn add_max(
291 &mut self,
292 field: impl Into<String>,
293 max: impl std::fmt::Display,
294 actual: impl std::fmt::Display,
295 ) {
296 self.add(
297 field,
298 ValidationErrorKind::Max,
299 format!("must be at most {max}, got {actual}"),
300 );
301 }
302
303 pub fn add_multiple_of(
307 &mut self,
308 field: impl Into<String>,
309 divisor: impl std::fmt::Display,
310 actual: impl std::fmt::Display,
311 ) {
312 self.add(
313 field,
314 ValidationErrorKind::MultipleOf,
315 format!("must be a multiple of {divisor}, got {actual}"),
316 );
317 }
318
319 pub fn add_min_items(&mut self, field: impl Into<String>, min: usize, actual: usize) {
323 self.add(
324 field,
325 ValidationErrorKind::MinItems,
326 format!("must have at least {min} items, got {actual}"),
327 );
328 }
329
330 pub fn add_max_items(&mut self, field: impl Into<String>, max: usize, actual: usize) {
334 self.add(
335 field,
336 ValidationErrorKind::MaxItems,
337 format!("must have at most {max} items, got {actual}"),
338 );
339 }
340
341 pub fn add_unique_items(&mut self, field: impl Into<String>, duplicate_count: usize) {
345 self.add(
346 field,
347 ValidationErrorKind::UniqueItems,
348 format!("must have unique items, found {duplicate_count} duplicate(s)"),
349 );
350 }
351
352 pub fn add_min_length(&mut self, field: impl Into<String>, min: usize, actual: usize) {
354 self.add(
355 field,
356 ValidationErrorKind::MinLength,
357 format!("must be at least {min} characters, got {actual}"),
358 );
359 }
360
361 pub fn add_max_length(&mut self, field: impl Into<String>, max: usize, actual: usize) {
363 self.add(
364 field,
365 ValidationErrorKind::MaxLength,
366 format!("must be at most {max} characters, got {actual}"),
367 );
368 }
369
370 pub fn add_pattern(&mut self, field: impl Into<String>, pattern: &str) {
372 self.add(
373 field,
374 ValidationErrorKind::Pattern,
375 format!("must match pattern '{pattern}'"),
376 );
377 }
378
379 pub fn add_required(&mut self, field: impl Into<String>) {
381 self.add(
382 field,
383 ValidationErrorKind::Required,
384 "is required".to_string(),
385 );
386 }
387
388 pub fn add_custom(&mut self, field: impl Into<String>, message: impl Into<String>) {
390 self.add(field, ValidationErrorKind::Custom, message);
391 }
392
393 pub fn add_model_error(&mut self, message: impl Into<String>) {
398 self.add("__model__", ValidationErrorKind::Model, message);
399 }
400
401 pub fn add_credit_card(&mut self, field: impl Into<String>) {
403 self.add(
404 field,
405 ValidationErrorKind::CreditCard,
406 "is not a valid credit card number".to_string(),
407 );
408 }
409
410 pub fn into_result(self) -> std::result::Result<(), Self> {
412 if self.is_empty() { Ok(()) } else { Err(self) }
413 }
414}
415
416impl Default for ValidationError {
417 fn default() -> Self {
418 Self::new()
419 }
420}
421
422impl Error {
423 pub fn is_retryable(&self) -> bool {
425 match self {
426 Error::Query(q) => matches!(
427 q.kind,
428 QueryErrorKind::Deadlock | QueryErrorKind::Serialization | QueryErrorKind::Timeout
429 ),
430 Error::Pool(p) => matches!(p.kind, PoolErrorKind::Exhausted | PoolErrorKind::Timeout),
431 Error::Connection(c) => matches!(c.kind, ConnectionErrorKind::PoolExhausted),
432 Error::Timeout => true,
433 _ => false,
434 }
435 }
436
437 pub fn is_pool_poisoned(&self) -> bool {
442 matches!(self, Error::Pool(p) if p.kind == PoolErrorKind::Poisoned)
443 }
444
445 pub fn is_connection_error(&self) -> bool {
447 match self {
448 Error::Connection(c) => matches!(
449 c.kind,
450 ConnectionErrorKind::Connect
451 | ConnectionErrorKind::Authentication
452 | ConnectionErrorKind::Disconnected
453 | ConnectionErrorKind::Ssl
454 | ConnectionErrorKind::DnsResolution
455 | ConnectionErrorKind::Refused
456 ),
457 Error::Protocol(_) | Error::Io(_) => true,
458 _ => false,
459 }
460 }
461
462 pub fn sqlstate(&self) -> Option<&str> {
464 match self {
465 Error::Query(q) => q.sqlstate.as_deref(),
466 _ => None,
467 }
468 }
469
470 pub fn sql(&self) -> Option<&str> {
472 match self {
473 Error::Query(q) => q.sql.as_deref(),
474 _ => None,
475 }
476 }
477}
478
479impl QueryError {
480 pub fn is_unique_violation(&self) -> bool {
482 self.sqlstate.as_deref() == Some("23505")
483 }
484
485 pub fn is_foreign_key_violation(&self) -> bool {
487 self.sqlstate.as_deref() == Some("23503")
488 }
489}
490
491impl fmt::Display for Error {
492 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
493 match self {
494 Error::Connection(e) => write!(f, "Connection error: {}", e.message),
495 Error::Query(e) => {
496 if let Some(sqlstate) = &e.sqlstate {
497 write!(f, "Query error (SQLSTATE {}): {}", sqlstate, e.message)
498 } else {
499 write!(f, "Query error: {}", e.message)
500 }
501 }
502 Error::Type(e) => {
503 if let Some(col) = &e.column {
504 write!(
505 f,
506 "Type error in column '{}': expected {}, found {}",
507 col, e.expected, e.actual
508 )
509 } else {
510 write!(f, "Type error: expected {}, found {}", e.expected, e.actual)
511 }
512 }
513 Error::Transaction(e) => write!(f, "Transaction error: {}", e.message),
514 Error::Protocol(e) => write!(f, "Protocol error: {}", e.message),
515 Error::Pool(e) => write!(f, "Pool error: {}", e.message),
516 Error::Schema(e) => write!(f, "Schema error: {}", e.message),
517 Error::Config(e) => write!(f, "Configuration error: {}", e.message),
518 Error::Validation(e) => write!(f, "Validation error: {}", e),
519 Error::Io(e) => write!(f, "I/O error: {}", e),
520 Error::Timeout => write!(f, "Operation timed out"),
521 Error::Cancelled => write!(f, "Operation cancelled"),
522 Error::Serde(msg) => write!(f, "Serialization error: {}", msg),
523 Error::Custom(msg) => write!(f, "{}", msg),
524 }
525 }
526}
527
528impl std::error::Error for Error {
529 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
530 match self {
531 Error::Connection(e) => e
532 .source
533 .as_deref()
534 .map(|err| err as &(dyn std::error::Error + 'static)),
535 Error::Query(e) => e
536 .source
537 .as_deref()
538 .map(|err| err as &(dyn std::error::Error + 'static)),
539 Error::Protocol(e) => e
540 .source
541 .as_deref()
542 .map(|err| err as &(dyn std::error::Error + 'static)),
543 Error::Pool(e) => e
544 .source
545 .as_deref()
546 .map(|err| err as &(dyn std::error::Error + 'static)),
547 Error::Schema(e) => e
548 .source
549 .as_deref()
550 .map(|err| err as &(dyn std::error::Error + 'static)),
551 Error::Config(e) => e
552 .source
553 .as_deref()
554 .map(|err| err as &(dyn std::error::Error + 'static)),
555 Error::Io(e) => Some(e),
556 _ => None,
557 }
558 }
559}
560
561impl fmt::Display for ConnectionError {
562 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
563 write!(f, "{}", self.message)
564 }
565}
566
567impl fmt::Display for QueryError {
568 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569 if let Some(sqlstate) = &self.sqlstate {
570 write!(f, "{} (SQLSTATE {})", self.message, sqlstate)
571 } else {
572 write!(f, "{}", self.message)
573 }
574 }
575}
576
577impl fmt::Display for TypeError {
578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579 if let Some(col) = &self.column {
580 write!(
581 f,
582 "expected {} for column '{}', found {}",
583 self.expected, col, self.actual
584 )
585 } else {
586 write!(f, "expected {}, found {}", self.expected, self.actual)
587 }
588 }
589}
590
591impl fmt::Display for TransactionError {
592 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593 write!(f, "{}", self.message)
594 }
595}
596
597impl fmt::Display for ProtocolError {
598 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
599 write!(f, "{}", self.message)
600 }
601}
602
603impl fmt::Display for PoolError {
604 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
605 write!(f, "{}", self.message)
606 }
607}
608
609impl fmt::Display for SchemaError {
610 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
611 write!(f, "{}", self.message)
612 }
613}
614
615impl fmt::Display for ConfigError {
616 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617 write!(f, "{}", self.message)
618 }
619}
620
621impl fmt::Display for ValidationError {
622 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623 if self.errors.is_empty() {
624 write!(f, "validation passed")
625 } else if self.errors.len() == 1 {
626 let err = &self.errors[0];
627 write!(f, "validation error on '{}': {}", err.field, err.message)
628 } else {
629 writeln!(f, "validation errors:")?;
630 for err in &self.errors {
631 writeln!(f, " - {}: {}", err.field, err.message)?;
632 }
633 Ok(())
634 }
635 }
636}
637
638impl std::error::Error for ValidationError {}
639
640impl From<std::io::Error> for Error {
641 fn from(err: std::io::Error) -> Self {
642 Error::Io(err)
643 }
644}
645
646impl From<ConnectionError> for Error {
647 fn from(err: ConnectionError) -> Self {
648 Error::Connection(err)
649 }
650}
651
652impl From<QueryError> for Error {
653 fn from(err: QueryError) -> Self {
654 Error::Query(err)
655 }
656}
657
658impl From<TypeError> for Error {
659 fn from(err: TypeError) -> Self {
660 Error::Type(err)
661 }
662}
663
664impl From<TransactionError> for Error {
665 fn from(err: TransactionError) -> Self {
666 Error::Transaction(err)
667 }
668}
669
670impl From<ProtocolError> for Error {
671 fn from(err: ProtocolError) -> Self {
672 Error::Protocol(err)
673 }
674}
675
676impl From<PoolError> for Error {
677 fn from(err: PoolError) -> Self {
678 Error::Pool(err)
679 }
680}
681
682impl From<SchemaError> for Error {
683 fn from(err: SchemaError) -> Self {
684 Error::Schema(err)
685 }
686}
687
688impl From<ConfigError> for Error {
689 fn from(err: ConfigError) -> Self {
690 Error::Config(err)
691 }
692}
693
694impl From<ValidationError> for Error {
695 fn from(err: ValidationError) -> Self {
696 Error::Validation(err)
697 }
698}
699
700pub type Result<T> = std::result::Result<T, Error>;
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706
707 #[test]
708 fn sqlstate_helpers() {
709 let query = QueryError {
710 kind: QueryErrorKind::Constraint,
711 sql: Some("SELECT 1".to_string()),
712 sqlstate: Some("23505".to_string()),
713 message: "unique violation".to_string(),
714 detail: None,
715 hint: None,
716 position: None,
717 source: None,
718 };
719
720 assert!(query.is_unique_violation());
721 assert!(!query.is_foreign_key_violation());
722
723 let err = Error::Query(query);
724 assert_eq!(err.sqlstate(), Some("23505"));
725 assert_eq!(err.sql(), Some("SELECT 1"));
726 }
727
728 #[test]
729 fn retryable_and_connection_flags() {
730 let retryable_query = Error::Query(QueryError {
731 kind: QueryErrorKind::Deadlock,
732 sql: None,
733 sqlstate: None,
734 message: "deadlock detected".to_string(),
735 detail: None,
736 hint: None,
737 position: None,
738 source: None,
739 });
740
741 let pool_exhausted = Error::Pool(PoolError {
742 kind: PoolErrorKind::Exhausted,
743 message: "pool exhausted".to_string(),
744 source: None,
745 });
746
747 let conn_exhausted = Error::Connection(ConnectionError {
748 kind: ConnectionErrorKind::PoolExhausted,
749 message: "pool exhausted".to_string(),
750 source: None,
751 });
752
753 assert!(retryable_query.is_retryable());
754 assert!(pool_exhausted.is_retryable());
755 assert!(conn_exhausted.is_retryable());
756
757 let conn_error = Error::Connection(ConnectionError {
758 kind: ConnectionErrorKind::Disconnected,
759 message: "lost connection".to_string(),
760 source: None,
761 });
762 assert!(conn_error.is_connection_error());
763 }
764
765 #[test]
766 fn pool_poisoned_error() {
767 let err = PoolError::poisoned("acquire");
769 assert_eq!(err.kind, PoolErrorKind::Poisoned);
770 assert!(err.message.contains("acquire"));
771 assert!(err.message.contains("poisoned"));
772 assert!(err.message.contains("panicked"));
773
774 let error = Error::Pool(err);
776 assert!(error.is_pool_poisoned());
777 assert!(!error.is_retryable()); assert!(!error.is_connection_error());
779
780 let display = format!("{}", error);
782 assert!(display.contains("Pool error"));
783 assert!(display.contains("poisoned"));
784 }
785
786 #[test]
787 fn pool_poisoned_not_retryable() {
788 let poisoned = Error::Pool(PoolError::poisoned("close"));
789 let exhausted = Error::Pool(PoolError {
790 kind: PoolErrorKind::Exhausted,
791 message: "no connections".to_string(),
792 source: None,
793 });
794 let timeout = Error::Pool(PoolError {
795 kind: PoolErrorKind::Timeout,
796 message: "timed out".to_string(),
797 source: None,
798 });
799
800 assert!(!poisoned.is_retryable());
802 assert!(exhausted.is_retryable());
804 assert!(timeout.is_retryable());
805 }
806}