Skip to main content

turso_orm_driver/
error.rs

1//! The driver's error type and its classification, modeled by [`Error`].
2//!
3//! Callers rarely need the exact engine error; they need to know whether to
4//! retry, whether a constraint was hit and which one, or whether the
5//! connection is gone. [`ErrorKind`] answers those questions uniformly for
6//! engine errors and for the driver's own failures, so retry loops and
7//! upsert fallbacks can be written once.
8//!
9//! The classification is partly textual because `turso::Error` carries only
10//! a string for most variants: the variant gives the broad kind, and for
11//! constraint violations and MVCC write conflicts the message is inspected.
12//! This module owns that mapping; it does not retry or log anything itself.
13//!
14//! - [`Error`]: the enum returned by every fallible operation of the crate;
15//! - [`ErrorKind`] and [`ConstraintKind`]: the classification;
16//! - [`Result`]: the crate's result alias.
17
18use std::fmt;
19
20/// The crate's result alias.
21pub type Result<T, E = Error> = std::result::Result<T, E>;
22
23/// Which constraint a statement violated.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25#[non_exhaustive]
26pub enum ConstraintKind {
27    /// A `UNIQUE` or `PRIMARY KEY` constraint.
28    Unique,
29    /// A `FOREIGN KEY` constraint.
30    ForeignKey,
31    /// A `NOT NULL` constraint.
32    NotNull,
33    /// A `CHECK` constraint.
34    Check,
35    /// Any other constraint.
36    Other,
37}
38
39/// The classification of an [`Error`].
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41#[non_exhaustive]
42pub enum ErrorKind {
43    /// Lock contention, a stale snapshot or an MVCC commit conflict. Worth
44    /// retrying.
45    Busy,
46    /// A constraint violation.
47    Constraint(ConstraintKind),
48    /// The database could not be opened or a connection could not be
49    /// established.
50    Connection,
51    /// The pool timed out handing out a connection.
52    PoolTimeout,
53    /// A value could not be decoded into the requested Rust type.
54    Decode,
55    /// A value could not be encoded as a parameter.
56    Encode,
57    /// A query returned no row where one was required.
58    NotFound,
59    /// Misuse of the API, for example contradictory options or a closed
60    /// pool.
61    Misuse,
62    /// Any other engine error.
63    Other,
64}
65
66/// The crate's error type.
67#[derive(Debug, thiserror::Error)]
68#[non_exhaustive]
69pub enum Error {
70    /// An error reported by the Turso engine, already classified.
71    #[error("{kind:?}: {source}")]
72    Turso {
73        /// The classification.
74        kind: ErrorKind,
75        /// The engine error.
76        #[source]
77        source: turso::Error,
78    },
79    /// An error reported by the serverless client, already classified.
80    #[cfg(feature = "serverless")]
81    #[cfg_attr(docsrs, doc(cfg(feature = "serverless")))]
82    #[error("{kind:?}: {source}")]
83    Remote {
84        /// The classification.
85        kind: ErrorKind,
86        /// The client error.
87        #[source]
88        source: turso_serverless::Error,
89    },
90    /// No pooled connection became free within the acquire timeout.
91    #[error("timed out waiting for a pooled connection")]
92    PoolTimeout,
93    /// A column could not be decoded into the requested Rust type.
94    #[error("cannot decode column {column} as {ty}: {reason}")]
95    Decode {
96        /// The column name or index, as the caller asked for it.
97        column: String,
98        /// The requested Rust type.
99        ty: &'static str,
100        /// Why the conversion failed.
101        reason: String,
102    },
103    /// A value could not be encoded as a parameter.
104    #[error("cannot encode value: {0}")]
105    Encode(String),
106    /// A query returned no row where one was required.
107    #[error("no row returned")]
108    NotFound,
109    /// The connect options are contradictory or unsupported.
110    #[error("invalid connect options: {0}")]
111    InvalidOptions(String),
112    /// The API was used in a way it cannot honour.
113    #[error("misuse: {0}")]
114    Misuse(String),
115    /// A free-form error, for callers layering on top of this crate.
116    #[error("{0}")]
117    Custom(String),
118}
119
120impl Error {
121    /// Classifies the error.
122    pub fn kind(&self) -> ErrorKind {
123        match self {
124            Error::Turso { kind, .. } => *kind,
125            #[cfg(feature = "serverless")]
126            Error::Remote { kind, .. } => *kind,
127            Error::PoolTimeout => ErrorKind::PoolTimeout,
128            Error::Decode { .. } => ErrorKind::Decode,
129            Error::Encode(_) => ErrorKind::Encode,
130            Error::NotFound => ErrorKind::NotFound,
131            Error::InvalidOptions(_) | Error::Misuse(_) => ErrorKind::Misuse,
132            Error::Custom(_) => ErrorKind::Other,
133        }
134    }
135
136    /// Whether the error is transient lock contention worth retrying.
137    pub fn is_busy(&self) -> bool {
138        self.kind() == ErrorKind::Busy
139    }
140
141    /// The constraint kind, when this is a constraint violation.
142    pub fn constraint(&self) -> Option<ConstraintKind> {
143        match self.kind() {
144            ErrorKind::Constraint(k) => Some(k),
145            _ => None,
146        }
147    }
148
149    /// Builds a decoding error for `column` and the requested type.
150    ///
151    /// Public so that hand-written and derived [`FromValue`](crate::FromValue)
152    /// impls report failures in the same shape as the built-in decoders.
153    pub fn decode(column: impl fmt::Display, ty: &'static str, reason: impl fmt::Display) -> Self {
154        Error::Decode {
155            column: column.to_string(),
156            ty,
157            reason: reason.to_string(),
158        }
159    }
160}
161
162impl From<turso::Error> for Error {
163    fn from(source: turso::Error) -> Self {
164        let kind = classify(&source);
165        Error::Turso { kind, source }
166    }
167}
168
169/// Classifies an engine error by variant, falling back to its message.
170///
171/// MVCC write-write conflicts are reported as a generic `Error` whose
172/// message mentions "conflict", so that case is matched textually to make
173/// it retryable like any other busy condition.
174fn classify(err: &turso::Error) -> ErrorKind {
175    match err {
176        turso::Error::Busy(_) | turso::Error::BusySnapshot(_) => ErrorKind::Busy,
177        turso::Error::Constraint(msg) => ErrorKind::Constraint(classify_constraint(msg)),
178        turso::Error::Misuse(_) => ErrorKind::Misuse,
179        turso::Error::Error(msg) if msg.to_ascii_lowercase().contains("conflict") => {
180            ErrorKind::Busy
181        }
182        turso::Error::IoError(..) | turso::Error::NotAdb(_) | turso::Error::Corrupt(_) => {
183            ErrorKind::Connection
184        }
185        _ => ErrorKind::Other,
186    }
187}
188
189/// Classifies a serverless client error by variant, falling back to its
190/// message the same way as for the engine. HTTP failures count as
191/// connection errors: the statement never reached the database.
192#[cfg(feature = "serverless")]
193impl From<turso_serverless::Error> for Error {
194    fn from(source: turso_serverless::Error) -> Self {
195        use turso_serverless::Error as E;
196        let kind = match &source {
197            E::Busy(_) | E::BusySnapshot(_) => ErrorKind::Busy,
198            E::Constraint(msg) => ErrorKind::Constraint(classify_constraint(msg)),
199            E::Misuse(_) => ErrorKind::Misuse,
200            E::Error(msg) if msg.to_ascii_lowercase().contains("conflict") => ErrorKind::Busy,
201            E::Http(_) | E::NotAdb(_) | E::Corrupt(_) => ErrorKind::Connection,
202            _ => ErrorKind::Other,
203        };
204        Error::Remote { kind, source }
205    }
206}
207
208/// Classifies a constraint violation from the engine's message, which is
209/// the only place the constraint kind is reported.
210fn classify_constraint(msg: &str) -> ConstraintKind {
211    let lower = msg.to_ascii_lowercase();
212    if lower.contains("unique") || lower.contains("primary key") {
213        ConstraintKind::Unique
214    } else if lower.contains("foreign key") {
215        ConstraintKind::ForeignKey
216    } else if lower.contains("not null") {
217        ConstraintKind::NotNull
218    } else if lower.contains("check") {
219        ConstraintKind::Check
220    } else {
221        ConstraintKind::Other
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    /// Engine errors are classified by variant, and MVCC conflicts hidden
230    /// in a generic error message are recognised as busy.
231    #[test]
232    fn classifies() {
233        let e: Error = turso::Error::Constraint("UNIQUE constraint failed: t.a".into()).into();
234        assert_eq!(e.constraint(), Some(ConstraintKind::Unique));
235        let e: Error = turso::Error::Busy("database is locked".into()).into();
236        assert!(e.is_busy());
237        let e: Error = turso::Error::Error("write-write conflict".into()).into();
238        assert!(e.is_busy());
239        let e: Error = turso::Error::Error("syntax error".into()).into();
240        assert_eq!(e.kind(), ErrorKind::Other);
241    }
242}