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 `DynoxideError::UnsupportedCapability` (HTTP 501
52    /// with the `UnsupportedOperation` envelope) through
53    /// `From<BackendError> for DynoxideError`.
54    #[error("backend: operation not supported: {capability}")]
55    Unsupported {
56        /// Static identifier for the unsupported capability, e.g. `"streams"`.
57        capability: &'static str,
58    },
59
60    /// OPFS is present but its pool could not be acquired - typically another tab
61    /// holds the database's sync access handles. wasm backend only; restored by
62    /// `From<BackendError> for DynoxideError` to a stable
63    /// `com.dynoxide.wasm#OpfsUnavailable` envelope a browser client can react to.
64    #[error("{0}")]
65    OpfsUnavailable(String),
66
67    /// Any other backend failure. Carries the original error's `Display` output.
68    #[error("backend: {0}")]
69    Other(String),
70}
71
72/// Convert a [`rusqlite::Error`] to a [`BackendError`].
73///
74/// The mapping covers the SQLite error codes the native rusqlite impl expects
75/// to surface across the trait. Anything not explicitly mapped falls through
76/// to [`BackendError::Other`] carrying the original error's `Display` output,
77/// so no rusqlite variant produces an empty backend error.
78///
79/// This is a named helper rather than a `From` impl on purpose: the
80/// `?`-conversion would otherwise silently turn rusqlite errors into
81/// `BackendError` in code that should keep them rusqlite-typed (action handlers
82/// using `Storage::conn()` directly).
83#[cfg(any(feature = "native-sqlite", feature = "_has-encryption"))]
84pub fn from_rusqlite(err: rusqlite::Error) -> BackendError {
85    use rusqlite::Error::SqliteFailure;
86    use rusqlite::ErrorCode;
87
88    match &err {
89        SqliteFailure(ffi_err, msg) => match ffi_err.code {
90            ErrorCode::NotADatabase => BackendError::NotADatabase,
91            ErrorCode::DatabaseBusy | ErrorCode::DatabaseLocked => BackendError::Locked,
92            ErrorCode::ConstraintViolation => {
93                BackendError::Constraint(msg.clone().unwrap_or_default())
94            }
95            ErrorCode::SystemIoFailure => BackendError::Io(msg.clone().unwrap_or_default()),
96            _ => BackendError::Other(err.to_string()),
97        },
98        _ => BackendError::Other(err.to_string()),
99    }
100}