1use prost::Message;
13use std::collections::BTreeMap;
14use std::collections::HashMap;
15use std::fmt;
16use tonic::Status;
17
18const ERROR_CONDITIONS_JSON: &str = include_str!("../error-conditions.json");
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum QueryContextType {
28 SQL,
29 DataFrame,
30}
31
32impl fmt::Display for QueryContextType {
33 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34 match self {
35 QueryContextType::SQL => write!(f, "SQL"),
36 QueryContextType::DataFrame => write!(f, "DataFrame"),
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct QueryContext {
44 pub context_type: QueryContextType,
45 pub object_type: String,
46 pub object_name: String,
47 pub start_index: i32,
48 pub stop_index: i32,
49 pub fragment: String,
50 pub call_site: String,
51 pub summary: String,
52}
53
54impl QueryContext {
55 pub fn new(
57 context_type: QueryContextType,
58 object_type: String,
59 object_name: String,
60 start_index: i32,
61 stop_index: i32,
62 fragment: String,
63 call_site: String,
64 summary: String,
65 ) -> Self {
66 Self {
67 context_type,
68 object_type,
69 object_name,
70 start_index,
71 stop_index,
72 fragment,
73 call_site,
74 summary,
75 }
76 }
77
78 pub fn context_type(&self) -> QueryContextType {
80 self.context_type
81 }
82
83 pub fn object_type(&self) -> &str {
85 &self.object_type
86 }
87
88 pub fn object_name(&self) -> &str {
90 &self.object_name
91 }
92
93 pub fn start_index(&self) -> i32 {
95 self.start_index
96 }
97
98 pub fn stop_index(&self) -> i32 {
100 self.stop_index
101 }
102
103 pub fn fragment(&self) -> &str {
105 &self.fragment
106 }
107
108 pub fn call_site(&self) -> &str {
110 &self.call_site
111 }
112
113 pub fn summary(&self) -> &str {
115 &self.summary
116 }
117}
118
119#[derive(Clone, PartialEq, Message)]
125struct RpcStatus {
126 #[prost(int32, tag = "1")]
127 code: i32,
128 #[prost(string, tag = "2")]
129 message: String,
130 #[prost(message, repeated, tag = "3")]
131 details: Vec<RpcAny>,
132}
133
134#[derive(Clone, PartialEq, Message)]
136struct RpcAny {
137 #[prost(string, tag = "1")]
138 type_url: String,
139 #[prost(bytes, tag = "2")]
140 value: Vec<u8>,
141}
142
143#[derive(Clone, PartialEq, Message)]
145struct RpcErrorInfo {
146 #[prost(string, tag = "1")]
147 reason: String,
148 #[prost(string, tag = "2")]
149 domain: String,
150 #[prost(map = "string, string", tag = "3")]
151 metadata: HashMap<String, String>,
152}
153
154#[derive(Debug, Clone)]
156pub struct SparkError {
157 pub kind: SparkErrorKind,
159 pub error_class: String,
161 pub params: BTreeMap<String, String>,
163 pub message: String,
165 pub sql_state: Option<String>,
167 pub contexts: Vec<QueryContext>,
169 pub server_stacktrace: Option<String>,
171 pub grpc_code: Option<i32>,
173 pub grpc_details: Option<Vec<u8>>,
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum SparkErrorKind {
184 ValueError,
186 TypeError,
188 IndexError,
190 AttributeError,
192 KeyError,
194 RuntimeError,
196 NotImplementedError,
198 AssertionError,
200 PicklingError,
202 ImportError,
204 Connect,
206 ConnectGrpc,
208 Analysis,
210 SessionNotSame,
212 TempTableAlreadyExists,
214 Parse,
216 IllegalArgument,
218 Arithmetic,
220 UnsupportedOperation,
222 ArrayIndexOutOfBounds,
224 DateTime,
226 NumberFormat,
228 StreamingQuery,
230 StreamingPythonRunnerInitialization,
232 QueryExecution,
234 Python,
236 SparkRuntime,
238 SparkUpgrade,
240 SparkNoSuchElement,
242 Unknown,
244 InvalidPlanInput,
246 PickleException,
248}
249
250impl SparkError {
251 pub fn value(error_class: &str, params: &[(&str, &str)]) -> Self {
253 Self::classed(SparkErrorKind::ValueError, error_class, params)
254 }
255
256 pub fn connect_msg(message: impl Into<String>) -> Self {
258 Self {
259 kind: SparkErrorKind::Connect,
260 error_class: String::new(),
261 params: BTreeMap::new(),
262 message: message.into(),
263 sql_state: None,
264 contexts: Vec::new(),
265 server_stacktrace: None,
266 grpc_code: None,
267 grpc_details: None,
268 }
269 }
270
271 pub fn value_msg(message: impl Into<String>) -> Self {
278 Self {
279 kind: SparkErrorKind::ValueError,
280 error_class: String::new(),
281 params: BTreeMap::new(),
282 message: message.into(),
283 sql_state: None,
284 contexts: Vec::new(),
285 server_stacktrace: None,
286 grpc_code: None,
287 grpc_details: None,
288 }
289 }
290
291 pub fn classed(kind: SparkErrorKind, error_class: &str, params: &[(&str, &str)]) -> Self {
293 Self {
294 kind,
295 error_class: error_class.to_string(),
296 params: params
297 .iter()
298 .map(|(k, v)| (k.to_string(), v.to_string()))
299 .collect(),
300 message: String::new(),
301 sql_state: None,
302 contexts: Vec::new(),
303 server_stacktrace: None,
304 grpc_code: None,
305 grpc_details: None,
306 }
307 }
308
309 pub fn message(&self) -> String {
317 if self.error_class.is_empty() {
318 return self.message.clone();
319 }
320
321 if let Ok(rendered) = render_message(&self.error_class, &self.params) {
323 format!("[{}] {}", self.error_class, rendered)
324 } else {
325 if self.message.is_empty() {
327 format!("[{}]", self.error_class)
328 } else {
329 format!("[{}] {}", self.error_class, self.message)
330 }
331 }
332 }
333
334 pub fn sql_state(&self) -> Option<String> {
336 if self.sql_state.is_some() {
337 self.sql_state.clone()
338 } else if !self.error_class.is_empty() {
339 get_sql_state(&self.error_class).map(|s| s.to_string())
340 } else {
341 None
342 }
343 }
344
345 pub fn from_grpc_status(status: Status) -> Self {
349 let message = status.message().to_string();
351 let code = status.code() as i32;
352
353 let details = status.details().to_vec();
355 let mut err = if details.is_empty() {
356 Self::connect_msg(format!("[{}] {}", status.code(), message))
357 } else {
358 match parse_error_info_from_details(&details) {
360 Some((error_class, params, sql_state, server_stacktrace)) => Self {
361 kind: classify_error_kind_with_classes(&error_class, ¶ms),
362 error_class,
363 params,
364 message,
365 sql_state,
366 contexts: Vec::new(),
367 server_stacktrace,
368 grpc_code: None,
369 grpc_details: None,
370 },
371 None => Self::connect_msg(format!("[{}] {}", status.code(), message)),
372 }
373 };
374 err.grpc_code = Some(code);
376 if !details.is_empty() {
377 err.grpc_details = Some(details);
378 }
379 err
380 }
381
382 pub fn get_condition(&self) -> Option<String> {
386 if self.error_class.is_empty() {
387 None
388 } else {
389 Some(self.error_class.clone())
390 }
391 }
392
393 pub fn get_error_class(&self) -> Option<String> {
397 self.get_condition()
398 }
399
400 pub fn get_message_parameters(&self) -> Option<BTreeMap<String, String>> {
404 if self.params.is_empty() && self.error_class.is_empty() {
405 None
406 } else {
407 Some(self.params.clone())
408 }
409 }
410
411 pub fn get_sql_state(&self) -> Option<String> {
415 if self.sql_state.is_some() {
416 self.sql_state.clone()
417 } else if !self.error_class.is_empty() {
418 get_sql_state(&self.error_class).map(|s| s.to_string())
419 } else {
420 None
421 }
422 }
423
424 pub fn get_message(&self) -> String {
428 self.message()
429 }
430
431 pub fn get_query_context(&self) -> Vec<QueryContext> {
435 self.contexts.clone()
436 }
437
438 pub fn get_stacktrace(&self) -> Option<String> {
440 self.server_stacktrace.clone()
441 }
442
443 pub fn grpc_code(&self) -> Option<i32> {
445 self.grpc_code
446 }
447
448 pub fn grpc_details(&self) -> Option<&[u8]> {
450 self.grpc_details.as_deref()
451 }
452}
453
454impl std::fmt::Display for SparkError {
455 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
456 write!(f, "{}", self.message())
457 }
458}
459
460impl std::error::Error for SparkError {}
461
462pub type Result<T> = std::result::Result<T, SparkError>;
463
464fn parse_error_info_from_details(
473 details: &[u8],
474) -> Option<(
475 String,
476 BTreeMap<String, String>,
477 Option<String>,
478 Option<String>,
479)> {
480 let rpc_status = RpcStatus::decode(details).ok()?;
482
483 for any_detail in &rpc_status.details {
485 if any_detail.type_url == "type.googleapis.com/google.rpc.ErrorInfo" {
487 if let Ok(error_info) = RpcErrorInfo::decode(&any_detail.value[..]) {
489 let error_class = error_info
491 .metadata
492 .get("errorClass")
493 .cloned()
494 .unwrap_or_else(|| error_info.reason.clone());
495
496 let sql_state = error_info.metadata.get("sqlState").cloned();
497 let stacktrace = error_info.metadata.get("stackTrace").cloned();
498
499 let params: BTreeMap<String, String> = error_info
501 .metadata
502 .iter()
503 .map(|(k, v)| (k.clone(), v.clone()))
504 .collect();
505
506 return Some((error_class, params, sql_state, stacktrace));
508 }
509 }
510 }
511
512 None
513}
514
515fn classify_error_kind_with_classes(
522 error_class: &str,
523 params: &BTreeMap<String, String>,
524) -> SparkErrorKind {
525 if let Some(classes) = params.get("classes") {
526 let checks: &[(&str, SparkErrorKind)] = &[
528 ("AnalysisException", SparkErrorKind::Analysis),
529 ("ParseException", SparkErrorKind::Parse),
530 ("StreamingQueryException", SparkErrorKind::StreamingQuery),
531 ("SparkUpgradeException", SparkErrorKind::SparkUpgrade),
532 ("NumberFormatException", SparkErrorKind::NumberFormat),
533 (
534 "ArrayIndexOutOfBoundsException",
535 SparkErrorKind::ArrayIndexOutOfBounds,
536 ),
537 ("DateTimeException", SparkErrorKind::DateTime),
538 ("ArithmeticException", SparkErrorKind::Arithmetic),
539 (
540 "UnsupportedOperationException",
541 SparkErrorKind::UnsupportedOperation,
542 ),
543 ("IllegalArgumentException", SparkErrorKind::IllegalArgument),
544 ("NoSuchElementException", SparkErrorKind::SparkNoSuchElement),
545 ("PythonException", SparkErrorKind::Python),
546 ("SparkRuntimeException", SparkErrorKind::SparkRuntime),
547 ];
548 for (needle, kind) in checks {
549 if classes.contains(needle) {
550 return *kind;
551 }
552 }
553 }
554 classify_error_kind(error_class)
555}
556
557fn classify_error_kind(error_class: &str) -> SparkErrorKind {
561 if error_class.starts_with("ANALYSIS") {
562 SparkErrorKind::Analysis
563 } else if error_class.starts_with("PARSE") {
564 SparkErrorKind::Parse
565 } else if error_class.starts_with("ILLEGAL") {
566 SparkErrorKind::IllegalArgument
567 } else if error_class.starts_with("UNSUPPORTED") {
568 SparkErrorKind::UnsupportedOperation
569 } else if error_class.starts_with("ARITHMETIC") {
570 SparkErrorKind::Arithmetic
571 } else if error_class.contains("NOT_IMPLEMENTED") {
572 SparkErrorKind::NotImplementedError
573 } else if error_class.contains("RUNTIME") {
574 SparkErrorKind::RuntimeError
575 } else if error_class.contains("VALUE") {
576 SparkErrorKind::ValueError
577 } else {
578 match error_class {
579 "INVALID_CONNECT_URL" => SparkErrorKind::RuntimeError,
580 "SYNTAX_ERROR" => SparkErrorKind::Parse,
581 "INVALID_PLAN_INPUT" => SparkErrorKind::InvalidPlanInput,
582 "PICKLE_ERROR" => SparkErrorKind::PickleException,
583 "RESPONSE_ALREADY_RECEIVED" | "INVALID_HANDLE" => SparkErrorKind::ConnectGrpc,
584 _ => SparkErrorKind::RuntimeError, }
586 }
587}
588
589fn render_message(
599 error_class: &str,
600 params: &BTreeMap<String, String>,
601) -> std::result::Result<String, String> {
602 let template = get_message_template(error_class)?;
603
604 let mut result = template.clone();
606
607 let pattern = regex::Regex::new(r"<([a-zA-Z0-9_\-]+)>").map_err(|e| e.to_string())?;
610
611 for cap in pattern.captures_iter(&template) {
612 if let Some(param_name) = cap.get(1) {
613 let name = param_name.as_str();
614 if let Some(value) = params.get(name) {
615 let placeholder = format!("<{}>", name);
616 result = result.replace(&placeholder, value);
617 }
618 }
619 }
620
621 Ok(result)
622}
623
624fn get_message_template(error_class: &str) -> std::result::Result<String, String> {
626 let parts: Vec<&str> = error_class.split('.').collect();
628
629 let json_obj = parse_error_conditions_json()
630 .map_err(|e| format!("Failed to parse error-conditions.json: {}", e))?;
631
632 match parts.len() {
633 1 => {
634 let main_class = parts[0];
635 if let Some(entry) = json_obj.get(main_class) {
636 if let Some(msg_array) = entry.get("message") {
637 if let Some(msg_list) = msg_array.as_array() {
638 let message_parts: Vec<String> = msg_list
639 .iter()
640 .filter_map(|m| m.as_str().map(|s| s.to_string()))
641 .collect();
642 return Ok(message_parts.join("\n"));
643 }
644 }
645 }
646 Err(format!("Error class not found: {}", main_class))
647 }
648 2 => {
649 let main_class = parts[0];
650 let sub_class = parts[1];
651 if let Some(entry) = json_obj.get(main_class) {
652 let mut message = String::new();
654 if let Some(msg_array) = entry.get("message") {
655 if let Some(msg_list) = msg_array.as_array() {
656 let message_parts: Vec<String> = msg_list
657 .iter()
658 .filter_map(|m| m.as_str().map(|s| s.to_string()))
659 .collect();
660 message = message_parts.join("\n");
661 }
662 }
663
664 if let Some(subclasses) = entry.get("sub_class") {
666 if let Some(sub) = subclasses.get(sub_class) {
667 if let Some(sub_msg_array) = sub.get("message") {
668 if let Some(sub_msg_list) = sub_msg_array.as_array() {
669 let sub_message_parts: Vec<String> = sub_msg_list
670 .iter()
671 .filter_map(|m| m.as_str().map(|s| s.to_string()))
672 .collect();
673 if !message.is_empty() {
674 message.push(' ');
675 }
676 message.push_str(&sub_message_parts.join("\n"));
677 }
678 }
679 }
680 }
681
682 if !message.is_empty() {
683 Ok(message)
684 } else {
685 Err(format!("No message found for error class: {}", error_class))
686 }
687 } else {
688 Err(format!("Main error class not found: {}", main_class))
689 }
690 }
691 _ => Err(format!("Invalid error class format: {}", error_class)),
692 }
693}
694
695fn get_sql_state(error_class: &str) -> Option<String> {
697 let parts: Vec<&str> = error_class.split('.').collect();
698
699 let json_obj = parse_error_conditions_json().ok()?;
700
701 match parts.len() {
702 1 => {
703 let main_class = parts[0];
704 json_obj
705 .get(main_class)
706 .and_then(|entry| entry.get("sqlState"))
707 .and_then(|state| state.as_str())
708 .map(|s| s.to_string())
709 }
710 2 => {
711 let main_class = parts[0];
712 let sub_class = parts[1];
713 json_obj
714 .get(main_class)
715 .and_then(|entry| entry.get("sub_class"))
716 .and_then(|subclasses| subclasses.get(sub_class))
717 .and_then(|sub| sub.get("sqlState"))
718 .and_then(|state| state.as_str())
719 .map(|s| s.to_string())
720 }
721 _ => None,
722 }
723}
724
725fn parse_error_conditions_json(
727) -> std::result::Result<serde_json::Map<String, serde_json::Value>, String> {
728 let json: serde_json::Value = serde_json::from_str(ERROR_CONDITIONS_JSON)
729 .map_err(|e| format!("Failed to parse JSON: {}", e))?;
730 match json {
731 serde_json::Value::Object(map) => Ok(map),
732 _ => Err("Expected JSON object at root".to_string()),
733 }
734}
735
736#[cfg(test)]
737mod tests {
738 use super::*;
739
740 #[test]
741 fn test_render_simple_message() {
742 let mut params = BTreeMap::new();
743 params.insert("arg_name".to_string(), "x".to_string());
744
745 let result = render_message("CANNOT_BE_NONE", ¶ms).expect("should render");
746 assert_eq!(result, "Argument `x` cannot be None.");
747 }
748
749 #[test]
750 fn test_error_message_format() {
751 let mut params = BTreeMap::new();
752 params.insert("arg_name".to_string(), "x".to_string());
753
754 let err = SparkError::classed(
755 SparkErrorKind::ValueError,
756 "CANNOT_BE_NONE",
757 &[("arg_name", "x")],
758 );
759
760 let msg = err.message();
761 assert_eq!(msg, "[CANNOT_BE_NONE] Argument `x` cannot be None.");
762 }
763
764 #[test]
765 fn test_error_display() {
766 let err = SparkError::classed(
767 SparkErrorKind::ValueError,
768 "CANNOT_BE_NONE",
769 &[("arg_name", "test_arg")],
770 );
771
772 let display = format!("{}", err);
773 assert_eq!(
774 display,
775 "[CANNOT_BE_NONE] Argument `test_arg` cannot be None."
776 );
777 }
778
779 #[test]
780 fn test_connect_msg() {
781 let err = SparkError::connect_msg("Connection failed");
782 let msg = err.message();
783 assert_eq!(msg, "Connection failed");
784 }
785
786 #[test]
787 fn test_get_sql_state_known() {
788 let sql_state = get_sql_state("ATTRIBUTE_NOT_SUPPORTED");
789 assert_eq!(sql_state, Some("0A000".to_string()));
790 }
791
792 #[test]
793 fn test_get_sql_state_unknown() {
794 let sql_state = get_sql_state("UNKNOWN_ERROR_CLASS_XXXXX");
795 assert_eq!(sql_state, None);
796 }
797
798 #[test]
799 fn test_multiple_params() {
800 let result = render_message(
801 "INVALID_CONNECT_URL",
802 &[(
803 "detail".to_string(),
804 "The URL must start with 'sc://'".to_string(),
805 )]
806 .iter()
807 .cloned()
808 .collect(),
809 );
810 assert!(result.is_ok());
811 }
812
813 #[test]
814 fn test_full_error_message_with_params() {
815 let err = SparkError::classed(
817 SparkErrorKind::ValueError,
818 "CANNOT_BE_NONE",
819 &[("arg_name", "my_arg")],
820 );
821 assert_eq!(
822 err.message(),
823 "[CANNOT_BE_NONE] Argument `my_arg` cannot be None."
824 );
825
826 let err = SparkError::classed(
828 SparkErrorKind::ValueError,
829 "ARGUMENT_REQUIRED",
830 &[("arg_name", "foo"), ("condition", "x > 0")],
831 );
832 assert_eq!(
833 err.message(),
834 "[ARGUMENT_REQUIRED] Argument `foo` is required when x > 0."
835 );
836
837 let err = SparkError::classed(
839 SparkErrorKind::RuntimeError,
840 "INVALID_CONNECT_URL",
841 &[("detail", "The URL must start with sc://")],
842 );
843 assert_eq!(
844 err.message(),
845 "[INVALID_CONNECT_URL] Invalid URL for Spark Connect: The URL must start with sc://"
846 );
847
848 let err = SparkError::classed(
850 SparkErrorKind::ValueError,
851 "ATTRIBUTE_NOT_CALLABLE",
852 &[("attr_name", "compute"), ("obj_name", "MyClass")],
853 );
854 assert_eq!(
855 err.message(),
856 "[ATTRIBUTE_NOT_CALLABLE] Attribute `compute` in provided object `MyClass` is not callable."
857 );
858 }
859
860 #[test]
861 fn test_error_without_error_class() {
862 let mut err = SparkError::connect_msg("Custom error message");
863 assert_eq!(err.message(), "Custom error message");
864
865 err.message = "Another message".to_string();
867 assert_eq!(err.message(), "Another message");
868 }
869
870 #[test]
871 fn test_error_kind_variants() {
872 let _ = SparkErrorKind::ValueError;
874 let _ = SparkErrorKind::TypeError;
875 let _ = SparkErrorKind::IndexError;
876 let _ = SparkErrorKind::AttributeError;
877 let _ = SparkErrorKind::KeyError;
878 let _ = SparkErrorKind::RuntimeError;
879 let _ = SparkErrorKind::NotImplementedError;
880 let _ = SparkErrorKind::AssertionError;
881 let _ = SparkErrorKind::PicklingError;
882 let _ = SparkErrorKind::ImportError;
883 let _ = SparkErrorKind::Connect;
884 let _ = SparkErrorKind::ConnectGrpc;
885 let _ = SparkErrorKind::Analysis;
886 let _ = SparkErrorKind::Parse;
887 let _ = SparkErrorKind::Python;
888 let _ = SparkErrorKind::Unknown;
889 }
890
891 #[test]
892 fn test_message_rendering_parity_with_python() {
893 let err = SparkError::classed(
901 SparkErrorKind::ValueError,
902 "CANNOT_BE_NONE",
903 &[("arg_name", "x")],
904 );
905 assert_eq!(
906 err.message(),
907 "[CANNOT_BE_NONE] Argument `x` cannot be None."
908 );
909
910 let err = SparkError::classed(
912 SparkErrorKind::ValueError,
913 "ARGUMENT_REQUIRED",
914 &[("arg_name", "foo"), ("condition", "x > 0")],
915 );
916 assert_eq!(
917 err.message(),
918 "[ARGUMENT_REQUIRED] Argument `foo` is required when x > 0."
919 );
920
921 let err = SparkError::classed(
923 SparkErrorKind::RuntimeError,
924 "INVALID_CONNECT_URL",
925 &[("detail", "test")],
926 );
927 assert_eq!(
928 err.message(),
929 "[INVALID_CONNECT_URL] Invalid URL for Spark Connect: test"
930 );
931
932 let err = SparkError::classed(
934 SparkErrorKind::ValueError,
935 "ATTRIBUTE_NOT_CALLABLE",
936 &[("attr_name", "compute"), ("obj_name", "MyClass")],
937 );
938 assert_eq!(
939 err.message(),
940 "[ATTRIBUTE_NOT_CALLABLE] Attribute `compute` in provided object `MyClass` is not callable."
941 );
942 }
943
944 #[test]
945 fn test_parse_error_info_from_grpc_status() {
946 let mut error_metadata = HashMap::new();
948 error_metadata.insert("errorClass".to_string(), "ANALYSIS_ERROR".to_string());
949 error_metadata.insert("sqlState".to_string(), "42601".to_string());
950 error_metadata.insert(
951 "messageParameters".to_string(),
952 r#"{"message":"test error"}"#.to_string(),
953 );
954 error_metadata.insert(
955 "stackTrace".to_string(),
956 "at org.apache.spark.sql....".to_string(),
957 );
958
959 let error_info = RpcErrorInfo {
960 reason: "ANALYSIS_ERROR".to_string(),
961 domain: "org.apache.spark.connect".to_string(),
962 metadata: error_metadata,
963 };
964
965 let error_info_bytes = error_info.encode_to_vec();
967 let any = RpcAny {
968 type_url: "type.googleapis.com/google.rpc.ErrorInfo".to_string(),
969 value: error_info_bytes,
970 };
971
972 let rpc_status = RpcStatus {
974 code: 0,
975 message: "Analysis error".to_string(),
976 details: vec![any],
977 };
978
979 let status_bytes = rpc_status.encode_to_vec();
980
981 let result = parse_error_info_from_details(&status_bytes);
983 assert!(result.is_some());
984
985 let (error_class, params, sql_state, stacktrace) = result.unwrap();
986 assert_eq!(error_class, "ANALYSIS_ERROR");
987 assert_eq!(sql_state, Some("42601".to_string()));
988 assert_eq!(stacktrace, Some("at org.apache.spark.sql....".to_string()));
989 assert!(params.contains_key("errorClass"));
990 }
991
992 #[test]
993 fn test_classify_error_kind() {
994 assert_eq!(
995 classify_error_kind("ANALYSIS_ERROR"),
996 SparkErrorKind::Analysis
997 );
998 assert_eq!(classify_error_kind("PARSE_ERROR"), SparkErrorKind::Parse);
999 assert_eq!(
1000 classify_error_kind("ILLEGAL_ARGUMENT"),
1001 SparkErrorKind::IllegalArgument
1002 );
1003 assert_eq!(
1004 classify_error_kind("ARITHMETIC_ERROR"),
1005 SparkErrorKind::Arithmetic
1006 );
1007 assert_eq!(
1008 classify_error_kind("UNSUPPORTED_OPERATION"),
1009 SparkErrorKind::UnsupportedOperation
1010 );
1011 assert_eq!(
1012 classify_error_kind("INVALID_PLAN_INPUT"),
1013 SparkErrorKind::InvalidPlanInput
1014 );
1015 assert_eq!(
1016 classify_error_kind("RESPONSE_ALREADY_RECEIVED"),
1017 SparkErrorKind::ConnectGrpc
1018 );
1019 }
1020
1021 #[test]
1022 fn test_accessor_methods_get_condition() {
1023 let err = SparkError::classed(
1025 SparkErrorKind::ValueError,
1026 "CANNOT_BE_NONE",
1027 &[("arg_name", "test")],
1028 );
1029 assert_eq!(err.get_condition(), Some("CANNOT_BE_NONE".to_string()));
1030 assert_eq!(err.get_error_class(), Some("CANNOT_BE_NONE".to_string()));
1031
1032 let err = SparkError::connect_msg("Plain message");
1034 assert_eq!(err.get_condition(), None);
1035 assert_eq!(err.get_error_class(), None);
1036 }
1037
1038 #[test]
1039 fn test_accessor_methods_get_message_parameters() {
1040 let err = SparkError::classed(
1042 SparkErrorKind::ValueError,
1043 "CANNOT_BE_NONE",
1044 &[("arg_name", "x"), ("other", "y")],
1045 );
1046 let params = err.get_message_parameters();
1047 assert!(params.is_some());
1048 let params = params.unwrap();
1049 assert_eq!(params.get("arg_name"), Some(&"x".to_string()));
1050 assert_eq!(params.get("other"), Some(&"y".to_string()));
1051
1052 let err = SparkError::connect_msg("Plain message");
1054 assert_eq!(err.get_message_parameters(), None);
1055 }
1056
1057 #[test]
1058 fn test_accessor_methods_get_sql_state() {
1059 let err = SparkError::classed(
1061 SparkErrorKind::ValueError,
1062 "ATTRIBUTE_NOT_SUPPORTED",
1063 &[("attr_name", "test")],
1064 );
1065 let sql_state = err.get_sql_state();
1066 assert_eq!(sql_state, Some("0A000".to_string()));
1067
1068 let err = SparkError::classed(
1070 SparkErrorKind::ValueError,
1071 "CANNOT_BE_NONE",
1072 &[("arg_name", "x")],
1073 );
1074 let sql_state = err.get_sql_state();
1075 assert_eq!(sql_state, None);
1076 }
1077
1078 #[test]
1079 fn test_accessor_methods_get_message() {
1080 let err = SparkError::classed(
1081 SparkErrorKind::ValueError,
1082 "CANNOT_BE_NONE",
1083 &[("arg_name", "x")],
1084 );
1085 assert_eq!(
1086 err.get_message(),
1087 "[CANNOT_BE_NONE] Argument `x` cannot be None."
1088 );
1089
1090 let err = SparkError::connect_msg("Custom message");
1092 assert_eq!(err.get_message(), "Custom message");
1093 }
1094
1095 #[test]
1096 fn test_accessor_methods_get_query_context() {
1097 let mut err = SparkError::connect_msg("Test error");
1098 assert!(err.get_query_context().is_empty());
1099
1100 let ctx = QueryContext::new(
1102 QueryContextType::SQL,
1103 "VIEW".to_string(),
1104 "my_view".to_string(),
1105 0,
1106 10,
1107 "SELECT * FROM".to_string(),
1108 "file.py:10".to_string(),
1109 "In DataFrame operation".to_string(),
1110 );
1111 err.contexts.push(ctx);
1112 let contexts = err.get_query_context();
1113 assert_eq!(contexts.len(), 1);
1114 assert_eq!(contexts[0].context_type(), QueryContextType::SQL);
1115 assert_eq!(contexts[0].object_type(), "VIEW");
1116 assert_eq!(contexts[0].object_name(), "my_view");
1117 }
1118
1119 #[test]
1120 fn test_query_context_creation() {
1121 let ctx = QueryContext::new(
1122 QueryContextType::DataFrame,
1123 "".to_string(),
1124 "".to_string(),
1125 5,
1126 15,
1127 "df.select(col('x'))".to_string(),
1128 "test.py:42".to_string(),
1129 "Selecting column x".to_string(),
1130 );
1131 assert_eq!(ctx.context_type(), QueryContextType::DataFrame);
1132 assert_eq!(ctx.start_index(), 5);
1133 assert_eq!(ctx.stop_index(), 15);
1134 assert_eq!(ctx.fragment(), "df.select(col('x'))");
1135 assert_eq!(ctx.call_site(), "test.py:42");
1136 assert_eq!(ctx.summary(), "Selecting column x");
1137 }
1138
1139 #[test]
1140 fn test_stacktrace_accessor() {
1141 let mut err = SparkError::connect_msg("Error with stack trace");
1142 assert_eq!(err.get_stacktrace(), None);
1143
1144 err.server_stacktrace = Some("at org.apache.spark.sql...".to_string());
1145 assert_eq!(
1146 err.get_stacktrace(),
1147 Some("at org.apache.spark.sql...".to_string())
1148 );
1149 }
1150
1151 #[test]
1152 fn test_parity_cannot_be_none() {
1153 let err = SparkError::classed(
1156 SparkErrorKind::ValueError,
1157 "CANNOT_BE_NONE",
1158 &[("arg_name", "x")],
1159 );
1160 assert_eq!(
1161 err.message(),
1162 "[CANNOT_BE_NONE] Argument `x` cannot be None."
1163 );
1164 assert_eq!(err.get_condition(), Some("CANNOT_BE_NONE".to_string()));
1165 let params = err.get_message_parameters().unwrap();
1166 assert_eq!(params.get("arg_name"), Some(&"x".to_string()));
1167 }
1168
1169 #[test]
1170 fn test_parity_argument_required() {
1171 let err = SparkError::classed(
1173 SparkErrorKind::ValueError,
1174 "ARGUMENT_REQUIRED",
1175 &[("arg_name", "foo"), ("condition", "x > 0")],
1176 );
1177 assert_eq!(
1178 err.message(),
1179 "[ARGUMENT_REQUIRED] Argument `foo` is required when x > 0."
1180 );
1181 }
1182
1183 #[test]
1184 fn test_parity_invalid_connect_url() {
1185 let err = SparkError::classed(
1187 SparkErrorKind::RuntimeError,
1188 "INVALID_CONNECT_URL",
1189 &[("detail", "must start with sc://")],
1190 );
1191 assert_eq!(
1192 err.message(),
1193 "[INVALID_CONNECT_URL] Invalid URL for Spark Connect: must start with sc://"
1194 );
1195 }
1196
1197 #[test]
1198 fn test_parity_attribute_not_callable() {
1199 let err = SparkError::classed(
1201 SparkErrorKind::ValueError,
1202 "ATTRIBUTE_NOT_CALLABLE",
1203 &[("attr_name", "compute"), ("obj_name", "MyClass")],
1204 );
1205 assert_eq!(
1206 err.message(),
1207 "[ATTRIBUTE_NOT_CALLABLE] Attribute `compute` in provided object `MyClass` is not callable."
1208 );
1209 }
1210
1211 #[test]
1212 fn test_analysis_exception_mapping() {
1213 let err = SparkError::classed(
1214 SparkErrorKind::Analysis,
1215 "ANALYSIS_ERROR",
1216 &[("message", "Table not found")],
1217 );
1218 assert_eq!(err.kind, SparkErrorKind::Analysis);
1219 assert_eq!(err.get_condition(), Some("ANALYSIS_ERROR".to_string()));
1220 }
1221
1222 #[test]
1223 fn test_illegal_argument_exception_mapping() {
1224 let err = SparkError::classed(SparkErrorKind::IllegalArgument, "ILLEGAL_ARGUMENT", &[]);
1225 assert_eq!(err.kind, SparkErrorKind::IllegalArgument);
1226 assert_eq!(err.get_condition(), Some("ILLEGAL_ARGUMENT".to_string()));
1227 }
1228
1229 #[test]
1230 fn test_arithmetic_exception_mapping() {
1231 let err = SparkError::classed(
1232 SparkErrorKind::Arithmetic,
1233 "ARITHMETIC_ERROR",
1234 &[("message", "Division by zero")],
1235 );
1236 assert_eq!(err.kind, SparkErrorKind::Arithmetic);
1237 }
1238
1239 #[test]
1240 fn test_unsupported_operation_exception_mapping() {
1241 let err = SparkError::classed(
1242 SparkErrorKind::UnsupportedOperation,
1243 "UNSUPPORTED_OPERATION",
1244 &[],
1245 );
1246 assert_eq!(err.kind, SparkErrorKind::UnsupportedOperation);
1247 }
1248
1249 #[test]
1250 fn test_query_execution_exception_mapping() {
1251 let err = SparkError::classed(
1252 SparkErrorKind::QueryExecution,
1253 "QUERY_EXECUTION_ERROR",
1254 &[("message", "Stage failed")],
1255 );
1256 assert_eq!(err.kind, SparkErrorKind::QueryExecution);
1257 }
1258
1259 #[test]
1260 fn test_streaming_query_exception_mapping() {
1261 let err = SparkError::classed(SparkErrorKind::StreamingQuery, "STREAMING_QUERY_ERROR", &[]);
1262 assert_eq!(err.kind, SparkErrorKind::StreamingQuery);
1263 }
1264
1265 #[test]
1266 fn test_python_exception_mapping() {
1267 let err = SparkError::classed(
1268 SparkErrorKind::Python,
1269 "PYTHON_ERROR",
1270 &[("message", "Python worker failed")],
1271 );
1272 assert_eq!(err.kind, SparkErrorKind::Python);
1273 }
1274
1275 #[test]
1276 fn test_spark_runtime_exception_mapping() {
1277 let err = SparkError::classed(SparkErrorKind::SparkRuntime, "SPARK_RUNTIME_ERROR", &[]);
1278 assert_eq!(err.kind, SparkErrorKind::SparkRuntime);
1279 }
1280
1281 #[test]
1282 fn test_connect_grpc_exception_mapping() {
1283 let err = SparkError::classed(
1284 SparkErrorKind::ConnectGrpc,
1285 "RESPONSE_ALREADY_RECEIVED",
1286 &[],
1287 );
1288 assert_eq!(err.kind, SparkErrorKind::ConnectGrpc);
1289 }
1290
1291 #[test]
1292 fn test_invalid_plan_input_exception_mapping() {
1293 let err = SparkError::classed(
1294 SparkErrorKind::InvalidPlanInput,
1295 "INVALID_PLAN_INPUT",
1296 &[("message", "Invalid plan")],
1297 );
1298 assert_eq!(err.kind, SparkErrorKind::InvalidPlanInput);
1299 }
1300}