Skip to main content

rango_orm_core/
error.rs

1//! Typed errors returned by every Rango operation.
2//!
3//! Backends translate driver errors into these variants so applications can react
4//! (`UniqueViolation` → HTTP 409, `Conflict` → retry) without string matching.
5//! Messages never contain SQL text or bound values.
6
7use std::fmt;
8
9pub type Result<T, E = Error> = std::result::Result<T, E>;
10
11#[derive(Debug)]
12#[non_exhaustive]
13pub enum Error {
14    /// The query expected a row and found none.
15    NotFound,
16    /// The table does not exist: migrations were not generated or applied.
17    TableMissing {
18        table: String,
19    },
20    UniqueViolation {
21        constraint: Option<String>,
22    },
23    ForeignKeyViolation {
24        constraint: Option<String>,
25    },
26    NotNullViolation {
27        constraint: Option<String>,
28    },
29    CheckViolation {
30        constraint: Option<String>,
31    },
32    /// Serialization failure or deadlock: retrying the whole transaction may succeed.
33    Conflict,
34    /// Pool acquire timeout, statement timeout or lock timeout.
35    Timeout,
36    /// Connection could not be opened or was lost (IO, TLS, closed pool).
37    Connection(String),
38    /// A row could not be decoded into the model.
39    Decode(String),
40    /// Invalid input or configuration, detected before or by the database.
41    Invalid(String),
42    /// Any other database error. `code` is the backend code (SQLSTATE on Postgres).
43    Database {
44        code: Option<String>,
45        message: String,
46    },
47    /// User code aborted the operation (e.g. inside `atomic`), which rolls it back.
48    Aborted(Box<dyn std::error::Error + Send + Sync>),
49    /// A `ModelHooks` hook returned an error.
50    Hook(Box<dyn std::error::Error + Send + Sync>),
51}
52
53impl Error {
54    /// Abort a transaction with an application error: `return Err(Error::abort("insufficient funds"))`.
55    pub fn abort(err: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
56        Error::Aborted(err.into())
57    }
58
59    /// True for errors where retrying the transaction can succeed.
60    pub fn is_retryable(&self) -> bool {
61        matches!(self, Error::Conflict)
62    }
63}
64
65impl fmt::Display for Error {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        let constraint =
68            |c: &Option<String>| c.as_deref().map(|c| format!(" ({c})")).unwrap_or_default();
69        match self {
70            Error::NotFound => write!(f, "row not found"),
71            Error::TableMissing { table } => write!(
72                f,
73                "table `{table}` does not exist — run `rango makemigrations` and `rango migrate`"
74            ),
75            Error::UniqueViolation { constraint: c } => {
76                write!(f, "unique violation{}", constraint(c))
77            }
78            Error::ForeignKeyViolation { constraint: c } => {
79                write!(f, "foreign key violation{}", constraint(c))
80            }
81            Error::NotNullViolation { constraint: c } => {
82                write!(f, "not null violation{}", constraint(c))
83            }
84            Error::CheckViolation { constraint: c } => {
85                write!(f, "check violation{}", constraint(c))
86            }
87            Error::Conflict => write!(
88                f,
89                "transaction conflict (serialization failure or deadlock)"
90            ),
91            Error::Timeout => write!(f, "timed out"),
92            Error::Connection(m) => write!(f, "connection error: {m}"),
93            Error::Decode(m) => write!(f, "decode error: {m}"),
94            Error::Invalid(m) => write!(f, "invalid: {m}"),
95            Error::Database {
96                code: Some(c),
97                message,
98            } => write!(f, "database error {c}: {message}"),
99            Error::Database {
100                code: None,
101                message,
102            } => write!(f, "database error: {message}"),
103            Error::Aborted(e) => write!(f, "aborted: {e}"),
104            Error::Hook(e) => write!(f, "hook failed: {e}"),
105        }
106    }
107}
108
109impl std::error::Error for Error {
110    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
111        match self {
112            Error::Aborted(e) | Error::Hook(e) => Some(e.as_ref()),
113            _ => None,
114        }
115    }
116}
117
118impl From<crate::RowError> for Error {
119    fn from(e: crate::RowError) -> Self {
120        Error::Decode(e.to_string())
121    }
122}
123
124impl From<crate::FieldError> for Error {
125    fn from(e: crate::FieldError) -> Self {
126        Error::Invalid(e.0)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::Error;
133
134    #[test]
135    fn field_error_converts_with_question_mark() {
136        fn f() -> super::Result<crate::FieldVarchar<1, 3>> {
137            Ok(crate::FieldVarchar::new("too long")?)
138        }
139        assert!(matches!(f(), Err(Error::Invalid(m)) if m.contains("too long")));
140    }
141
142    #[test]
143    fn display_and_retryable() {
144        let e = Error::UniqueViolation {
145            constraint: Some("user_email_key".into()),
146        };
147        assert_eq!(e.to_string(), "unique violation (user_email_key)");
148        assert_eq!(
149            Error::TableMissing {
150                table: "user".into()
151            }
152            .to_string(),
153            "table `user` does not exist — run `rango makemigrations` and `rango migrate`"
154        );
155        assert!(Error::Conflict.is_retryable());
156        assert!(!Error::NotFound.is_retryable());
157        assert_eq!(Error::abort("no funds").to_string(), "aborted: no funds");
158        assert_eq!(Error::Hook("nope".into()).to_string(), "hook failed: nope");
159    }
160}