Skip to main content

rullst_orm/
error.rs

1use std::error::Error;
2use std::fmt;
3
4/// The standard error type for rullst-orm, shielding users from internal dependency errors.
5#[derive(Debug, Clone)]
6#[non_exhaustive]
7pub enum RullstError {
8    /// The global ORM state has not been initialized yet.
9    NotInitialized,
10    /// The global ORM state was already initialized by an earlier call.
11    AlreadyInitialized,
12    /// A record was not found in the database.
13    RecordNotFound,
14    /// A general database or query error.
15    DatabaseError(String),
16    /// A unique or primary-key constraint rejected the statement, for example
17    /// a second user with the same email. Match it as
18    /// `UniqueViolation { .. }`: later versions may add fields. Added in v13.
19    #[non_exhaustive]
20    UniqueViolation {
21        /// The violated constraint or index, when the database names it:
22        /// PostgreSQL does, SQLite and MySQL/MariaDB do not.
23        constraint: Option<String>,
24    },
25    /// A foreign-key constraint rejected the statement: the referenced row
26    /// does not exist, or a row still references the one being deleted.
27    /// Match it as `ForeignKeyViolation { .. }`. Added in v13.
28    #[non_exhaustive]
29    ForeignKeyViolation {
30        /// The violated constraint, when the database names it: PostgreSQL
31        /// does, SQLite and MySQL/MariaDB do not.
32        constraint: Option<String>,
33    },
34    /// The connected database cannot make this schema change, for example
35    /// adding a foreign key to an existing SQLite table. `Schema::table`
36    /// returns it before running any statement. Match it as
37    /// `UnsupportedSchemaChange { .. }`. Added in v13.
38    #[non_exhaustive]
39    UnsupportedSchemaChange {
40        /// The SQL dialect: `sqlite`, `postgres` or `mysql`.
41        driver: String,
42        /// The change that cannot be made, and the usual alternative.
43        change: String,
44    },
45    /// A serialization or deserialization error (e.g., JSON).
46    SerializationError(String),
47    /// A cache or event-related error.
48    CacheError(String),
49    /// A database commit succeeded, but one or more post-commit effects failed.
50    PostCommit(String),
51    /// A validation error, such as invalid SQL identifiers.
52    Validation(String),
53    /// Other internal errors.
54    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
93/// Names the constraint only: the database's own message can quote the
94/// rejected values (MySQL's "Duplicate entry 'alice@example.com'").
95fn 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    /// A database error that names its constraint, as PostgreSQL's does.
236    #[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        // SQLx enables SQLite foreign keys on every connection it opens.
306        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}