1use 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 NotFound,
16 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 Conflict,
34 Timeout,
36 Connection(String),
38 Decode(String),
40 Invalid(String),
42 Database {
44 code: Option<String>,
45 message: String,
46 },
47 Aborted(Box<dyn std::error::Error + Send + Sync>),
49 Hook(Box<dyn std::error::Error + Send + Sync>),
51}
52
53impl Error {
54 pub fn abort(err: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
56 Error::Aborted(err.into())
57 }
58
59 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}