Skip to main content

toasty_core/
error.rs

1//! Error types for Toasty operations.
2//!
3//! This module defines [`Error`], the unified error type used throughout
4//! Toasty. Errors are cheap to clone (backed by `Arc`) and support chaining
5//! via [`Error::context`]. Each error variant has a dedicated constructor
6//! (e.g., [`Error::record_not_found`]) and a corresponding predicate
7//! (e.g., [`Error::is_record_not_found`]).
8
9mod adhoc;
10mod condition_failed;
11mod connection_lost;
12mod connection_pool;
13mod driver_operation_failed;
14mod expression_evaluation_failed;
15mod invalid_connection_url;
16mod invalid_driver_configuration;
17mod invalid_record_count;
18mod invalid_result;
19mod invalid_schema;
20mod invalid_statement;
21mod invalid_type_conversion;
22mod read_only_transaction;
23mod record_not_found;
24mod serialization_failure;
25mod transaction_timeout;
26mod unsupported_feature;
27mod validation;
28
29use adhoc::Adhoc;
30use condition_failed::ConditionFailed;
31use connection_lost::ConnectionLost;
32use connection_pool::ConnectionPool;
33use driver_operation_failed::DriverOperationFailed;
34use expression_evaluation_failed::ExpressionEvaluationFailed;
35use invalid_connection_url::InvalidConnectionUrl;
36use invalid_driver_configuration::InvalidDriverConfiguration;
37use invalid_record_count::InvalidRecordCount;
38use invalid_result::InvalidResult;
39use invalid_schema::InvalidSchema;
40use invalid_statement::InvalidStatement;
41use invalid_type_conversion::InvalidTypeConversion;
42use read_only_transaction::ReadOnlyTransaction;
43use record_not_found::RecordNotFound;
44use serialization_failure::SerializationFailure;
45use std::sync::Arc;
46use transaction_timeout::TransactionTimeout;
47use unsupported_feature::UnsupportedFeature;
48use validation::ValidationFailed;
49
50/// The error type used throughout Toasty.
51///
52/// `Error` is a thin wrapper around an `Arc`, making it cheap to clone. Errors
53/// form a chain: each error can optionally carry a *cause* that provides
54/// additional context.  When displayed, the chain is printed from outermost
55/// context to innermost root cause, separated by `: `.
56///
57/// Construct errors through the associated functions on this type
58/// (e.g., [`Error::record_not_found`], [`Error::from_args`]).
59///
60/// # Examples
61///
62/// ```
63/// use toasty_core::Error;
64///
65/// // Create an ad-hoc error
66/// let err = Error::from_args(format_args!("something went wrong"));
67/// assert_eq!(err.to_string(), "something went wrong");
68///
69/// // Wrap it with additional context
70/// let wrapped = err.context(Error::from_args(format_args!("while loading user")));
71/// assert_eq!(wrapped.to_string(), "while loading user: something went wrong");
72/// ```
73#[derive(Clone)]
74#[non_exhaustive]
75pub struct Error {
76    inner: Arc<ErrorInner>,
77}
78
79/// Trait for types that can be converted into an [`Error`].
80///
81/// This is used by [`Error::context`] to accept either an `Error` directly or
82/// any type that can be converted into one.
83///
84/// # Examples
85///
86/// ```
87/// use toasty_core::Error;
88///
89/// // Error itself implements IntoError, so you can pass it directly:
90/// let cause = Error::from_args(format_args!("root cause"));
91/// let outer = Error::from_args(format_args!("outer"));
92/// let chained = cause.context(outer);
93/// assert_eq!(chained.to_string(), "outer: root cause");
94/// ```
95pub trait IntoError {
96    /// Converts this type into an [`Error`].
97    fn into_error(self) -> Error;
98}
99
100#[derive(Debug)]
101struct ErrorInner {
102    kind: ErrorKind,
103    cause: Option<Error>,
104}
105
106#[derive(Debug)]
107enum ErrorKind {
108    Adhoc(Adhoc),
109    DriverOperationFailed(DriverOperationFailed),
110    ConnectionLost(ConnectionLost),
111    ConnectionPool(ConnectionPool),
112    ExpressionEvaluationFailed(ExpressionEvaluationFailed),
113    InvalidConnectionUrl(InvalidConnectionUrl),
114    InvalidDriverConfiguration(InvalidDriverConfiguration),
115    InvalidTypeConversion(InvalidTypeConversion),
116    InvalidRecordCount(InvalidRecordCount),
117    RecordNotFound(RecordNotFound),
118    InvalidResult(InvalidResult),
119    InvalidSchema(InvalidSchema),
120    InvalidStatement(InvalidStatement),
121    ReadOnlyTransaction(ReadOnlyTransaction),
122    SerializationFailure(SerializationFailure),
123    TransactionTimeout(TransactionTimeout),
124    UnsupportedFeature(UnsupportedFeature),
125    ValidationFailed(ValidationFailed),
126    ConditionFailed(ConditionFailed),
127}
128
129impl Error {
130    /// Wraps this error with additional context.
131    ///
132    /// The `consequent` becomes the new outermost error and `self` becomes its
133    /// cause. When displayed, the chain reads from outermost to innermost:
134    ///
135    /// ```text
136    /// consequent: self
137    /// ```
138    ///
139    /// # Panics
140    ///
141    /// Panics if `consequent` already has a cause attached.
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use toasty_core::Error;
147    ///
148    /// let root = Error::from_args(format_args!("disk full"));
149    /// let err = root.context(Error::from_args(format_args!("failed to save")));
150    /// assert_eq!(err.to_string(), "failed to save: disk full");
151    /// ```
152    pub fn context(self, consequent: impl IntoError) -> Error {
153        self.context_impl(consequent.into_error())
154    }
155
156    fn context_impl(self, consequent: Error) -> Error {
157        let mut err = consequent;
158        let inner = Arc::get_mut(&mut err.inner).unwrap();
159        assert!(
160            inner.cause.is_none(),
161            "consequent error must not already have a cause"
162        );
163        inner.cause = Some(self);
164        err
165    }
166
167    fn chain(&self) -> impl Iterator<Item = &Error> {
168        let mut err = self;
169        core::iter::once(err).chain(core::iter::from_fn(move || {
170            err = err.inner.cause.as_ref()?;
171            Some(err)
172        }))
173    }
174
175    fn kind(&self) -> &ErrorKind {
176        &self.inner.kind
177    }
178}
179
180impl std::error::Error for Error {
181    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
182        match self.kind() {
183            ErrorKind::DriverOperationFailed(err) => Some(err),
184            ErrorKind::ConnectionLost(err) => Some(err),
185            ErrorKind::ConnectionPool(err) => Some(err),
186            _ => None,
187        }
188    }
189}
190
191impl core::fmt::Display for Error {
192    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
193        let mut it = self.chain().peekable();
194        while let Some(err) = it.next() {
195            core::fmt::Display::fmt(err.kind(), f)?;
196            if it.peek().is_some() {
197                f.write_str(": ")?;
198            }
199        }
200        Ok(())
201    }
202}
203
204impl core::fmt::Debug for Error {
205    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
206        if !f.alternate() {
207            core::fmt::Display::fmt(self, f)
208        } else {
209            f.debug_struct("Error")
210                .field("kind", &self.inner.kind)
211                .field("cause", &self.inner.cause)
212                .finish()
213        }
214    }
215}
216
217impl core::fmt::Display for ErrorKind {
218    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
219        use self::ErrorKind::*;
220
221        match self {
222            Adhoc(err) => core::fmt::Display::fmt(err, f),
223            DriverOperationFailed(err) => core::fmt::Display::fmt(err, f),
224            ConnectionLost(err) => core::fmt::Display::fmt(err, f),
225            ConnectionPool(err) => core::fmt::Display::fmt(err, f),
226            ExpressionEvaluationFailed(err) => core::fmt::Display::fmt(err, f),
227            InvalidConnectionUrl(err) => core::fmt::Display::fmt(err, f),
228            InvalidDriverConfiguration(err) => core::fmt::Display::fmt(err, f),
229            InvalidTypeConversion(err) => core::fmt::Display::fmt(err, f),
230            InvalidRecordCount(err) => core::fmt::Display::fmt(err, f),
231            RecordNotFound(err) => core::fmt::Display::fmt(err, f),
232            InvalidResult(err) => core::fmt::Display::fmt(err, f),
233            InvalidSchema(err) => core::fmt::Display::fmt(err, f),
234            InvalidStatement(err) => core::fmt::Display::fmt(err, f),
235            ReadOnlyTransaction(err) => core::fmt::Display::fmt(err, f),
236            SerializationFailure(err) => core::fmt::Display::fmt(err, f),
237            TransactionTimeout(err) => core::fmt::Display::fmt(err, f),
238            UnsupportedFeature(err) => core::fmt::Display::fmt(err, f),
239            ValidationFailed(err) => core::fmt::Display::fmt(err, f),
240            ConditionFailed(err) => core::fmt::Display::fmt(err, f),
241        }
242    }
243}
244
245impl From<ErrorKind> for Error {
246    fn from(kind: ErrorKind) -> Error {
247        Error {
248            inner: Arc::new(ErrorInner { kind, cause: None }),
249        }
250    }
251}
252
253impl IntoError for Error {
254    fn into_error(self) -> Error {
255        self
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn error_size() {
265        // Ensure Error stays at one word (size of pointer/Arc)
266        let expected_size = core::mem::size_of::<usize>();
267        assert_eq!(expected_size, core::mem::size_of::<Error>());
268    }
269
270    #[test]
271    fn error_from_args() {
272        let err = Error::from_args(format_args!("test error: {}", 42));
273        assert_eq!(err.to_string(), "test error: 42");
274    }
275
276    #[test]
277    fn error_chain_display() {
278        let root = Error::from_args(format_args!("root cause"));
279        let mid = Error::from_args(format_args!("middle context"));
280        let top = Error::from_args(format_args!("top context"));
281
282        let chained = root.context(mid).context(top);
283        assert_eq!(
284            chained.to_string(),
285            "top context: middle context: root cause"
286        );
287    }
288
289    #[test]
290    fn type_conversion_error() {
291        let value = crate::stmt::Value::I64(42);
292        let err = Error::type_conversion(value, "String");
293        assert_eq!(err.to_string(), "cannot convert I64 to String");
294    }
295
296    #[test]
297    fn type_conversion_error_range() {
298        // Simulates usize conversion failure due to range
299        let value = crate::stmt::Value::U64(u64::MAX);
300        let err = Error::type_conversion(value, "usize");
301        assert_eq!(err.to_string(), "cannot convert U64 to usize");
302    }
303
304    #[test]
305    fn record_not_found_with_immediate_context() {
306        let err = Error::record_not_found("table=users key={id: 123}");
307        assert_eq!(
308            err.to_string(),
309            "record not found: table=users key={id: 123}"
310        );
311    }
312
313    #[test]
314    fn record_not_found_with_context_chain() {
315        let err = Error::record_not_found("table=users key={id: 123}")
316            .context(Error::from_args(format_args!("update query failed")))
317            .context(Error::from_args(format_args!("User.update() operation")));
318
319        assert_eq!(
320            err.to_string(),
321            "User.update() operation: update query failed: record not found: table=users key={id: 123}"
322        );
323    }
324
325    #[test]
326    fn invalid_record_count_with_context() {
327        let err = Error::invalid_record_count("expected 1 record, found multiple");
328        assert_eq!(
329            err.to_string(),
330            "invalid record count: expected 1 record, found multiple"
331        );
332    }
333
334    #[test]
335    fn invalid_result_error() {
336        let err = Error::invalid_result("expected Stream, got Count");
337        assert_eq!(
338            err.to_string(),
339            "invalid result: expected Stream, got Count"
340        );
341    }
342
343    #[test]
344    fn validation_length_too_short() {
345        let err = Error::validation_length(3, Some(5), Some(10));
346        assert_eq!(err.to_string(), "value length 3 is too short (minimum: 5)");
347    }
348
349    #[test]
350    fn validation_length_too_long() {
351        let err = Error::validation_length(15, Some(5), Some(10));
352        assert_eq!(err.to_string(), "value length 15 is too long (maximum: 10)");
353    }
354
355    #[test]
356    fn validation_length_exact_mismatch() {
357        let err = Error::validation_length(3, Some(5), Some(5));
358        assert_eq!(
359            err.to_string(),
360            "value length 3 does not match required length 5"
361        );
362    }
363
364    #[test]
365    fn validation_length_min_only() {
366        let err = Error::validation_length(3, Some(5), None);
367        assert_eq!(err.to_string(), "value length 3 is too short (minimum: 5)");
368    }
369
370    #[test]
371    fn validation_length_max_only() {
372        let err = Error::validation_length(15, None, Some(10));
373        assert_eq!(err.to_string(), "value length 15 is too long (maximum: 10)");
374    }
375
376    #[test]
377    fn condition_failed_with_context() {
378        let err = Error::condition_failed("optimistic lock version mismatch");
379        assert_eq!(
380            err.to_string(),
381            "condition failed: optimistic lock version mismatch"
382        );
383    }
384
385    #[test]
386    fn condition_failed_with_format() {
387        let expected = 1;
388        let actual = 0;
389        let err = Error::condition_failed(format!(
390            "expected {} row affected, got {}",
391            expected, actual
392        ));
393        assert_eq!(
394            err.to_string(),
395            "condition failed: expected 1 row affected, got 0"
396        );
397    }
398
399    #[test]
400    fn invalid_schema_error() {
401        let err = Error::invalid_schema("duplicate index name `idx_users`");
402        assert_eq!(
403            err.to_string(),
404            "invalid schema: duplicate index name `idx_users`"
405        );
406    }
407
408    #[test]
409    fn invalid_schema_with_context() {
410        let err = Error::invalid_schema(
411            "auto_increment column `id` in table `users` must have a numeric type, found String",
412        )
413        .context(Error::from_args(format_args!("schema verification failed")));
414        assert_eq!(
415            err.to_string(),
416            "schema verification failed: invalid schema: auto_increment column `id` in table `users` must have a numeric type, found String"
417        );
418    }
419
420    #[test]
421    fn expression_evaluation_failed() {
422        let err = Error::expression_evaluation_failed("failed to resolve argument");
423        assert_eq!(
424            err.to_string(),
425            "expression evaluation failed: failed to resolve argument"
426        );
427    }
428
429    #[test]
430    fn expression_evaluation_failed_with_context() {
431        let err = Error::expression_evaluation_failed("expected boolean value")
432            .context(Error::from_args(format_args!("query execution failed")));
433        assert_eq!(
434            err.to_string(),
435            "query execution failed: expression evaluation failed: expected boolean value"
436        );
437    }
438
439    #[test]
440    fn unsupported_feature() {
441        let err = Error::unsupported_feature("VARCHAR type is not supported by this database");
442        assert_eq!(
443            err.to_string(),
444            "unsupported feature: VARCHAR type is not supported by this database"
445        );
446    }
447
448    #[test]
449    fn unsupported_feature_with_context() {
450        let err = Error::unsupported_feature("type List is not supported by this database")
451            .context(Error::from_args(format_args!("schema creation failed")));
452        assert_eq!(
453            err.to_string(),
454            "schema creation failed: unsupported feature: type List is not supported by this database"
455        );
456    }
457
458    #[test]
459    fn invalid_driver_configuration() {
460        let err = Error::invalid_driver_configuration(
461            "native_varchar is true but storage_types.varchar is None",
462        );
463        assert_eq!(
464            err.to_string(),
465            "invalid driver configuration: native_varchar is true but storage_types.varchar is None"
466        );
467    }
468
469    #[test]
470    fn invalid_driver_configuration_with_context() {
471        let err = Error::invalid_driver_configuration("inconsistent capability flags").context(
472            Error::from_args(format_args!("driver initialization failed")),
473        );
474        assert_eq!(
475            err.to_string(),
476            "driver initialization failed: invalid driver configuration: inconsistent capability flags"
477        );
478    }
479
480    #[test]
481    fn invalid_statement_error() {
482        let err = Error::invalid_statement("field `unknown_field` does not exist on model `User`");
483        assert_eq!(
484            err.to_string(),
485            "invalid statement: field `unknown_field` does not exist on model `User`"
486        );
487    }
488
489    #[test]
490    fn invalid_statement_with_context() {
491        let err = Error::invalid_statement("cannot update primary key field `id`")
492            .context(Error::from_args(format_args!("statement lowering failed")));
493        assert_eq!(
494            err.to_string(),
495            "statement lowering failed: invalid statement: cannot update primary key field `id`"
496        );
497    }
498
499    #[test]
500    fn read_only_transaction_display() {
501        let err = Error::read_only_transaction("cannot execute UPDATE in a read-only transaction");
502        assert_eq!(
503            err.to_string(),
504            "read-only transaction: cannot execute UPDATE in a read-only transaction"
505        );
506    }
507
508    #[test]
509    fn read_only_transaction_is_predicate() {
510        let err = Error::read_only_transaction("write not allowed");
511        assert!(err.is_read_only_transaction());
512    }
513
514    #[test]
515    fn read_only_transaction_predicate_false_for_other_errors() {
516        let err = Error::serialization_failure("concurrent update conflict");
517        assert!(!err.is_read_only_transaction());
518    }
519
520    #[test]
521    fn connection_lost_predicate_and_display() {
522        let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "broken pipe");
523        let err = Error::connection_lost(io_err);
524        assert!(err.is_connection_lost());
525        assert!(!err.is_driver_operation_failed());
526        assert_eq!(err.to_string(), "connection lost: broken pipe");
527    }
528
529    #[test]
530    fn read_only_transaction_with_context() {
531        let err = Error::read_only_transaction("INSERT not allowed")
532            .context(Error::from_args(format_args!("create user failed")));
533        assert_eq!(
534            err.to_string(),
535            "create user failed: read-only transaction: INSERT not allowed"
536        );
537    }
538}