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