1use thiserror::Error;
2
3#[derive(Error, Debug)]
4pub enum DbError {
5 #[error("Configuration error: {0}")]
6 Config(String),
7
8 #[error("Connection error: {0}")]
9 Connection(String),
10
11 #[error("Query error: {0}")]
12 Query(String),
13
14 #[error("Transaction error: {0}")]
15 Transaction(String),
16
17 #[error("Lock error: {0}")]
18 Lock(String),
19
20 #[error("SQL injection detected: {0}")]
21 SqlInjection(String),
22
23 #[error("Invalid table name: {0}")]
24 InvalidTableName(String),
25
26 #[error("Invalid field name: {0}")]
27 InvalidFieldName(String),
28
29 #[error("IO error: {0}")]
30 Io(#[from] std::io::Error),
31
32 #[error("Parse error: {0}")]
33 Parse(String),
34
35 #[error("Pool error: {0}")]
36 Pool(String),
37
38 #[error("Timeout error: {0}")]
39 Timeout(String),
40
41 #[error("Not found: {0}")]
42 NotFound(String),
43
44 #[error("Database not initialized")]
45 NotInitialized,
46
47 #[error("Unknown error: {0}")]
48 Unknown(String),
49}
50
51pub type DbResult<T> = Result<T, DbError>;
52
53impl From<String> for DbError {
54 fn from(s: String) -> Self {
55 DbError::Unknown(s)
56 }
57}
58
59impl From<&str> for DbError {
60 fn from(s: &str) -> Self {
61 DbError::Unknown(s.to_string())
62 }
63}
64
65impl<T> From<std::sync::PoisonError<T>> for DbError {
66 fn from(e: std::sync::PoisonError<T>) -> Self {
67 DbError::Lock(e.to_string())
68 }
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use std::error::Error;
75
76 use std::sync::{Arc, Mutex};
77
78 #[test]
79 fn display_messages_for_all_variants() {
80 assert_eq!(
81 DbError::Config("bad cfg".to_string()).to_string(),
82 "Configuration error: bad cfg"
83 );
84 assert_eq!(
85 DbError::Connection("conn down".to_string()).to_string(),
86 "Connection error: conn down"
87 );
88 assert_eq!(
89 DbError::Query("bad sql".to_string()).to_string(),
90 "Query error: bad sql"
91 );
92 assert_eq!(
93 DbError::Transaction("rollback".to_string()).to_string(),
94 "Transaction error: rollback"
95 );
96 assert_eq!(
97 DbError::Lock("mutex".to_string()).to_string(),
98 "Lock error: mutex"
99 );
100 assert_eq!(
101 DbError::SqlInjection("DROP TABLE".to_string()).to_string(),
102 "SQL injection detected: DROP TABLE"
103 );
104 assert_eq!(
105 DbError::InvalidTableName("bad table".to_string()).to_string(),
106 "Invalid table name: bad table"
107 );
108 assert_eq!(
109 DbError::InvalidFieldName("bad field".to_string()).to_string(),
110 "Invalid field name: bad field"
111 );
112
113 let io_err = std::io::Error::other("disk fail");
114 assert_eq!(DbError::Io(io_err).to_string(), "IO error: disk fail");
115
116 assert_eq!(
117 DbError::Parse("invalid json".to_string()).to_string(),
118 "Parse error: invalid json"
119 );
120 assert_eq!(
121 DbError::Pool("pool exhausted".to_string()).to_string(),
122 "Pool error: pool exhausted"
123 );
124 assert_eq!(
125 DbError::Timeout("30s".to_string()).to_string(),
126 "Timeout error: 30s"
127 );
128 assert_eq!(
129 DbError::NotFound("record".to_string()).to_string(),
130 "Not found: record"
131 );
132 assert_eq!(
133 DbError::NotInitialized.to_string(),
134 "Database not initialized"
135 );
136 assert_eq!(
137 DbError::Unknown("other".to_string()).to_string(),
138 "Unknown error: other"
139 );
140 }
141
142 #[test]
143 fn from_string_maps_to_unknown() {
144 let err: DbError = String::from("string failure").into();
145 match err {
146 DbError::Unknown(msg) => assert_eq!(msg, "string failure"),
147 _ => panic!("expected DbError::Unknown"),
148 }
149 }
150
151 #[test]
152 fn from_str_maps_to_unknown() {
153 let err: DbError = "str failure".into();
154 match err {
155 DbError::Unknown(msg) => assert_eq!(msg, "str failure"),
156 _ => panic!("expected DbError::Unknown"),
157 }
158 }
159
160 #[test]
161 fn from_poison_error_maps_to_lock() {
162 let mutex = Arc::new(Mutex::new(1));
163 let mutex_in_thread = Arc::clone(&mutex);
164
165 let handle = std::thread::spawn(move || {
166 let _guard = mutex_in_thread.lock().expect("lock in worker");
167 panic!("poison mutex");
168 });
169 let _ = handle.join();
170
171 let poison_err = mutex.lock().expect_err("lock should be poisoned");
172 let err: DbError = poison_err.into();
173 match err {
174 DbError::Lock(msg) => assert!(
175 msg.contains("poison"),
176 "unexpected lock error message: {msg}"
177 ),
178 _ => panic!("expected DbError::Lock"),
179 }
180 }
181
182 #[test]
183 fn dbresult_alias_ok_and_err() {
184 assert_eq!(7_i32, 7);
185
186 let err = DbError::NotFound("id=1".to_string());
187 assert_eq!(err.to_string(), "Not found: id=1");
188 }
189
190 #[test]
191 fn error_trait_behavior() {
192 fn assert_is_error<E: Error>(_err: &E) {}
193
194 let query_err = DbError::Query("bad query".to_string());
195 assert_is_error(&query_err);
196 assert_eq!(query_err.to_string(), "Query error: bad query");
197 assert!(query_err.source().is_none());
198
199 let io_err = DbError::Io(std::io::Error::other("io root cause"));
200 assert_eq!(io_err.to_string(), "IO error: io root cause");
201 assert_eq!(
202 io_err.source().expect("io source").to_string(),
203 "io root cause"
204 );
205 }
206}