Skip to main content

dynoxide/storage_backend/
error.rs

1//! Backend-neutral error type returned by the [`StorageBackend`] trait.
2//!
3//! `BackendError` is the trait surface's error type. The native rusqlite-backed
4//! `Storage` impl converts `rusqlite::Error` into `BackendError` via
5//! [`from_rusqlite`], keeping the trait surface free of rusqlite types.
6//!
7//! `DynoxideError` is unchanged. Action handlers that call rusqlite directly
8//! through `Storage::conn()` continue to surface `DynoxideError::SqliteError`.
9//!
10//! [`StorageBackend`]: super::StorageBackend
11//! [`from_rusqlite`]: from_rusqlite
12
13/// Backend-neutral error variants surfaced by the [`StorageBackend`] trait.
14///
15/// Marked `#[non_exhaustive]`: a future backend may surface failure modes the
16/// native one cannot, so downstream code must not assume the variant set is
17/// closed. Added before the trait's first release so later additions stay
18/// non-breaking.
19///
20/// [`StorageBackend`]: super::StorageBackend
21#[derive(Debug, thiserror::Error)]
22#[non_exhaustive]
23pub enum BackendError {
24    /// The opened file is not a valid SQLite database, or is encrypted with the
25    /// wrong key.
26    #[error("backend: not a valid database")]
27    NotADatabase,
28
29    /// The database or a table within it is locked or busy.
30    #[error("backend: database is locked or busy")]
31    Locked,
32
33    /// A backend-level constraint (uniqueness, check, foreign key) was violated.
34    #[error("backend: constraint violation: {0}")]
35    Constraint(String),
36
37    /// An I/O error from the backend.
38    #[error("backend: I/O error: {0}")]
39    Io(String),
40
41    /// A client-facing validation failure raised by a backend method (for
42    /// example the tag-count limit in `set_tags`). Carries the original
43    /// message so `From<BackendError> for DynoxideError` can restore it as a
44    /// `ValidationException` rather than collapsing it to a 500.
45    #[error("{0}")]
46    Validation(String),
47
48    /// A capability the active backend does not implement (for example streams,
49    /// TTL, or the cross-item `TransactWriteItems` action on the wasm backend).
50    /// Carries a static tag so callers can distinguish which capability was
51    /// refused. Surfaces as an `InternalServerError` through
52    /// `From<BackendError> for DynoxideError`.
53    #[error("backend: operation not supported: {capability}")]
54    Unsupported {
55        /// Static identifier for the unsupported capability, e.g. `"streams"`.
56        capability: &'static str,
57    },
58
59    /// Any other backend failure. Carries the original error's `Display` output.
60    #[error("backend: {0}")]
61    Other(String),
62}
63
64/// Convert a [`rusqlite::Error`] to a [`BackendError`].
65///
66/// The mapping covers the SQLite error codes the native rusqlite impl expects
67/// to surface across the trait. Anything not explicitly mapped falls through
68/// to [`BackendError::Other`] carrying the original error's `Display` output,
69/// so no rusqlite variant produces an empty backend error.
70///
71/// This is a named helper rather than a `From` impl on purpose: the
72/// `?`-conversion would otherwise silently turn rusqlite errors into
73/// `BackendError` in code that should keep them rusqlite-typed (action handlers
74/// using `Storage::conn()` directly).
75#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
76pub fn from_rusqlite(err: rusqlite::Error) -> BackendError {
77    use rusqlite::Error::SqliteFailure;
78    use rusqlite::ErrorCode;
79
80    match &err {
81        SqliteFailure(ffi_err, msg) => match ffi_err.code {
82            ErrorCode::NotADatabase => BackendError::NotADatabase,
83            ErrorCode::DatabaseBusy | ErrorCode::DatabaseLocked => BackendError::Locked,
84            ErrorCode::ConstraintViolation => {
85                BackendError::Constraint(msg.clone().unwrap_or_default())
86            }
87            ErrorCode::SystemIoFailure => BackendError::Io(msg.clone().unwrap_or_default()),
88            _ => BackendError::Other(err.to_string()),
89        },
90        _ => BackendError::Other(err.to_string()),
91    }
92}