1use std::error::Error;
2use std::fmt;
3
4#[derive(Debug, Clone)]
6#[non_exhaustive]
7pub enum RullstError {
8 NotInitialized,
10 AlreadyInitialized,
12 RecordNotFound,
14 DatabaseError(String),
16 #[non_exhaustive]
20 UniqueViolation {
21 constraint: Option<String>,
24 },
25 #[non_exhaustive]
29 ForeignKeyViolation {
30 constraint: Option<String>,
33 },
34 #[non_exhaustive]
39 UnsupportedSchemaChange {
40 driver: String,
42 change: String,
44 },
45 SerializationError(String),
47 CacheError(String),
49 PostCommit(String),
51 Validation(String),
53 Internal(String),
55}
56
57impl fmt::Display for RullstError {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 match self {
60 RullstError::NotInitialized => {
61 write!(
62 f,
63 "Orm is not initialized. Call Orm::init() before querying."
64 )
65 }
66 RullstError::AlreadyInitialized => write!(f, "Orm has already been initialized"),
67 RullstError::RecordNotFound => write!(f, "Record not found"),
68 RullstError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
69 RullstError::UniqueViolation { constraint } => {
70 write_violation(f, "unique", constraint.as_deref())
71 }
72 RullstError::ForeignKeyViolation { constraint } => {
73 write_violation(f, "foreign key", constraint.as_deref())
74 }
75 RullstError::UnsupportedSchemaChange { driver, change } => {
76 write!(f, "Unsupported schema change on {driver}: {change}")
77 }
78 RullstError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
79 RullstError::CacheError(msg) => write!(f, "Cache error: {}", msg),
80 RullstError::PostCommit(msg) => {
81 write!(
82 f,
83 "Database committed, but a post-commit effect failed: {}",
84 msg
85 )
86 }
87 RullstError::Validation(msg) => write!(f, "Validation error: {}", msg),
88 RullstError::Internal(msg) => write!(f, "Internal error: {}", msg),
89 }
90 }
91}
92
93fn write_violation(
96 f: &mut fmt::Formatter<'_>,
97 kind: &str,
98 constraint: Option<&str>,
99) -> fmt::Result {
100 match constraint {
101 Some(name) => write!(f, "Database error: {kind} constraint `{name}` violated"),
102 None => write!(f, "Database error: {kind} constraint violated"),
103 }
104}
105
106impl Error for RullstError {}
107
108impl From<sqlx::Error> for RullstError {
109 fn from(err: sqlx::Error) -> Self {
110 match err {
111 sqlx::Error::RowNotFound => RullstError::RecordNotFound,
112 sqlx::Error::Database(ref error) if error.is_unique_violation() => {
113 RullstError::UniqueViolation {
114 constraint: error.constraint().map(str::to_owned),
115 }
116 }
117 sqlx::Error::Database(ref error) if error.is_foreign_key_violation() => {
118 RullstError::ForeignKeyViolation {
119 constraint: error.constraint().map(str::to_owned),
120 }
121 }
122 _ => RullstError::DatabaseError(err.to_string()),
123 }
124 }
125}
126
127impl From<serde_json::Error> for RullstError {
128 fn from(err: serde_json::Error) -> Self {
129 RullstError::SerializationError(err.to_string())
130 }
131}
132
133impl From<crate::privacy::PrivacyError> for RullstError {
134 fn from(err: crate::privacy::PrivacyError) -> Self {
135 RullstError::Internal(err.to_string())
136 }
137}
138
139#[cfg(feature = "redis")]
140impl From<redis::RedisError> for RullstError {
141 #[cfg_attr(test, mutants::skip)]
142 fn from(err: redis::RedisError) -> Self {
143 RullstError::CacheError(err.to_string())
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_rullst_error_display() {
153 assert_eq!(
154 RullstError::NotInitialized.to_string(),
155 "Orm is not initialized. Call Orm::init() before querying."
156 );
157 assert_eq!(
158 RullstError::AlreadyInitialized.to_string(),
159 "Orm has already been initialized"
160 );
161 assert_eq!(RullstError::RecordNotFound.to_string(), "Record not found");
162 assert_eq!(
163 RullstError::DatabaseError("msg".to_string()).to_string(),
164 "Database error: msg"
165 );
166 assert_eq!(
167 RullstError::SerializationError("msg".to_string()).to_string(),
168 "Serialization error: msg"
169 );
170 assert_eq!(
171 RullstError::PostCommit("msg".to_string()).to_string(),
172 "Database committed, but a post-commit effect failed: msg"
173 );
174 assert_eq!(
175 RullstError::Validation("msg".to_string()).to_string(),
176 "Validation error: msg"
177 );
178 assert_eq!(
179 RullstError::Internal("msg".to_string()).to_string(),
180 "Internal error: msg"
181 );
182 }
183
184 #[test]
185 fn test_rullst_error_from() {
186 let sqlx_err = sqlx::Error::RowNotFound;
187 let err: RullstError = sqlx_err.into();
188 assert!(matches!(err, RullstError::RecordNotFound));
189
190 let sqlx_other_err = sqlx::Error::PoolClosed;
191 let err2: RullstError = sqlx_other_err.into();
192 assert!(matches!(err2, RullstError::DatabaseError(_)));
193 }
194
195 #[test]
196 fn constraint_violations_name_only_their_constraint() {
197 let named = RullstError::UniqueViolation {
198 constraint: Some("users_email_key".to_string()),
199 };
200 assert_eq!(
201 named.to_string(),
202 "Database error: unique constraint `users_email_key` violated"
203 );
204 let unnamed = RullstError::UniqueViolation { constraint: None };
205 assert_eq!(
206 unnamed.to_string(),
207 "Database error: unique constraint violated"
208 );
209 let foreign = RullstError::ForeignKeyViolation {
210 constraint: Some("posts_user_id_fkey".to_string()),
211 };
212 assert_eq!(
213 foreign.to_string(),
214 "Database error: foreign key constraint `posts_user_id_fkey` violated"
215 );
216 let unnamed = RullstError::ForeignKeyViolation { constraint: None };
217 assert_eq!(
218 unnamed.to_string(),
219 "Database error: foreign key constraint violated"
220 );
221 }
222
223 #[test]
224 fn unsupported_schema_changes_name_the_driver_and_change() {
225 let error = RullstError::UnsupportedSchemaChange {
226 driver: "sqlite".to_string(),
227 change: "adding a foreign key to an existing table".to_string(),
228 };
229 assert_eq!(
230 error.to_string(),
231 "Unsupported schema change on sqlite: adding a foreign key to an existing table"
232 );
233 }
234
235 #[derive(Debug)]
237 struct NamedConstraintError {
238 unique: bool,
239 }
240
241 impl fmt::Display for NamedConstraintError {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 f.write_str("Key (email)=(alice@example.test) already exists.")
244 }
245 }
246
247 impl Error for NamedConstraintError {}
248
249 impl sqlx::error::DatabaseError for NamedConstraintError {
250 fn message(&self) -> &str {
251 "Key (email)=(alice@example.test) already exists."
252 }
253
254 fn as_error(&self) -> &(dyn Error + Send + Sync + 'static) {
255 self
256 }
257
258 fn as_error_mut(&mut self) -> &mut (dyn Error + Send + Sync + 'static) {
259 self
260 }
261
262 fn into_error(self: Box<Self>) -> Box<dyn Error + Send + Sync + 'static> {
263 self
264 }
265
266 fn constraint(&self) -> Option<&str> {
267 Some(if self.unique {
268 "users_email_key"
269 } else {
270 "posts_user_id_fkey"
271 })
272 }
273
274 fn kind(&self) -> sqlx::error::ErrorKind {
275 if self.unique {
276 sqlx::error::ErrorKind::UniqueViolation
277 } else {
278 sqlx::error::ErrorKind::ForeignKeyViolation
279 }
280 }
281 }
282
283 #[test]
284 fn database_errors_keep_the_constraint_name_but_not_the_message() {
285 let unique: RullstError =
286 sqlx::Error::Database(Box::new(NamedConstraintError { unique: true })).into();
287 assert!(
288 matches!(&unique, RullstError::UniqueViolation { constraint: Some(name) } if name == "users_email_key"),
289 "{unique:?}"
290 );
291 let foreign: RullstError =
292 sqlx::Error::Database(Box::new(NamedConstraintError { unique: false })).into();
293 assert!(
294 matches!(&foreign, RullstError::ForeignKeyViolation { constraint: Some(name) } if name == "posts_user_id_fkey"),
295 "{foreign:?}"
296 );
297 assert!(!format!("{unique} {unique:?} {foreign}").contains("alice@example.test"));
298 }
299
300 #[cfg(any(feature = "drivers-all", feature = "strict-sqlite"))]
301 #[tokio::test]
302 async fn sqlite_constraint_errors_become_typed_variants() {
303 use sqlx::Connection;
304
305 let mut connection = sqlx::SqliteConnection::connect("sqlite::memory:")
307 .await
308 .unwrap();
309 sqlx::raw_sql(
310 "CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE);
311 CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id));
312 INSERT INTO users (id, email) VALUES (1, 'alice@example.test');",
313 )
314 .execute(&mut connection)
315 .await
316 .unwrap();
317
318 let duplicate = sqlx::query("INSERT INTO users (email) VALUES ('alice@example.test')")
319 .execute(&mut connection)
320 .await
321 .unwrap_err();
322 let duplicate = RullstError::from(duplicate);
323 assert!(
324 matches!(duplicate, RullstError::UniqueViolation { constraint: None }),
325 "{duplicate:?}"
326 );
327 assert_eq!(
328 duplicate.to_string(),
329 "Database error: unique constraint violated"
330 );
331
332 let orphan = sqlx::query("INSERT INTO posts (user_id) VALUES (99)")
333 .execute(&mut connection)
334 .await
335 .unwrap_err();
336 let orphan = RullstError::from(orphan);
337 assert!(
338 matches!(
339 orphan,
340 RullstError::ForeignKeyViolation { constraint: None }
341 ),
342 "{orphan:?}"
343 );
344
345 let missing = sqlx::query("SELECT * FROM missing_table")
346 .execute(&mut connection)
347 .await
348 .unwrap_err();
349 let missing = RullstError::from(missing);
350 assert!(
351 matches!(&missing, RullstError::DatabaseError(message) if message.contains("no such table: missing_table")),
352 "{missing:?}"
353 );
354 }
355
356 #[test]
357 fn test_serde_json_error_from() {
358 let json_err = serde_json::from_str::<serde_json::Value>("{invalid}").unwrap_err();
359 let err: RullstError = json_err.into();
360 assert!(matches!(err, RullstError::SerializationError(_)));
361 assert!(err.to_string().contains("Serialization error:"));
362 }
363
364 #[cfg(feature = "redis")]
365 #[test]
366 fn test_redis_error_from() {
367 let redis_err = redis::RedisError::from((redis::ErrorKind::Io, "redis io error"));
368 let err: RullstError = redis_err.into();
369 assert!(matches!(err, RullstError::CacheError(_)));
370 assert_eq!(err.to_string(), "Cache error: redis io error - Io");
371 }
372}