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