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 ErrorCategory {
32 pub fn error_code(&self) -> Option<u32> {
34 match self {
35 Self::ServerError => Some(0x0000),
36 Self::ProtocolError => Some(0x000A),
37 Self::AuthenticationError => Some(0x0100),
38 Self::Unavailable => Some(0x1000),
39 Self::Overloaded => Some(0x1001),
40 Self::IsBootstrapping => Some(0x1002),
41 Self::TruncateError => Some(0x1003),
42 Self::WriteTimeout => Some(0x1100),
43 Self::ReadTimeout => Some(0x1200),
44 Self::ReadFailure => Some(0x1300),
45 Self::FunctionFailure => Some(0x1400),
46 Self::WriteFailure => Some(0x1500),
47 Self::SyntaxException => Some(0x2000),
48 Self::Unauthorized => Some(0x2100),
49 Self::InvalidRequest => Some(0x2200),
50 Self::ConfigurationException => Some(0x2300),
51 Self::AlreadyExists => Some(0x2400),
52 Self::ConnectionError => None,
53 }
54 }
55
56 fn server_label(&self) -> &'static str {
58 match self {
59 Self::ServerError => "Server error",
60 Self::ProtocolError => "Protocol error",
61 Self::AuthenticationError => "Bad credentials",
62 Self::Unavailable => "Unavailable exception",
63 Self::Overloaded => "Overloaded",
64 Self::IsBootstrapping => "Is bootstrapping",
65 Self::TruncateError => "Truncate error",
66 Self::WriteTimeout => "Write timeout",
67 Self::ReadTimeout => "Read timeout",
68 Self::ReadFailure => "Read failure",
69 Self::FunctionFailure => "Function failure",
70 Self::WriteFailure => "Write failure",
71 Self::SyntaxException => "Syntax error",
72 Self::Unauthorized => "Unauthorized",
73 Self::InvalidRequest => "Invalid query",
74 Self::ConfigurationException => "Configuration error",
75 Self::AlreadyExists => "Already exists",
76 Self::ConnectionError => "Connection error",
77 }
78 }
79}
80
81impl std::fmt::Display for ErrorCategory {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 match self {
84 Self::SyntaxException => write!(f, "SyntaxException"),
85 Self::InvalidRequest => write!(f, "InvalidRequest"),
86 Self::Unauthorized => write!(f, "Unauthorized"),
87 Self::Unavailable => write!(f, "Unavailable"),
88 Self::ReadTimeout => write!(f, "ReadTimeout"),
89 Self::WriteTimeout => write!(f, "WriteTimeout"),
90 Self::ConfigurationException => write!(f, "ConfigurationException"),
91 Self::AlreadyExists => write!(f, "AlreadyExists"),
92 Self::Overloaded => write!(f, "Overloaded"),
93 Self::IsBootstrapping => write!(f, "IsBootstrapping"),
94 Self::TruncateError => write!(f, "TruncateError"),
95 Self::ReadFailure => write!(f, "ReadFailure"),
96 Self::WriteFailure => write!(f, "WriteFailure"),
97 Self::FunctionFailure => write!(f, "FunctionFailure"),
98 Self::AuthenticationError => write!(f, "AuthenticationError"),
99 Self::ServerError => write!(f, "ServerError"),
100 Self::ProtocolError => write!(f, "ProtocolError"),
101 Self::ConnectionError => write!(f, "ConnectionError"),
102 }
103 }
104}
105
106pub struct ClassifiedError {
108 pub category: ErrorCategory,
109 pub message: String,
110}
111
112pub fn classify_error(error: &anyhow::Error) -> ClassifiedError {
114 for cause in error.chain() {
116 if let Some(exec_err) = cause.downcast_ref::<ExecutionError>() {
117 if let Some(classified) = classify_execution_error(exec_err) {
118 return classified;
119 }
120 }
121 if let Some(req_err) = cause.downcast_ref::<RequestError>() {
122 if let Some(classified) = classify_request_error(req_err) {
123 return classified;
124 }
125 }
126 if let Some(attempt_err) = cause.downcast_ref::<RequestAttemptError>() {
127 if let Some(classified) = classify_attempt_error(attempt_err) {
128 return classified;
129 }
130 }
131 }
132
133 ClassifiedError {
135 category: ErrorCategory::ServerError,
136 message: error.root_cause().to_string(),
137 }
138}
139
140pub fn format_error(error: &anyhow::Error) -> String {
142 let classified = classify_error(error);
143 match classified.category.error_code() {
144 Some(code) => format!(
145 "{}: Error from server: code={:04X} [{}] message=\"{}\"",
146 classified.category,
147 code,
148 classified.category.server_label(),
149 classified.message
150 ),
151 None => format!("{}: {}", classified.category, classified.message),
152 }
153}
154
155pub fn format_error_colored(
157 error: &anyhow::Error,
158 colorizer: &crate::colorizer::CqlColorizer,
159) -> String {
160 let plain = format_error(error);
161 colorizer.colorize_error(&plain)
162}
163
164fn categorize_db_error(db_error: &DbError) -> ErrorCategory {
165 match db_error {
166 DbError::SyntaxError => ErrorCategory::SyntaxException,
167 DbError::Invalid => ErrorCategory::InvalidRequest,
168 DbError::Unauthorized => ErrorCategory::Unauthorized,
169 DbError::Unavailable { .. } => ErrorCategory::Unavailable,
170 DbError::ReadTimeout { .. } => ErrorCategory::ReadTimeout,
171 DbError::WriteTimeout { .. } => ErrorCategory::WriteTimeout,
172 DbError::ConfigError => ErrorCategory::ConfigurationException,
173 DbError::AlreadyExists { .. } => ErrorCategory::AlreadyExists,
174 DbError::Overloaded => ErrorCategory::Overloaded,
175 DbError::IsBootstrapping => ErrorCategory::IsBootstrapping,
176 DbError::TruncateError => ErrorCategory::TruncateError,
177 DbError::ReadFailure { .. } => ErrorCategory::ReadFailure,
178 DbError::WriteFailure { .. } => ErrorCategory::WriteFailure,
179 DbError::FunctionFailure { .. } => ErrorCategory::FunctionFailure,
180 DbError::AuthenticationError => ErrorCategory::AuthenticationError,
181 DbError::ServerError => ErrorCategory::ServerError,
182 DbError::ProtocolError => ErrorCategory::ProtocolError,
183 _ => ErrorCategory::ServerError,
184 }
185}
186
187fn clean_db_message(reason: &str) -> String {
189 let cleaned = reason;
190 let cleaned = cleaned
192 .strip_prefix("The submitted query has a syntax error, ")
193 .unwrap_or(cleaned);
194 let cleaned = cleaned
195 .strip_prefix("The query is syntactically correct but invalid, ")
196 .unwrap_or(cleaned);
197 let cleaned = cleaned.strip_prefix("Error message: ").unwrap_or(cleaned);
198 cleaned.to_string()
199}
200
201fn classify_execution_error(err: &ExecutionError) -> Option<ClassifiedError> {
202 match err {
203 ExecutionError::LastAttemptError(attempt) => classify_attempt_error(attempt),
204 ExecutionError::EmptyPlan => Some(ClassifiedError {
205 category: ErrorCategory::ConnectionError,
206 message: "No nodes available for query execution".to_string(),
207 }),
208 ExecutionError::RequestTimeout(dur) => Some(ClassifiedError {
209 category: ErrorCategory::ReadTimeout,
210 message: format!("Request timed out after {dur:?}"),
211 }),
212 _ => None,
213 }
214}
215
216fn classify_request_error(err: &RequestError) -> Option<ClassifiedError> {
217 match err {
218 RequestError::LastAttemptError(attempt) => classify_attempt_error(attempt),
219 RequestError::EmptyPlan => Some(ClassifiedError {
220 category: ErrorCategory::ConnectionError,
221 message: "No nodes available for query execution".to_string(),
222 }),
223 RequestError::RequestTimeout(dur) => Some(ClassifiedError {
224 category: ErrorCategory::ReadTimeout,
225 message: format!("Request timed out after {dur:?}"),
226 }),
227 _ => None,
228 }
229}
230
231fn classify_attempt_error(err: &RequestAttemptError) -> Option<ClassifiedError> {
232 match err {
233 RequestAttemptError::DbError(db_error, reason) => {
234 let category = categorize_db_error(db_error);
235 let message = clean_db_message(reason);
236 Some(ClassifiedError { category, message })
237 }
238 _ => None,
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn category_display_names() {
248 assert_eq!(
249 ErrorCategory::SyntaxException.to_string(),
250 "SyntaxException"
251 );
252 assert_eq!(ErrorCategory::InvalidRequest.to_string(), "InvalidRequest");
253 assert_eq!(ErrorCategory::Unauthorized.to_string(), "Unauthorized");
254 assert_eq!(ErrorCategory::ServerError.to_string(), "ServerError");
255 assert_eq!(
256 ErrorCategory::ConfigurationException.to_string(),
257 "ConfigurationException"
258 );
259 }
260
261 #[test]
262 fn categorize_syntax_error() {
263 assert_eq!(
264 categorize_db_error(&DbError::SyntaxError),
265 ErrorCategory::SyntaxException
266 );
267 }
268
269 #[test]
270 fn categorize_invalid() {
271 assert_eq!(
272 categorize_db_error(&DbError::Invalid),
273 ErrorCategory::InvalidRequest
274 );
275 }
276
277 #[test]
278 fn clean_strips_syntax_prefix() {
279 let msg = clean_db_message(
280 "The submitted query has a syntax error, Error message: line 1:0 no viable alternative at input 'SELEC'",
281 );
282 assert_eq!(msg, "line 1:0 no viable alternative at input 'SELEC'");
283 }
284
285 #[test]
286 fn clean_strips_invalid_prefix() {
287 let msg = clean_db_message(
288 "The query is syntactically correct but invalid, Error message: unconfigured table foo",
289 );
290 assert_eq!(msg, "unconfigured table foo");
291 }
292
293 #[test]
294 fn clean_preserves_already_clean() {
295 let msg = clean_db_message("table foo does not exist");
296 assert_eq!(msg, "table foo does not exist");
297 }
298
299 #[test]
300 fn classify_syntax_from_execution_error() {
301 let attempt = RequestAttemptError::DbError(
302 DbError::SyntaxError,
303 "Error message: line 1:0 no viable alternative at input 'SELEC'".to_string(),
304 );
305 let exec = ExecutionError::LastAttemptError(attempt);
306 let err = anyhow::Error::new(exec);
307
308 let classified = classify_error(&err);
309 assert_eq!(classified.category, ErrorCategory::SyntaxException);
310 assert_eq!(
311 classified.message,
312 "line 1:0 no viable alternative at input 'SELEC'"
313 );
314 }
315
316 #[test]
317 fn classify_invalid_from_execution_error() {
318 let attempt = RequestAttemptError::DbError(
319 DbError::Invalid,
320 "Error message: unconfigured table no_such_table".to_string(),
321 );
322 let exec = ExecutionError::LastAttemptError(attempt);
323 let err = anyhow::Error::new(exec);
324
325 let classified = classify_error(&err);
326 assert_eq!(classified.category, ErrorCategory::InvalidRequest);
327 assert_eq!(classified.message, "unconfigured table no_such_table");
328 }
329
330 #[test]
331 fn format_syntax_error() {
332 let attempt = RequestAttemptError::DbError(
333 DbError::SyntaxError,
334 "Error message: line 1:0 bad input".to_string(),
335 );
336 let exec = ExecutionError::LastAttemptError(attempt);
337 let err = anyhow::Error::new(exec);
338
339 assert_eq!(format_error(&err), "SyntaxException: Error from server: code=2000 [Syntax error] message=\"line 1:0 bad input\"");
340 }
341
342 #[test]
343 fn classify_through_anyhow_context() {
344 let attempt = RequestAttemptError::DbError(
345 DbError::SyntaxError,
346 "Error message: line 1:0 bad input".to_string(),
347 );
348 let exec = ExecutionError::LastAttemptError(attempt);
349 let err = anyhow::Error::new(exec).context("executing CQL query");
350
351 let classified = classify_error(&err);
352 assert_eq!(classified.category, ErrorCategory::SyntaxException);
353 }
354
355 #[test]
356 fn classify_fallback_unknown() {
357 let err = anyhow::anyhow!("something went wrong");
358 let classified = classify_error(&err);
359 assert_eq!(classified.category, ErrorCategory::ServerError);
360 assert_eq!(classified.message, "something went wrong");
361 }
362
363 #[test]
366 fn categorize_unauthorized() {
367 assert_eq!(
368 categorize_db_error(&DbError::Unauthorized),
369 ErrorCategory::Unauthorized
370 );
371 }
372
373 #[test]
374 fn categorize_unavailable() {
375 assert_eq!(
376 categorize_db_error(&DbError::Unavailable {
377 consistency: scylla::frame::types::Consistency::Quorum,
378 required: 2,
379 alive: 1,
380 }),
381 ErrorCategory::Unavailable
382 );
383 }
384
385 #[test]
386 fn categorize_read_timeout() {
387 assert_eq!(
388 categorize_db_error(&DbError::ReadTimeout {
389 consistency: scylla::frame::types::Consistency::One,
390 received: 0,
391 required: 1,
392 data_present: false,
393 }),
394 ErrorCategory::ReadTimeout
395 );
396 }
397
398 #[test]
399 fn categorize_write_timeout() {
400 assert_eq!(
401 categorize_db_error(&DbError::WriteTimeout {
402 consistency: scylla::frame::types::Consistency::Quorum,
403 received: 1,
404 required: 2,
405 write_type: scylla::errors::WriteType::Simple,
406 }),
407 ErrorCategory::WriteTimeout
408 );
409 }
410
411 #[test]
412 fn categorize_config_error() {
413 assert_eq!(
414 categorize_db_error(&DbError::ConfigError),
415 ErrorCategory::ConfigurationException
416 );
417 }
418
419 #[test]
420 fn categorize_already_exists() {
421 assert_eq!(
422 categorize_db_error(&DbError::AlreadyExists {
423 keyspace: "ks".to_string(),
424 table: "tbl".to_string(),
425 }),
426 ErrorCategory::AlreadyExists
427 );
428 }
429
430 #[test]
431 fn categorize_overloaded() {
432 assert_eq!(
433 categorize_db_error(&DbError::Overloaded),
434 ErrorCategory::Overloaded
435 );
436 }
437
438 #[test]
439 fn categorize_is_bootstrapping() {
440 assert_eq!(
441 categorize_db_error(&DbError::IsBootstrapping),
442 ErrorCategory::IsBootstrapping
443 );
444 }
445
446 #[test]
447 fn categorize_truncate_error() {
448 assert_eq!(
449 categorize_db_error(&DbError::TruncateError),
450 ErrorCategory::TruncateError
451 );
452 }
453
454 #[test]
455 fn categorize_read_failure() {
456 assert_eq!(
457 categorize_db_error(&DbError::ReadFailure {
458 consistency: scylla::frame::types::Consistency::One,
459 received: 1,
460 required: 1,
461 numfailures: 1,
462 data_present: false,
463 }),
464 ErrorCategory::ReadFailure
465 );
466 }
467
468 #[test]
469 fn categorize_write_failure() {
470 assert_eq!(
471 categorize_db_error(&DbError::WriteFailure {
472 consistency: scylla::frame::types::Consistency::Quorum,
473 received: 1,
474 required: 2,
475 numfailures: 1,
476 write_type: scylla::errors::WriteType::Simple,
477 }),
478 ErrorCategory::WriteFailure
479 );
480 }
481
482 #[test]
483 fn categorize_function_failure() {
484 assert_eq!(
485 categorize_db_error(&DbError::FunctionFailure {
486 keyspace: "ks".to_string(),
487 function: "fn".to_string(),
488 arg_types: vec!["int".to_string()],
489 }),
490 ErrorCategory::FunctionFailure
491 );
492 }
493
494 #[test]
495 fn categorize_authentication_error() {
496 assert_eq!(
497 categorize_db_error(&DbError::AuthenticationError),
498 ErrorCategory::AuthenticationError
499 );
500 }
501
502 #[test]
503 fn categorize_server_error() {
504 assert_eq!(
505 categorize_db_error(&DbError::ServerError),
506 ErrorCategory::ServerError
507 );
508 }
509
510 #[test]
511 fn categorize_protocol_error() {
512 assert_eq!(
513 categorize_db_error(&DbError::ProtocolError),
514 ErrorCategory::ProtocolError
515 );
516 }
517
518 #[test]
521 fn classify_empty_plan_execution() {
522 let exec = ExecutionError::EmptyPlan;
523 let err = anyhow::Error::new(exec);
524 let classified = classify_error(&err);
525 assert_eq!(classified.category, ErrorCategory::ConnectionError);
526 assert!(classified.message.contains("No nodes available"));
527 }
528
529 #[test]
530 fn classify_request_timeout_execution() {
531 let exec = ExecutionError::RequestTimeout(std::time::Duration::from_secs(10));
532 let err = anyhow::Error::new(exec);
533 let classified = classify_error(&err);
534 assert_eq!(classified.category, ErrorCategory::ReadTimeout);
535 assert!(classified.message.contains("timed out"));
536 }
537
538 #[test]
539 fn classify_empty_plan_request() {
540 let req = RequestError::EmptyPlan;
541 let err = anyhow::Error::new(req);
542 let classified = classify_error(&err);
543 assert_eq!(classified.category, ErrorCategory::ConnectionError);
544 assert!(classified.message.contains("No nodes available"));
545 }
546
547 #[test]
548 fn classify_request_timeout_request() {
549 let req = RequestError::RequestTimeout(std::time::Duration::from_secs(5));
550 let err = anyhow::Error::new(req);
551 let classified = classify_error(&err);
552 assert_eq!(classified.category, ErrorCategory::ReadTimeout);
553 assert!(classified.message.contains("timed out"));
554 }
555
556 #[test]
559 fn format_connection_error() {
560 let exec = ExecutionError::EmptyPlan;
561 let err = anyhow::Error::new(exec);
562 let formatted = format_error(&err);
563 assert!(formatted.starts_with("ConnectionError:"));
564 assert!(!formatted.contains("code="));
565 }
566
567 #[test]
570 fn error_codes_some_known() {
571 assert_eq!(ErrorCategory::SyntaxException.error_code(), Some(0x2000));
572 assert_eq!(ErrorCategory::InvalidRequest.error_code(), Some(0x2200));
573 assert_eq!(ErrorCategory::Unavailable.error_code(), Some(0x1000));
574 assert_eq!(ErrorCategory::ReadTimeout.error_code(), Some(0x1200));
575 assert_eq!(ErrorCategory::WriteTimeout.error_code(), Some(0x1100));
576 assert_eq!(ErrorCategory::ConnectionError.error_code(), None);
577 }
578
579 #[test]
582 fn server_labels() {
583 assert_eq!(
584 ErrorCategory::SyntaxException.server_label(),
585 "Syntax error"
586 );
587 assert_eq!(
588 ErrorCategory::InvalidRequest.server_label(),
589 "Invalid query"
590 );
591 assert_eq!(ErrorCategory::Unauthorized.server_label(), "Unauthorized");
592 assert_eq!(
593 ErrorCategory::Unavailable.server_label(),
594 "Unavailable exception"
595 );
596 assert_eq!(ErrorCategory::Overloaded.server_label(), "Overloaded");
597 assert_eq!(
598 ErrorCategory::IsBootstrapping.server_label(),
599 "Is bootstrapping"
600 );
601 assert_eq!(
602 ErrorCategory::TruncateError.server_label(),
603 "Truncate error"
604 );
605 assert_eq!(ErrorCategory::ReadFailure.server_label(), "Read failure");
606 assert_eq!(ErrorCategory::WriteFailure.server_label(), "Write failure");
607 assert_eq!(
608 ErrorCategory::FunctionFailure.server_label(),
609 "Function failure"
610 );
611 assert_eq!(
612 ErrorCategory::AuthenticationError.server_label(),
613 "Bad credentials"
614 );
615 assert_eq!(ErrorCategory::ServerError.server_label(), "Server error");
616 assert_eq!(
617 ErrorCategory::ProtocolError.server_label(),
618 "Protocol error"
619 );
620 assert_eq!(
621 ErrorCategory::ConnectionError.server_label(),
622 "Connection error"
623 );
624 }
625
626 #[test]
629 fn display_all_categories() {
630 let categories = vec![
631 (ErrorCategory::Unavailable, "Unavailable"),
632 (ErrorCategory::ReadTimeout, "ReadTimeout"),
633 (ErrorCategory::WriteTimeout, "WriteTimeout"),
634 (ErrorCategory::Overloaded, "Overloaded"),
635 (ErrorCategory::IsBootstrapping, "IsBootstrapping"),
636 (ErrorCategory::TruncateError, "TruncateError"),
637 (ErrorCategory::ReadFailure, "ReadFailure"),
638 (ErrorCategory::WriteFailure, "WriteFailure"),
639 (ErrorCategory::FunctionFailure, "FunctionFailure"),
640 (ErrorCategory::AuthenticationError, "AuthenticationError"),
641 (ErrorCategory::ProtocolError, "ProtocolError"),
642 (ErrorCategory::ConnectionError, "ConnectionError"),
643 (ErrorCategory::AlreadyExists, "AlreadyExists"),
644 ];
645 for (cat, expected) in categories {
646 assert_eq!(cat.to_string(), expected);
647 }
648 }
649
650 #[test]
653 fn classify_db_error_through_request_error() {
654 let attempt = RequestAttemptError::DbError(
655 DbError::Unauthorized,
656 "User has no permission".to_string(),
657 );
658 let req = RequestError::LastAttemptError(attempt);
659 let err = anyhow::Error::new(req);
660 let classified = classify_error(&err);
661 assert_eq!(classified.category, ErrorCategory::Unauthorized);
662 assert_eq!(classified.message, "User has no permission");
663 }
664}