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