1use scylla::errors::{DbError, ExecutionError, RequestAttemptError, RequestError};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum ErrorCategory {
11 SyntaxException,
12 InvalidRequest,
13 Unauthorized,
14 Unavailable,
15 ReadTimeout,
16 WriteTimeout,
17 ConfigurationException,
18 AlreadyExists,
19 Overloaded,
20 IsBootstrapping,
21 TruncateError,
22 ReadFailure,
23 WriteFailure,
24 FunctionFailure,
25 AuthenticationError,
26 ServerError,
27 ProtocolError,
28 ConnectionError,
29}
30
31impl std::fmt::Display for ErrorCategory {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 Self::SyntaxException => write!(f, "SyntaxException"),
35 Self::InvalidRequest => write!(f, "InvalidRequest"),
36 Self::Unauthorized => write!(f, "Unauthorized"),
37 Self::Unavailable => write!(f, "Unavailable"),
38 Self::ReadTimeout => write!(f, "ReadTimeout"),
39 Self::WriteTimeout => write!(f, "WriteTimeout"),
40 Self::ConfigurationException => write!(f, "ConfigurationException"),
41 Self::AlreadyExists => write!(f, "AlreadyExists"),
42 Self::Overloaded => write!(f, "Overloaded"),
43 Self::IsBootstrapping => write!(f, "IsBootstrapping"),
44 Self::TruncateError => write!(f, "TruncateError"),
45 Self::ReadFailure => write!(f, "ReadFailure"),
46 Self::WriteFailure => write!(f, "WriteFailure"),
47 Self::FunctionFailure => write!(f, "FunctionFailure"),
48 Self::AuthenticationError => write!(f, "AuthenticationError"),
49 Self::ServerError => write!(f, "ServerError"),
50 Self::ProtocolError => write!(f, "ProtocolError"),
51 Self::ConnectionError => write!(f, "ConnectionError"),
52 }
53 }
54}
55
56pub struct ClassifiedError {
58 pub category: ErrorCategory,
59 pub message: String,
60}
61
62pub fn classify_error(error: &anyhow::Error) -> ClassifiedError {
64 for cause in error.chain() {
66 if let Some(exec_err) = cause.downcast_ref::<ExecutionError>() {
67 if let Some(classified) = classify_execution_error(exec_err) {
68 return classified;
69 }
70 }
71 if let Some(req_err) = cause.downcast_ref::<RequestError>() {
72 if let Some(classified) = classify_request_error(req_err) {
73 return classified;
74 }
75 }
76 if let Some(attempt_err) = cause.downcast_ref::<RequestAttemptError>() {
77 if let Some(classified) = classify_attempt_error(attempt_err) {
78 return classified;
79 }
80 }
81 }
82
83 ClassifiedError {
85 category: ErrorCategory::ServerError,
86 message: error.root_cause().to_string(),
87 }
88}
89
90pub fn format_error(error: &anyhow::Error) -> String {
92 let classified = classify_error(error);
93 format!("{}: {}", classified.category, classified.message)
94}
95
96pub fn format_error_colored(
98 error: &anyhow::Error,
99 colorizer: &crate::colorizer::CqlColorizer,
100) -> String {
101 let plain = format_error(error);
102 colorizer.colorize_error(&plain)
103}
104
105fn categorize_db_error(db_error: &DbError) -> ErrorCategory {
106 match db_error {
107 DbError::SyntaxError => ErrorCategory::SyntaxException,
108 DbError::Invalid => ErrorCategory::InvalidRequest,
109 DbError::Unauthorized => ErrorCategory::Unauthorized,
110 DbError::Unavailable { .. } => ErrorCategory::Unavailable,
111 DbError::ReadTimeout { .. } => ErrorCategory::ReadTimeout,
112 DbError::WriteTimeout { .. } => ErrorCategory::WriteTimeout,
113 DbError::ConfigError => ErrorCategory::ConfigurationException,
114 DbError::AlreadyExists { .. } => ErrorCategory::AlreadyExists,
115 DbError::Overloaded => ErrorCategory::Overloaded,
116 DbError::IsBootstrapping => ErrorCategory::IsBootstrapping,
117 DbError::TruncateError => ErrorCategory::TruncateError,
118 DbError::ReadFailure { .. } => ErrorCategory::ReadFailure,
119 DbError::WriteFailure { .. } => ErrorCategory::WriteFailure,
120 DbError::FunctionFailure { .. } => ErrorCategory::FunctionFailure,
121 DbError::AuthenticationError => ErrorCategory::AuthenticationError,
122 DbError::ServerError => ErrorCategory::ServerError,
123 DbError::ProtocolError => ErrorCategory::ProtocolError,
124 _ => ErrorCategory::ServerError,
125 }
126}
127
128fn clean_db_message(reason: &str) -> String {
130 let cleaned = reason;
131 let cleaned = cleaned
133 .strip_prefix("The submitted query has a syntax error, ")
134 .unwrap_or(cleaned);
135 let cleaned = cleaned
136 .strip_prefix("The query is syntactically correct but invalid, ")
137 .unwrap_or(cleaned);
138 let cleaned = cleaned.strip_prefix("Error message: ").unwrap_or(cleaned);
139 cleaned.to_string()
140}
141
142fn classify_execution_error(err: &ExecutionError) -> Option<ClassifiedError> {
143 match err {
144 ExecutionError::LastAttemptError(attempt) => classify_attempt_error(attempt),
145 ExecutionError::EmptyPlan => Some(ClassifiedError {
146 category: ErrorCategory::ConnectionError,
147 message: "No nodes available for query execution".to_string(),
148 }),
149 ExecutionError::RequestTimeout(dur) => Some(ClassifiedError {
150 category: ErrorCategory::ReadTimeout,
151 message: format!("Request timed out after {dur:?}"),
152 }),
153 _ => None,
154 }
155}
156
157fn classify_request_error(err: &RequestError) -> Option<ClassifiedError> {
158 match err {
159 RequestError::LastAttemptError(attempt) => classify_attempt_error(attempt),
160 RequestError::EmptyPlan => Some(ClassifiedError {
161 category: ErrorCategory::ConnectionError,
162 message: "No nodes available for query execution".to_string(),
163 }),
164 RequestError::RequestTimeout(dur) => Some(ClassifiedError {
165 category: ErrorCategory::ReadTimeout,
166 message: format!("Request timed out after {dur:?}"),
167 }),
168 _ => None,
169 }
170}
171
172fn classify_attempt_error(err: &RequestAttemptError) -> Option<ClassifiedError> {
173 match err {
174 RequestAttemptError::DbError(db_error, reason) => {
175 let category = categorize_db_error(db_error);
176 let message = clean_db_message(reason);
177 Some(ClassifiedError { category, message })
178 }
179 _ => None,
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn category_display_names() {
189 assert_eq!(
190 ErrorCategory::SyntaxException.to_string(),
191 "SyntaxException"
192 );
193 assert_eq!(ErrorCategory::InvalidRequest.to_string(), "InvalidRequest");
194 assert_eq!(ErrorCategory::Unauthorized.to_string(), "Unauthorized");
195 assert_eq!(ErrorCategory::ServerError.to_string(), "ServerError");
196 assert_eq!(
197 ErrorCategory::ConfigurationException.to_string(),
198 "ConfigurationException"
199 );
200 }
201
202 #[test]
203 fn categorize_syntax_error() {
204 assert_eq!(
205 categorize_db_error(&DbError::SyntaxError),
206 ErrorCategory::SyntaxException
207 );
208 }
209
210 #[test]
211 fn categorize_invalid() {
212 assert_eq!(
213 categorize_db_error(&DbError::Invalid),
214 ErrorCategory::InvalidRequest
215 );
216 }
217
218 #[test]
219 fn clean_strips_syntax_prefix() {
220 let msg = clean_db_message(
221 "The submitted query has a syntax error, Error message: line 1:0 no viable alternative at input 'SELEC'",
222 );
223 assert_eq!(msg, "line 1:0 no viable alternative at input 'SELEC'");
224 }
225
226 #[test]
227 fn clean_strips_invalid_prefix() {
228 let msg = clean_db_message(
229 "The query is syntactically correct but invalid, Error message: unconfigured table foo",
230 );
231 assert_eq!(msg, "unconfigured table foo");
232 }
233
234 #[test]
235 fn clean_preserves_already_clean() {
236 let msg = clean_db_message("table foo does not exist");
237 assert_eq!(msg, "table foo does not exist");
238 }
239
240 #[test]
241 fn classify_syntax_from_execution_error() {
242 let attempt = RequestAttemptError::DbError(
243 DbError::SyntaxError,
244 "Error message: line 1:0 no viable alternative at input 'SELEC'".to_string(),
245 );
246 let exec = ExecutionError::LastAttemptError(attempt);
247 let err = anyhow::Error::new(exec);
248
249 let classified = classify_error(&err);
250 assert_eq!(classified.category, ErrorCategory::SyntaxException);
251 assert_eq!(
252 classified.message,
253 "line 1:0 no viable alternative at input 'SELEC'"
254 );
255 }
256
257 #[test]
258 fn classify_invalid_from_execution_error() {
259 let attempt = RequestAttemptError::DbError(
260 DbError::Invalid,
261 "Error message: unconfigured table no_such_table".to_string(),
262 );
263 let exec = ExecutionError::LastAttemptError(attempt);
264 let err = anyhow::Error::new(exec);
265
266 let classified = classify_error(&err);
267 assert_eq!(classified.category, ErrorCategory::InvalidRequest);
268 assert_eq!(classified.message, "unconfigured table no_such_table");
269 }
270
271 #[test]
272 fn format_syntax_error() {
273 let attempt = RequestAttemptError::DbError(
274 DbError::SyntaxError,
275 "Error message: line 1:0 bad input".to_string(),
276 );
277 let exec = ExecutionError::LastAttemptError(attempt);
278 let err = anyhow::Error::new(exec);
279
280 assert_eq!(format_error(&err), "SyntaxException: line 1:0 bad input");
281 }
282
283 #[test]
284 fn classify_through_anyhow_context() {
285 let attempt = RequestAttemptError::DbError(
286 DbError::SyntaxError,
287 "Error message: line 1:0 bad input".to_string(),
288 );
289 let exec = ExecutionError::LastAttemptError(attempt);
290 let err = anyhow::Error::new(exec).context("executing CQL query");
291
292 let classified = classify_error(&err);
293 assert_eq!(classified.category, ErrorCategory::SyntaxException);
294 }
295
296 #[test]
297 fn classify_fallback_unknown() {
298 let err = anyhow::anyhow!("something went wrong");
299 let classified = classify_error(&err);
300 assert_eq!(classified.category, ErrorCategory::ServerError);
301 assert_eq!(classified.message, "something went wrong");
302 }
303}