Skip to main content

renox_core/db/
error.rs

1use std::fmt;
2
3/// A database error: a query that failed, a lost connection, a broken
4/// constraint. Converts into [`crate::Error`] with `?`.
5///
6/// ```
7/// # use renox::prelude::*;
8/// # async fn demo(db: Db) -> Result {
9/// let inserted = renox::db::sql("INSERT INTO tags (name) VALUES (?)").bind("coffee").execute(&db).await;
10/// match inserted {
11///     Err(err) if err.is_unique_violation() => { /* already there */ }
12///     other => { other?; }
13/// }
14/// # Ok(()) }
15/// ```
16pub struct DbError(sqlx::Error);
17
18impl DbError {
19    /// A `UNIQUE` constraint (or primary key) was violated.
20    pub fn is_unique_violation(&self) -> bool {
21        matches!(&self.0, sqlx::Error::Database(db) if db.is_unique_violation())
22    }
23
24    /// A `REFERENCES` constraint was violated.
25    pub fn is_foreign_key_violation(&self) -> bool {
26        matches!(&self.0, sqlx::Error::Database(db) if db.is_foreign_key_violation())
27    }
28
29    /// `fetch_one` or `scalar` found no row.
30    pub fn is_row_not_found(&self) -> bool {
31        matches!(self.0, sqlx::Error::RowNotFound)
32    }
33
34    /// Another transaction got in the way and trying again may work:
35    /// SQLite's "database is locked" (busy), PostgreSQL's serialization
36    /// failure or deadlock. `Db::transaction_retrying` retries on these.
37    pub fn is_retryable(&self) -> bool {
38        let sqlx::Error::Database(db) = &self.0 else {
39            return false;
40        };
41        match db.code() {
42            Some(code) if code == "40001" || code == "40P01" => true,
43            // SQLite's primary codes 5 (BUSY) and 6 (LOCKED), extended or not.
44            Some(code) => code.parse::<i32>().is_ok_and(|c| matches!(c & 0xff, 5 | 6)),
45            None => false,
46        }
47    }
48
49    /// No connection was free within `DATABASE_ACQUIRE_TIMEOUT`.
50    pub fn is_timeout(&self) -> bool {
51        matches!(self.0, sqlx::Error::PoolTimedOut)
52    }
53
54    /// The underlying sqlx error, for what Renox doesn't cover. sqlx's
55    /// version isn't part of Renox's stability promise.
56    pub fn sqlx(&self) -> &sqlx::Error {
57        &self.0
58    }
59}
60
61impl From<sqlx::Error> for DbError {
62    fn from(err: sqlx::Error) -> Self {
63        Self(err)
64    }
65}
66
67impl fmt::Display for DbError {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        fmt::Display::fmt(&self.0, f)
70    }
71}
72
73impl fmt::Debug for DbError {
74    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75        fmt::Debug::fmt(&self.0, f)
76    }
77}
78
79impl std::error::Error for DbError {
80    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
81        // The wrapped error's own cause; its message is already ours.
82        self.0.source()
83    }
84}
85
86/// A database error with a chosen code, for tests of the code checks.
87#[cfg(test)]
88pub(crate) mod fake {
89    use std::borrow::Cow;
90    use std::fmt;
91
92    use sqlx::error::{DatabaseError, ErrorKind};
93
94    #[derive(Debug)]
95    struct Coded(Option<&'static str>, ErrorKind);
96
97    impl fmt::Display for Coded {
98        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99            write!(f, "error {:?}", self.0)
100        }
101    }
102
103    impl std::error::Error for Coded {}
104
105    impl DatabaseError for Coded {
106        fn message(&self) -> &str {
107            "fake"
108        }
109        fn code(&self) -> Option<Cow<'_, str>> {
110            self.0.map(Cow::Borrowed)
111        }
112        fn as_error(&self) -> &(dyn std::error::Error + Send + Sync + 'static) {
113            self
114        }
115        fn as_error_mut(&mut self) -> &mut (dyn std::error::Error + Send + Sync + 'static) {
116            self
117        }
118        fn into_error(self: Box<Self>) -> Box<dyn std::error::Error + Send + Sync + 'static> {
119            self
120        }
121        fn kind(&self) -> ErrorKind {
122            match self.1 {
123                ErrorKind::UniqueViolation => ErrorKind::UniqueViolation,
124                _ => ErrorKind::Other,
125            }
126        }
127    }
128
129    /// A database error answering `code`.
130    pub(crate) fn coded(code: Option<&'static str>) -> sqlx::Error {
131        sqlx::Error::Database(Box::new(Coded(code, ErrorKind::Other)))
132    }
133
134    /// PostgreSQL's unique violation.
135    pub(crate) fn unique() -> sqlx::Error {
136        sqlx::Error::Database(Box::new(Coded(Some("23505"), ErrorKind::UniqueViolation)))
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::{DbError, fake};
143
144    /// Retryable: PostgreSQL's serialization failure and deadlock, SQLite's
145    /// busy and locked with their extended codes; nothing else.
146    #[test]
147    fn retryable_codes() {
148        for code in ["40001", "40P01", "5", "517", "6", "262"] {
149            assert!(
150                DbError::from(fake::coded(Some(code))).is_retryable(),
151                "{code}"
152            );
153        }
154        for code in [Some("23505"), Some("19"), Some("XX000"), None] {
155            assert!(!DbError::from(fake::coded(code)).is_retryable(), "{code:?}");
156        }
157        assert!(!DbError::from(sqlx::Error::PoolTimedOut).is_retryable());
158        assert!(DbError::from(sqlx::Error::PoolTimedOut).is_timeout());
159        assert!(!DbError::from(sqlx::Error::PoolClosed).is_unique_violation());
160    }
161
162    /// PostgreSQL's 23505 is a unique violation, through `DbError` and
163    /// through `renox::Error`, also when the raw sqlx error was converted.
164    #[test]
165    fn unique_violations_through_every_wrapper() {
166        assert!(DbError::from(fake::unique()).is_unique_violation());
167        assert!(crate::Error::from(DbError::from(fake::unique())).is_unique_violation());
168        let raw = crate::Error::from(anyhow::Error::from(fake::unique()));
169        assert!(raw.is_unique_violation());
170        assert!(!crate::Error::from(anyhow::anyhow!("no")).is_unique_violation());
171        assert!(!crate::Error::NotFound.is_unique_violation());
172    }
173}