Skip to main content

sqlx_cloudflare_core/
error.rs

1use std::borrow::Cow;
2use std::error::Error as StdError;
3use std::fmt::{self, Display, Formatter};
4
5use sqlx_core::error::ErrorKind;
6
7/// An error the database returned for a statement.
8///
9/// Neither D1 nor Durable Object storage reports a structured result code
10/// across the binding, only SQLite's message as text, which ends with the code
11/// by name: `NOT NULL constraint failed: users.email: SQLITE_CONSTRAINT
12/// (extended: SQLITE_CONSTRAINT_NOTNULL)`.
13/// [`code`](sqlx_core::error::DatabaseError::code) is that name, and
14/// [`kind`](sqlx_core::error::DatabaseError::kind) is read from it.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct DatabaseError {
17    message: String,
18}
19
20impl DatabaseError {
21    /// The error for `message`, as the backend threw it.
22    #[must_use]
23    pub fn new(message: String) -> Self {
24        Self { message }
25    }
26}
27
28impl Display for DatabaseError {
29    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
30        f.write_str(&self.message)
31    }
32}
33
34impl StdError for DatabaseError {}
35
36impl sqlx_core::error::DatabaseError for DatabaseError {
37    fn message(&self) -> &str {
38        &self.message
39    }
40
41    /// SQLite's result code by name -- the extended one when the message has it, as
42    /// in `SQLITE_CONSTRAINT_UNIQUE`, else the primary one.
43    fn code(&self) -> Option<Cow<'_, str>> {
44        code(&self.message).map(Cow::Borrowed)
45    }
46
47    fn as_error(&self) -> &(dyn StdError + Send + Sync + 'static) {
48        self
49    }
50
51    fn as_error_mut(&mut self) -> &mut (dyn StdError + Send + Sync + 'static) {
52        self
53    }
54
55    fn into_error(self: Box<Self>) -> Box<dyn StdError + Send + Sync + 'static> {
56        self
57    }
58
59    fn kind(&self) -> ErrorKind {
60        kind(&self.message)
61    }
62}
63
64/// The result code at the end of a message: the `extended:` one if there
65/// is one, else the last `SQLITE_*` word.
66fn code(message: &str) -> Option<&str> {
67    let is_code_char = |c: char| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_';
68
69    if let Some((_, rest)) = message.rsplit_once("(extended: ") {
70        let end = rest.find(|c| !is_code_char(c)).unwrap_or(rest.len());
71        if end > 0 {
72            return Some(&rest[..end]);
73        }
74    }
75
76    let start = message.rfind("SQLITE_")?;
77    let rest = &message[start..];
78    Some(&rest[..rest.find(|c| !is_code_char(c)).unwrap_or(rest.len())])
79}
80
81/// The kind of error a message means.
82///
83/// From the extended result code when the message carries one, as
84/// sqlx-sqlite maps them. Failing that, from SQLite's constraint text, which
85/// is brittle -- which is why both live here and nowhere else: when a backend
86/// changes its format, this is the only thing to change, and the tests below
87/// are what notice.
88fn kind(message: &str) -> ErrorKind {
89    match code(message) {
90        Some("SQLITE_CONSTRAINT_UNIQUE" | "SQLITE_CONSTRAINT_PRIMARYKEY") => {
91            return ErrorKind::UniqueViolation
92        }
93        Some("SQLITE_CONSTRAINT_FOREIGNKEY") => return ErrorKind::ForeignKeyViolation,
94        Some("SQLITE_CONSTRAINT_NOTNULL") => return ErrorKind::NotNullViolation,
95        Some("SQLITE_CONSTRAINT_CHECK") => return ErrorKind::CheckViolation,
96        _ => {}
97    }
98
99    if message.contains("UNIQUE constraint failed") {
100        ErrorKind::UniqueViolation
101    } else if message.contains("FOREIGN KEY constraint failed") {
102        ErrorKind::ForeignKeyViolation
103    } else if message.contains("NOT NULL constraint failed") {
104        ErrorKind::NotNullViolation
105    } else if message.contains("CHECK constraint failed") {
106        ErrorKind::CheckViolation
107    } else {
108        ErrorKind::Other
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use sqlx_core::error::{DatabaseError as _, ErrorKind};
115
116    use super::DatabaseError;
117
118    fn kind(message: &str) -> ErrorKind {
119        DatabaseError::new(message.to_owned()).kind()
120    }
121
122    // As local D1 (workerd) reported it, 2026-09-23.
123    const NOT_NULL: &str = "NOT NULL constraint failed: users.email: SQLITE_CONSTRAINT \
124                            (extended: SQLITE_CONSTRAINT_NOTNULL)";
125
126    #[test]
127    fn reads_the_extended_code() {
128        let error = DatabaseError::new(NOT_NULL.to_owned());
129
130        assert_eq!(error.code().as_deref(), Some("SQLITE_CONSTRAINT_NOTNULL"));
131        assert!(matches!(error.kind(), ErrorKind::NotNullViolation));
132    }
133
134    #[test]
135    fn falls_back_to_the_primary_code() {
136        let error = DatabaseError::new(
137            "D1_ERROR: near \"SELEC\": syntax error at offset 0: SQLITE_ERROR".to_owned(),
138        );
139
140        assert_eq!(error.code().as_deref(), Some("SQLITE_ERROR"));
141        assert!(matches!(error.kind(), ErrorKind::Other));
142    }
143
144    #[test]
145    fn maps_each_extended_constraint_code() {
146        for (code, expected) in [
147            ("SQLITE_CONSTRAINT_UNIQUE", "unique"),
148            ("SQLITE_CONSTRAINT_PRIMARYKEY", "unique"),
149            ("SQLITE_CONSTRAINT_FOREIGNKEY", "foreign key"),
150            ("SQLITE_CONSTRAINT_NOTNULL", "not null"),
151            ("SQLITE_CONSTRAINT_CHECK", "check"),
152        ] {
153            let kind = kind(&format!("x: SQLITE_CONSTRAINT (extended: {code})"));
154            let actual = match kind {
155                ErrorKind::UniqueViolation => "unique",
156                ErrorKind::ForeignKeyViolation => "foreign key",
157                ErrorKind::NotNullViolation => "not null",
158                ErrorKind::CheckViolation => "check",
159                _ => "other",
160            };
161            assert_eq!(actual, expected, "{code}");
162        }
163    }
164
165    // Without the extended code, the constraint text still decides.
166    #[test]
167    fn maps_constraint_messages_to_kinds() {
168        assert!(matches!(
169            kind("D1_ERROR: UNIQUE constraint failed: users.email: SQLITE_CONSTRAINT"),
170            ErrorKind::UniqueViolation
171        ));
172        assert!(matches!(
173            kind("D1_ERROR: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT"),
174            ErrorKind::ForeignKeyViolation
175        ));
176        assert!(matches!(
177            kind("D1_ERROR: NOT NULL constraint failed: users.name: SQLITE_CONSTRAINT"),
178            ErrorKind::NotNullViolation
179        ));
180        assert!(matches!(
181            kind("D1_ERROR: CHECK constraint failed: age >= 0: SQLITE_CONSTRAINT"),
182            ErrorKind::CheckViolation
183        ));
184    }
185
186    #[test]
187    fn anything_else_is_other() {
188        assert!(matches!(
189            kind("D1_ERROR: no such table: users: SQLITE_ERROR"),
190            ErrorKind::Other
191        ));
192    }
193}