Skip to main content

vibe_ready/store/db/enums/
db_error.rs

1#[cfg(any(feature = "log-diesel", feature = "store-diesel-sqlite"))]
2use diesel::result::Error as DieselError;
3#[cfg(any(feature = "log-diesel", feature = "store-diesel-sqlite"))]
4use diesel::ConnectionError;
5use serde::Serialize;
6use std::panic::Location;
7use std::sync::PoisonError;
8use tokio::task::JoinError;
9
10#[repr(i32)]
11#[derive(Debug, Clone, PartialEq, Serialize)]
12/// Database error category used by storage backends.
13pub enum DbError {
14    /// Backend open operation failed.
15    OpenFailed,
16    /// Backend reported an I/O error.
17    DatabaseIOError,
18    /// Operation requires an opened database.
19    NotOpen,
20    /// Requested row was not found.
21    TargetNotFound,
22
23    /// Database worker thread or channel failed.
24    DatabaseThreadError,
25
26    /// Database lock was poisoned or could not be acquired.
27    DatabaseUnlockError,
28
29    /// Worker join operation failed.
30    JoinError,
31
32    /// Backend does not support the requested operation yet.
33    NotSupportedYet,
34    /// Page token argument is invalid.
35    InvalidArgumentPageToken,
36    /// Backend returned a business-level error.
37    GeneralBusinessError,
38}
39
40#[derive(Debug)]
41/// Detailed database error information for diagnostics.
42pub struct VibeDbErrorInfo {
43    location: String,
44    desc: String,
45    code: DbError,
46    sql: Option<String>,
47}
48
49impl VibeDbErrorInfo {
50    #[track_caller]
51    #[cfg(any(feature = "log-diesel", feature = "store-diesel-sqlite"))]
52    /// Creates error info from a Diesel connection error.
53    ///
54    /// # Returns
55    ///
56    /// A [`VibeDbErrorInfo`] with [`DbError::OpenFailed`].
57    pub fn from_connection(value: ConnectionError) -> Self {
58        let location = Self::gen_location(Location::caller());
59        VibeDbErrorInfo::new(location, value.to_string(), DbError::OpenFailed, None)
60    }
61
62    #[track_caller]
63    #[cfg(any(feature = "log-diesel", feature = "store-diesel-sqlite"))]
64    /// Creates error info from a Diesel query error.
65    ///
66    /// # Returns
67    ///
68    /// A [`VibeDbErrorInfo`] mapped to [`DbError::TargetNotFound`] or
69    /// [`DbError::DatabaseIOError`] depending on the Diesel error kind.
70    pub fn from_diesel(value: DieselError, sql: Option<&str>) -> Self {
71        let location = Self::gen_location(Location::caller());
72        let code = match value {
73            DieselError::NotFound => DbError::TargetNotFound,
74            DieselError::DatabaseError(_, _) => DbError::DatabaseIOError,
75            _ => DbError::DatabaseIOError,
76        };
77        VibeDbErrorInfo::new(
78            location,
79            value.to_string(),
80            code,
81            sql.map(std::string::ToString::to_string),
82        )
83    }
84
85    /// Creates error info from a poisoned lock.
86    ///
87    /// # Returns
88    ///
89    /// A [`VibeDbErrorInfo`] with [`DbError::DatabaseUnlockError`].
90    #[track_caller]
91    pub fn from_lock<T>(error: PoisonError<T>) -> Self {
92        let location = Self::gen_location(Location::caller());
93        let ext = VibeDbErrorInfo::new(
94            location.clone(),
95            error.to_string(),
96            DbError::DatabaseUnlockError,
97            None,
98        );
99        ext
100    }
101
102    /// Creates error info for a database worker thread failure.
103    ///
104    /// # Returns
105    ///
106    /// A [`VibeDbErrorInfo`] with [`DbError::DatabaseThreadError`].
107    #[track_caller]
108    pub fn from_thread(desc: String) -> Self {
109        let location = Self::gen_location(Location::caller());
110        let ext = VibeDbErrorInfo::new(
111            location.clone(),
112            desc.clone(),
113            DbError::DatabaseThreadError,
114            None,
115        );
116        ext
117    }
118
119    /// Creates error info from a Tokio join error.
120    ///
121    /// # Returns
122    ///
123    /// A [`VibeDbErrorInfo`] with [`DbError::JoinError`].
124    #[track_caller]
125    pub fn from_join_error(db_error: JoinError) -> Self {
126        let location = Self::gen_location(Location::caller());
127        let ext = VibeDbErrorInfo::new(
128            location.clone(),
129            db_error.to_string(),
130            DbError::JoinError,
131            None,
132        );
133        ext
134    }
135
136    /// Creates error info for an I/O or open failure.
137    ///
138    /// # Returns
139    ///
140    /// A [`VibeDbErrorInfo`] with [`DbError::OpenFailed`].
141    #[track_caller]
142    pub fn from_io(desc: String) -> Self {
143        let location = Self::gen_location(Location::caller());
144        let ext = VibeDbErrorInfo::new(
145            location.clone(),
146            desc.to_string(),
147            DbError::OpenFailed,
148            None,
149        );
150        ext
151    }
152
153    /// Creates error info for a missing target.
154    ///
155    /// # Returns
156    ///
157    /// A [`VibeDbErrorInfo`] with [`DbError::TargetNotFound`].
158    #[track_caller]
159    pub fn from_not_found() -> Self {
160        let location = Self::gen_location(Location::caller());
161        let ext = VibeDbErrorInfo::new(
162            location.clone(),
163            "Target Not Found".to_string(),
164            DbError::TargetNotFound,
165            None,
166        );
167        ext
168    }
169
170    /// Creates error info for an unsupported operation.
171    ///
172    /// # Returns
173    ///
174    /// A [`VibeDbErrorInfo`] with [`DbError::NotSupportedYet`].
175    #[track_caller]
176    pub fn from_not_supported(desc: String) -> Self {
177        let location = Self::gen_location(Location::caller());
178        VibeDbErrorInfo::new(location, desc, DbError::NotSupportedYet, None)
179    }
180
181    fn gen_location(location: &'static Location<'static>) -> String {
182        let location_str = format!(
183            "{}:{}:{}",
184            location.file(),
185            location.line(),
186            location.column()
187        );
188        location_str
189    }
190}
191
192impl VibeDbErrorInfo {
193    /// Creates detailed database error information.
194    ///
195    /// # Returns
196    ///
197    /// A [`VibeDbErrorInfo`] containing location, description, code, and SQL text.
198    ///
199    /// # Examples
200    ///
201    /// ```
202    /// use vibe_ready::{DbError, VibeDbErrorInfo};
203    ///
204    /// let info = VibeDbErrorInfo::new("file.rs:1:1".into(), "failed".into(), DbError::OpenFailed, None);
205    /// assert_eq!(info.code(), DbError::OpenFailed);
206    /// ```
207    pub fn new(location: String, desc: String, code: DbError, sql: Option<String>) -> Self {
208        Self {
209            location,
210            desc,
211            code,
212            sql,
213        }
214    }
215
216    /// Returns the source-code location captured for this error.
217    ///
218    /// # Returns
219    ///
220    /// A cloned location string.
221    pub fn location(&self) -> String {
222        self.location.clone()
223    }
224
225    /// Returns the database error description.
226    ///
227    /// # Returns
228    ///
229    /// A cloned description string.
230    pub fn desc(&self) -> String {
231        self.desc.clone()
232    }
233
234    /// Returns the database error category.
235    ///
236    /// # Returns
237    ///
238    /// The [`DbError`] variant stored in this error info.
239    pub fn code(&self) -> DbError {
240        self.code.clone()
241    }
242
243    /// Returns the SQL associated with this error, if any.
244    ///
245    /// # Returns
246    ///
247    /// A cloned SQL string, or an empty string when no SQL was captured.
248    pub fn sql(&self) -> String {
249        match &self.sql {
250            None => String::from(""),
251            Some(val) => val.clone(),
252        }
253    }
254}
255
256impl std::fmt::Display for VibeDbErrorInfo {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        write!(
259            f,
260            "DbError[{:?}] at {}: {}{}",
261            self.code,
262            self.location,
263            self.desc,
264            if let Some(sql) = &self.sql {
265                format!(" (SQL: {})", sql)
266            } else {
267                String::new()
268            }
269        )
270    }
271}
272
273#[cfg(test)]
274mod strict_tests {
275    use super::*;
276    include!(concat!(
277        env!("CARGO_MANIFEST_DIR"),
278        "/test/unit/store/db_error_tests.rs"
279    ));
280}