1use std::fmt;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum DatabaseConstraintError {
7 UniqueViolation,
8 ForeignKeyViolation,
9 NotNullViolation,
10 CheckViolation,
11}
12
13#[derive(Debug)]
16pub struct DatabaseError {
17 constraint: Option<DatabaseConstraintError>,
18 source: anyhow::Error,
19}
20
21impl DatabaseError {
22 pub fn new(source: anyhow::Error) -> Self {
23 let constraint = classify_constraint(&source);
24 Self { constraint, source }
25 }
26
27 pub fn constraint(&self) -> Option<DatabaseConstraintError> {
28 self.constraint
29 }
30
31 pub fn original(&self) -> &anyhow::Error {
32 &self.source
33 }
34
35 pub fn into_original(self) -> anyhow::Error {
36 self.source
37 }
38}
39
40impl fmt::Display for DatabaseError {
41 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
42 self.source.fmt(formatter)
43 }
44}
45
46impl std::error::Error for DatabaseError {
47 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
48 Some(self.source.as_ref())
49 }
50}
51
52#[derive(Debug)]
55pub struct RowDecodeError {
56 model: &'static str,
57}
58
59impl RowDecodeError {
60 pub fn new(model: &'static str) -> Self {
61 Self { model }
62 }
63}
64
65impl fmt::Display for RowDecodeError {
66 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67 write!(formatter, "failed to decode database row as `{}`", self.model)
68 }
69}
70
71impl std::error::Error for RowDecodeError {}
72
73pub fn is_decode_error(error: &anyhow::Error) -> bool {
74 error.chain().any(|source| source.is::<RowDecodeError>())
75}
76
77fn classify_constraint(error: &anyhow::Error) -> Option<DatabaseConstraintError> {
78 for source in error.chain() {
79 if let Some(error) = source.downcast_ref::<rusqlite::Error>()
80 && let rusqlite::Error::SqliteFailure(code, _) = error
81 {
82 return match code.extended_code {
83 rusqlite::ffi::SQLITE_CONSTRAINT_UNIQUE | rusqlite::ffi::SQLITE_CONSTRAINT_PRIMARYKEY => {
84 Some(DatabaseConstraintError::UniqueViolation)
85 }
86 rusqlite::ffi::SQLITE_CONSTRAINT_FOREIGNKEY => Some(DatabaseConstraintError::ForeignKeyViolation),
87 rusqlite::ffi::SQLITE_CONSTRAINT_NOTNULL => Some(DatabaseConstraintError::NotNullViolation),
88 rusqlite::ffi::SQLITE_CONSTRAINT_CHECK => Some(DatabaseConstraintError::CheckViolation),
89 _ => None,
90 };
91 }
92
93 if let Some(error) = source.downcast_ref::<tokio_postgres::Error>()
94 && let Some(error) = error.as_db_error()
95 {
96 return match *error.code() {
97 tokio_postgres::error::SqlState::UNIQUE_VIOLATION => Some(DatabaseConstraintError::UniqueViolation),
98 tokio_postgres::error::SqlState::FOREIGN_KEY_VIOLATION => {
99 Some(DatabaseConstraintError::ForeignKeyViolation)
100 }
101 tokio_postgres::error::SqlState::NOT_NULL_VIOLATION => Some(DatabaseConstraintError::NotNullViolation),
102 tokio_postgres::error::SqlState::CHECK_VIOLATION => Some(DatabaseConstraintError::CheckViolation),
103 _ => None,
104 };
105 }
106
107 if let Some(mysql_async::Error::Server(error)) = source.downcast_ref::<mysql_async::Error>() {
108 return match error.code {
109 1062 => Some(DatabaseConstraintError::UniqueViolation),
110 1451 | 1452 => Some(DatabaseConstraintError::ForeignKeyViolation),
111 1048 => Some(DatabaseConstraintError::NotNullViolation),
112 3819 => Some(DatabaseConstraintError::CheckViolation),
113 _ => None,
114 };
115 }
116 }
117
118 None
119}