Skip to main content

dinoco_engine/
error.rs

1use std::fmt;
2
3/// Portable constraint categories exposed only when a driver supplies a
4/// structured error code.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum DatabaseConstraintError {
7    UniqueViolation,
8    ForeignKeyViolation,
9    NotNullViolation,
10    CheckViolation,
11}
12
13/// A database failure together with Dinoco's portable classification. The
14/// original driver error remains available through the standard error chain.
15#[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/// Marker used by adapters when a row exists but a generated model cannot be
53/// decoded from it.
54#[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}