1use std::{
28 collections::{BTreeMap, BTreeSet},
29 error::Error as StdError,
30 fmt,
31 sync::Arc,
32};
33
34use datafusion::{
35 common::{DataFusionError, SchemaError},
36 logical_expr::{DdlStatement, LogicalPlan},
37 object_store::Error as ObjectStoreError,
38};
39use query_engine::object_store_error::query_error_code_for_object_store_error;
40use query_planning::error::QueryError;
41use query_runtime::native_error::{cudf_message_fact, query_error_code_for_cudf_error};
42
43pub(crate) mod client;
44mod code;
45
46use code::code_for_native_error;
47pub use code::{ErrorCode, ErrorKind, ErrorStatus};
48
49type BoxedSourceError = Box<dyn StdError + Send + Sync + 'static>;
50
51const NATIVE_FACT_COLLISION_KEYS: &str = "native_fact_collision_keys";
52const NATIVE_MEMORY_FACT_KEYS: [&str; 4] =
55 ["requested_allocation_bytes", "budget_bytes", "budget_peak_bytes", "budget_current_bytes"];
56const FACT_COLLISION_KEYS: &str = "fact_collision_keys";
57const EMPTY_FACT_KEY: &str = "<empty>";
58const FALLBACK_OPERATION: &str = "algeon_datafusion.error.unspecified_operation";
59const FALLBACK_MESSAGE: &str = "Algeon DataFusion adapter error without a message";
60
61pub type Result<T> = std::result::Result<T, Error>;
63
64const MAX_SOURCE_CHAIN_DEPTH: usize = 32;
65const MAX_SOURCE_CHAIN_MESSAGE_BYTES: usize = 512;
66const MAX_SOURCE_CHAIN_BYTES: usize = 2_048;
67const MAX_LOG_CAUSE_BYTES: usize = 512;
68const MAX_LOG_SOURCE_PATH_BYTES: usize = 1_024;
69const MAX_ALGEON_ERROR_TRAVERSAL_DEPTH: usize = 64;
70const MAX_ALGEON_ERROR_TRAVERSAL_NODES: usize = 256;
71const MAX_LOG_SAFE_FACT_COUNT: usize = 16;
72const MAX_LOG_SAFE_FACT_VALUE_BYTES: usize = 256;
73const MAX_LOG_SAFE_FACTS_RENDERED_BYTES: usize = 2_048;
74pub(crate) const ERROR_FACTS_TRUNCATED_KEY: &str = "error_facts_truncated";
75
76#[derive(Clone, PartialEq, Eq)]
83pub enum ClientDetail {
84 UnsupportedColumn {
87 name: String,
89 data_type: String,
91 role: &'static str,
93 },
94 RelationField {
96 field: String,
98 data_type: String,
100 },
101 Field {
104 name: String,
106 },
107 Column {
109 name: String,
111 },
112 OrderingKey {
114 name: String,
116 },
117 BlockingSubject {
121 text: String,
123 },
124}
125
126impl ClientDetail {
127 pub fn entries(&self) -> Vec<(&'static str, &str)> {
130 match self {
131 Self::UnsupportedColumn { name, data_type, role } => vec![
132 ("unsupported_column", name.as_str()),
133 ("unsupported_column_type", data_type.as_str()),
134 ("unsupported_column_role", role),
135 ],
136 Self::RelationField { field, data_type } => {
137 vec![("field", field.as_str()), ("actual_type", data_type.as_str())]
138 },
139 Self::Field { name } => vec![("field", name.as_str())],
140 Self::Column { name } => vec![("column", name.as_str())],
141 Self::OrderingKey { name } => vec![("key", name.as_str())],
142 Self::BlockingSubject { text } => vec![("blocking_subject", text.as_str())],
143 }
144 }
145
146 pub fn value(&self, key: &str) -> Option<&str> {
148 self.entries().into_iter().find_map(|(entry, value)| (entry == key).then_some(value))
149 }
150}
151
152impl fmt::Debug for ClientDetail {
153 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154 match self {
155 Self::UnsupportedColumn { role, .. } => {
156 f.debug_struct("UnsupportedColumn").field("role", role).finish_non_exhaustive()
157 },
158 Self::RelationField { .. } => f.debug_struct("RelationField").finish_non_exhaustive(),
159 Self::Field { .. } => f.debug_struct("Field").finish_non_exhaustive(),
160 Self::Column { .. } => f.debug_struct("Column").finish_non_exhaustive(),
161 Self::OrderingKey { .. } => f.debug_struct("OrderingKey").finish_non_exhaustive(),
162 Self::BlockingSubject { .. } => f.debug_struct("BlockingSubject").finish_non_exhaustive(),
163 }
164 }
165}
166
167pub trait DataFusionErrorExt {
169 fn find_algeon_error(&self) -> Option<&Error>;
179}
180
181impl DataFusionErrorExt for DataFusionError {
182 fn find_algeon_error(&self) -> Option<&Error> {
183 let mut visited_nodes = 0;
184 find_algeon_error_in(self, 0, &mut visited_nodes)
185 }
186}
187
188fn find_algeon_error_in<'a>(
189 error: &'a (dyn StdError + 'static),
190 depth: usize,
191 visited_nodes: &mut usize,
192) -> Option<&'a Error> {
193 if *visited_nodes >= MAX_ALGEON_ERROR_TRAVERSAL_NODES {
194 return None;
195 }
196 *visited_nodes += 1;
197
198 if let Some(error) = error.downcast_ref::<Error>() {
199 return Some(error);
200 }
201 if depth >= MAX_ALGEON_ERROR_TRAVERSAL_DEPTH {
202 return None;
203 }
204
205 if let Some(DataFusionError::Collection(errors)) = error.downcast_ref::<DataFusionError>() {
206 return errors.iter().find_map(|error| find_algeon_error_in(error, depth + 1, visited_nodes));
207 }
208
209 error.source().and_then(|source| find_algeon_error_in(source, depth + 1, visited_nodes))
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub(crate) enum FailedStatement {
215 DropTableOrView,
219 Other,
221}
222
223impl FailedStatement {
224 #[cfg_attr(
225 not(feature = "server"),
226 expect(dead_code, reason = "only the Flight statement boundary names a DDL statement")
227 )]
228 pub(crate) fn of(plan: &LogicalPlan) -> Self {
229 match plan {
230 LogicalPlan::Ddl(DdlStatement::DropTable(_) | DdlStatement::DropView(_)) => Self::DropTableOrView,
231 _ => Self::Other,
232 }
233 }
234}
235
236pub(crate) fn classify(error: &DataFusionError, statement: FailedStatement) -> ErrorCode {
244 if let Some(adapter) = error.find_algeon_error() {
245 return adapter.code();
246 }
247 let mut deepest = error;
248 let mut object_store = None;
249 let mut layer: Option<&(dyn StdError + 'static)> = Some(error);
250 for _ in 0..=MAX_SOURCE_CHAIN_DEPTH {
251 let Some(current) = layer else { break };
252 if let Some(datafusion) = current.downcast_ref::<DataFusionError>() {
253 deepest = datafusion;
254 } else if let Some(shared) = current.downcast_ref::<Arc<DataFusionError>>() {
255 deepest = shared.as_ref();
256 } else if object_store.is_none() {
257 object_store = current.downcast_ref::<ObjectStoreError>();
258 }
259 layer = current.source();
260 }
261 if let Some(object_store) = object_store {
262 return code_for_native_error(query_error_code_for_object_store_error(object_store));
263 }
264 match deepest {
265 DataFusionError::SQL(_, _) => ErrorCode::SqlParse,
266 DataFusionError::Plan(_) | DataFusionError::Configuration(_) => ErrorCode::DataFusionPlanning,
267 DataFusionError::SchemaError(schema, _) => match schema.as_ref() {
268 SchemaError::FieldNotFound { .. } => ErrorCode::FieldNotFound,
269 SchemaError::AmbiguousReference { .. }
270 | SchemaError::DuplicateQualifiedField { .. }
271 | SchemaError::DuplicateUnqualifiedField { .. } => ErrorCode::DataFusionPlanning,
272 },
273 DataFusionError::NotImplemented(_) => ErrorCode::UnsupportedOperator,
274 DataFusionError::ResourcesExhausted(_) => ErrorCode::HostMemoryExhausted,
275 DataFusionError::ExecutionJoin(join) if join.is_cancelled() => ErrorCode::Cancelled,
276 DataFusionError::Execution(_) if statement == FailedStatement::DropTableOrView => ErrorCode::ObjectNotFound,
277 _ => ErrorCode::Internal,
280 }
281}
282
283#[derive(Clone, Default, PartialEq, Eq)]
298pub struct Facts {
299 values: BTreeMap<&'static str, FactValue>,
300 collision_keys: BTreeSet<&'static str>,
301}
302
303#[derive(Clone, PartialEq, Eq)]
304struct FactValue {
305 value: String,
306 log_safe: bool,
307}
308
309pub struct LogSafeFactProjection {
311 entries: Vec<(&'static str, String)>,
312 truncated: bool,
313}
314
315impl Facts {
316 pub fn new() -> Self {
318 Self::default()
319 }
320
321 pub fn get(&self, key: &str) -> Option<&str> {
323 self.values.get(key).map(|fact| fact.value.as_str())
324 }
325
326 pub fn iter(&self) -> impl Iterator<Item = (&'static str, &str)> {
328 self.values.iter().map(|(key, fact)| (*key, fact.value.as_str()))
329 }
330
331 fn insert(&mut self, key: &'static str, value: String) {
332 self.insert_with_log_safety(key, value, false);
333 }
334
335 pub fn insert_log_safe(&mut self, key: &'static str, value: String) {
340 self.insert_with_log_safety(key, value, key != ERROR_FACTS_TRUNCATED_KEY);
344 }
345
346 fn insert_with_log_safety(&mut self, key: &'static str, value: String, log_safe: bool) {
347 if key.is_empty() {
348 self.record_collision(EMPTY_FACT_KEY);
349 return;
350 }
351 if key == FACT_COLLISION_KEYS || self.values.contains_key(key) {
352 self.record_collision(key);
353 return;
354 }
355 self.values.insert(key, FactValue { value, log_safe });
356 }
357
358 fn record_collision(&mut self, key: &'static str) {
359 self.collision_keys.insert(key);
360 let value = self.collision_keys.iter().copied().collect::<Vec<_>>().join(",");
361 self.values.insert(FACT_COLLISION_KEYS, FactValue { value, log_safe: true });
362 }
363
364 pub fn log_safe_projection(&self) -> LogSafeFactProjection {
368 let safe_facts = self
369 .values
370 .iter()
371 .filter(|(key, fact)| **key != ERROR_FACTS_TRUNCATED_KEY && fact.log_safe)
372 .map(|(key, fact)| (*key, fact.value.as_str()))
373 .collect::<Vec<_>>();
374 let mut truncated = safe_facts.len() > MAX_LOG_SAFE_FACT_COUNT;
375 let mut entries = Vec::with_capacity(safe_facts.len().min(MAX_LOG_SAFE_FACT_COUNT));
376 let mut rendered_bytes = 0usize;
377
378 for (key, raw_value) in safe_facts.into_iter().take(MAX_LOG_SAFE_FACT_COUNT) {
379 let sanitized = sanitize_log_text(raw_value);
380 let (mut value, value_truncated) = truncate_with_marker(&sanitized, MAX_LOG_SAFE_FACT_VALUE_BYTES);
381 truncated |= value_truncated;
382
383 let separator_bytes = usize::from(!entries.is_empty()) * 2;
384 let fixed_bytes = separator_bytes.saturating_add(key.len()).saturating_add(1);
385 let Some(value_budget) = MAX_LOG_SAFE_FACTS_RENDERED_BYTES
386 .checked_sub(rendered_bytes)
387 .and_then(|remaining| remaining.checked_sub(fixed_bytes))
388 else {
389 truncated = true;
390 break;
391 };
392 let (total_capped_value, total_truncated) = truncate_with_marker(&value, value_budget);
393 value = total_capped_value;
394 truncated |= total_truncated;
395 rendered_bytes = rendered_bytes.saturating_add(fixed_bytes).saturating_add(value.len());
396 entries.push((key, value));
397 if total_truncated {
398 break;
399 }
400 }
401
402 LogSafeFactProjection { entries, truncated }
403 }
404
405 pub fn retained_capture_entries(&self, client_detail: Option<&ClientDetail>) -> Vec<(String, String)> {
415 let safe = self.log_safe_projection();
416 let mut truncated = safe.truncated;
417 let mut entries =
418 safe.entries.iter().map(|(key, value)| ((*key).to_string(), value.clone())).collect::<Vec<_>>();
419 for (key, raw) in client_detail.map(ClientDetail::entries).unwrap_or_default() {
420 let (value, value_truncated) = truncate_with_marker(&sanitize_log_text(raw), MAX_LOG_SAFE_FACT_VALUE_BYTES);
421 truncated |= value_truncated;
422 entries.push((key.to_string(), value));
423 }
424 if truncated {
425 entries.push((ERROR_FACTS_TRUNCATED_KEY.to_string(), "true".to_string()));
426 }
427 entries.sort_by(|left, right| left.0.cmp(&right.0));
428 entries
429 }
430}
431
432impl fmt::Debug for Facts {
433 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
434 let projection = self.log_safe_projection();
435 f.debug_struct("Facts")
436 .field("values", &projection.entries)
437 .field(ERROR_FACTS_TRUNCATED_KEY, &projection.truncated)
438 .finish()
439 }
440}
441
442impl LogSafeFactProjection {
443 pub fn retained_entries(&self) -> Vec<(String, String)> {
446 let mut entries =
447 self.entries.iter().map(|(key, value)| ((*key).to_string(), value.clone())).collect::<Vec<_>>();
448 if self.truncated {
449 entries.push((ERROR_FACTS_TRUNCATED_KEY.to_string(), "true".to_string()));
450 entries.sort_by(|left, right| left.0.cmp(&right.0));
451 }
452 entries
453 }
454
455 pub fn rendered(&self) -> Option<String> {
459 (!self.entries.is_empty())
460 .then(|| self.entries.iter().map(|(key, value)| format!("{key}={value}")).collect::<Vec<_>>().join(", "))
461 }
462
463 pub fn truncated(&self) -> bool {
465 self.truncated
466 }
467}
468
469fn sanitize_log_text(value: &str) -> String {
470 let normalized = value.split_whitespace().collect::<Vec<_>>().join(" ");
471 if normalized.split_whitespace().any(is_credential_token) {
472 return "<redacted-credential>".to_string();
473 }
474 normalized
475 .split_whitespace()
476 .map(|token| if token.contains("://") { "<redacted-uri>" } else { token })
477 .collect::<Vec<_>>()
478 .join(" ")
479}
480
481fn is_credential_token(token: &str) -> bool {
482 let token = token
483 .trim_matches(|ch: char| matches!(ch, '(' | ')' | '[' | ']' | '{' | '}' | ',' | ';' | '"' | '\''))
484 .to_ascii_lowercase();
485 let key = token.split_once(['=', ':']).map_or(token.as_str(), |(key, _)| key);
486 let normalized_key = key.chars().filter(|ch| !matches!(ch, '-' | '_' | '.')).collect::<String>();
487 matches!(
488 normalized_key.as_str(),
489 "authorization"
490 | "proxyauthorization"
491 | "cookie"
492 | "setcookie"
493 | "password"
494 | "passwd"
495 | "apikey"
496 | "accesskey"
497 | "secret"
498 | "token"
499 | "clientsecret"
500 | "refreshtoken"
501 | "awssecretaccesskey"
502 | "secretaccesskey"
503 )
504}
505
506fn truncate_with_marker(value: &str, max_bytes: usize) -> (String, bool) {
507 if value.len() <= max_bytes {
508 return (value.to_string(), false);
509 }
510 const MARKER: &str = "...";
511 if max_bytes <= MARKER.len() {
512 return (MARKER[..max_bytes].to_string(), true);
513 }
514 let mut end = max_bytes - MARKER.len();
515 while !value.is_char_boundary(end) {
516 end -= 1;
517 }
518 (format!("{}{MARKER}", &value[..end]), true)
519}
520
521pub struct Error {
529 code: ErrorCode,
530 operation: &'static str,
531 message: String,
532 facts: Facts,
533 client_detail: Option<Box<ClientDetail>>,
536 source: Option<BoxedSourceError>,
538}
539
540#[derive(Debug, Clone, PartialEq, Eq)]
550pub struct ErrorSourceDiagnostics {
551 chain: Option<String>,
552 root_cause: Option<String>,
553}
554
555#[derive(Debug, Clone, PartialEq, Eq)]
564pub struct ErrorLogDiagnostics {
565 cause: Option<String>,
566 source_path: Option<String>,
567 truncated: bool,
568}
569
570impl ErrorLogDiagnostics {
571 pub fn from_error(error: &(dyn StdError + 'static)) -> Self {
578 let mut current = Some(error);
579 let mut cause = None;
580 let mut diagnostics = Self { cause: None, source_path: None, truncated: false };
581 for _ in 0..MAX_SOURCE_CHAIN_DEPTH {
582 let Some(error) = current else {
583 break;
584 };
585 if diagnostics.source_path.is_none() {
586 diagnostics.set_source_path(diagnostic_source_path(error));
587 }
588 let (message, message_truncated) =
589 truncate_with_marker(&log_diagnostic_error_message(error), MAX_LOG_CAUSE_BYTES);
590 diagnostics.truncated |= message_truncated;
591 if !message.is_empty() {
592 cause = Some(message);
593 }
594 current = error.source();
595 }
596 let depth_truncated = current.is_some();
597 let (cause, message_truncated) = match cause {
598 Some(cause) => {
599 let (cause, truncated) = truncate_with_marker(&cause, MAX_LOG_CAUSE_BYTES);
600 (Some(cause), truncated)
601 },
602 None => (None, false),
603 };
604 diagnostics.cause = cause;
605 diagnostics.truncated |= depth_truncated || message_truncated;
606 diagnostics
607 }
608
609 pub fn cause(&self) -> Option<&str> {
611 self.cause.as_deref()
612 }
613
614 pub fn source_path(&self) -> Option<&str> {
616 self.source_path.as_deref()
617 }
618
619 pub fn truncated(&self) -> bool {
621 self.truncated
622 }
623
624 fn set_source_path(&mut self, path: Option<&str>) {
625 let Some(path) = path.map(str::trim).filter(|path| !path.is_empty()) else {
626 return;
627 };
628 let path = if path.contains("://") { "<redacted-uri>" } else { path };
629 let (path, truncated) = truncate_with_marker(path, MAX_LOG_SOURCE_PATH_BYTES);
630 self.source_path = Some(path);
631 self.truncated |= truncated;
632 }
633}
634
635impl ErrorSourceDiagnostics {
636 pub fn from_error(error: &(dyn StdError + 'static)) -> Self {
638 Self::from_iter(Some(error))
639 }
640
641 pub fn from_source(source: Option<&(dyn StdError + 'static)>) -> Self {
644 Self::from_iter(source)
645 }
646
647 pub fn chain(&self) -> Option<&str> {
649 self.chain.as_deref()
650 }
651
652 pub fn root_cause(&self) -> Option<&str> {
654 self.root_cause.as_deref()
655 }
656
657 pub fn root_cause_when_distinct(&self) -> Option<&str> {
659 match (self.chain(), self.root_cause()) {
660 (Some(chain), Some(root_cause)) if chain != root_cause => Some(root_cause),
661 _ => None,
662 }
663 }
664
665 fn from_iter(mut current: Option<&(dyn StdError + 'static)>) -> Self {
666 let mut messages = Vec::new();
667 let mut chain_bytes = 0usize;
668 let mut chain_dropped = false;
669 for _ in 0..MAX_SOURCE_CHAIN_DEPTH {
670 let Some(error) = current else {
671 break;
672 };
673 let (message, _) = truncate_with_marker(&diagnostic_error_message(error), MAX_SOURCE_CHAIN_MESSAGE_BYTES);
678 if !message.is_empty() && messages.last() != Some(&message) {
679 chain_bytes = chain_bytes.saturating_add(message.len());
680 if chain_bytes > MAX_SOURCE_CHAIN_BYTES {
681 chain_dropped = true;
682 break;
683 }
684 messages.push(message);
685 }
686 current = error.source();
687 }
688 let root_cause = messages.last().cloned();
689 let chain = (!messages.is_empty()).then(|| {
690 let joined = messages.join(": ");
691 let (mut capped, truncated) = truncate_with_marker(&joined, MAX_SOURCE_CHAIN_BYTES);
692 if chain_dropped && !truncated {
693 capped = truncate_with_marker(&format!("{capped} ..."), MAX_SOURCE_CHAIN_BYTES).0;
694 }
695 capped
696 });
697 Self { chain, root_cause }
698 }
699}
700
701impl Error {
702 pub fn new(code: ErrorCode, operation: &'static str, message: impl Into<String>) -> Self {
708 let message = message.into();
709 let operation = if operation.is_empty() { FALLBACK_OPERATION } else { operation };
710 let message = if message.is_empty() { FALLBACK_MESSAGE.to_string() } else { message };
711 Self { code, operation, message, facts: Facts::new(), client_detail: None, source: None }
712 }
713
714 #[must_use]
724 pub fn with_fact(mut self, key: &'static str, value: impl ToString) -> Self {
725 self.facts.insert(key, value.to_string());
726 self
727 }
728
729 #[must_use]
747 pub fn with_log_safe_fact(mut self, key: &'static str, value: impl ToString) -> Self {
748 self.facts.insert_log_safe(key, value.to_string());
749 self
750 }
751
752 #[must_use]
755 pub fn with_client_detail(mut self, detail: ClientDetail) -> Self {
756 self.client_detail.get_or_insert_with(|| Box::new(detail));
757 self
758 }
759
760 pub fn with_preserved_facts(mut self, facts: Facts) -> Self {
762 self.facts = facts;
763 self
764 }
765
766 #[must_use]
771 pub fn with_source<E>(mut self, source: E) -> Self
772 where E: StdError + Send + Sync + 'static {
773 if self.source.is_none() {
774 self.source = Some(Box::new(source));
775 }
776 self
777 }
778
779 #[must_use]
784 pub fn with_boxed_source(mut self, source: Box<dyn StdError + Send + Sync + 'static>) -> Self {
785 if self.source.is_none() {
786 self.source = Some(source);
787 }
788 self
789 }
790
791 pub fn from_cudf(operation: &'static str, message: impl Into<String>, source: cudf::Error) -> Self {
798 let code = code_for_native_error(query_error_code_for_cudf_error(&source));
802 let mut error = Self::new(code, operation, message)
803 .with_log_safe_fact("cudf_code", source.code())
804 .with_log_safe_fact("cudf_kind", source.kind())
805 .with_log_safe_fact("cudf_status", source.status())
806 .with_log_safe_fact("cudf_operation", source.operation());
807 if let Some(detail) = source.ffi_detail() {
808 error = error
809 .with_log_safe_fact("failure_source", detail.failure_source)
810 .with_log_safe_fact("requested_allocation_bytes", detail.requested_bytes)
811 .with_log_safe_fact("allocation_alignment", detail.alignment)
812 .with_log_safe_fact("cuda_status", detail.cuda_error);
813 }
814 match cudf_message_fact(&source) {
815 Some(cudf_message) => error.with_fact("cudf_message", cudf_message),
816 None => error,
817 }
818 .with_source(source)
819 }
820
821 pub(crate) fn from_native(operation: &'static str, message: impl Into<String>, source: QueryError) -> Self {
835 let code = code_for_native_error(source.code());
836 let mut error = Self::new(code, operation, message)
837 .with_log_safe_fact("native_code", source.code())
838 .with_log_safe_fact("native_kind", source.kind())
839 .with_log_safe_fact("native_status", source.status())
840 .with_log_safe_fact("native_operation", source.operation())
841 .with_fact("native_message", source.message());
842 let mut collision_keys = Vec::new();
843 for (key, value) in source.wire_facts() {
844 if key == NATIVE_FACT_COLLISION_KEYS || error.facts.get(key).is_some() {
845 collision_keys.push(key);
846 continue;
847 }
848 let value = value.into_owned();
849 if NATIVE_MEMORY_FACT_KEYS.contains(&key) && value.parse::<u64>().is_ok() {
850 error.facts.insert_log_safe(key, value);
851 } else {
852 error.facts.insert(key, value);
853 }
854 }
855 if !collision_keys.is_empty() {
856 error.facts.insert_log_safe(NATIVE_FACT_COLLISION_KEYS, collision_keys.join(","));
857 }
858 error.with_source(source)
859 }
860
861 pub(crate) fn from_native_with_blocking_diagnostics(
869 operation: &'static str,
870 message: impl Into<String>,
871 source: QueryError,
872 ) -> Self {
873 const CLIENT_FACTS: &[&str] = &[
876 "device_ordinal",
877 "stage",
878 "phase",
879 "allocation_limit_bytes",
880 "physical_total_bytes",
881 "configured_limit_bytes",
882 "owned_capacity_bytes",
883 "usable_capacity_bytes",
884 "managed_capacity_bytes",
885 "backend_reserve_bytes",
886 "limit_bytes",
887 "fallback_bytes",
888 "schedule_reason",
889 "sort_rejection_reason",
890 "reason",
891 "validation_reason",
892 "relation_role",
893 "training_rows",
894 "non_finite_count",
895 "null_count",
896 "remedy",
897 "occurrence_id",
898 "node_id",
899 "input_node_id",
900 "left_rows",
901 "right_rows",
902 "output_rows",
903 "input_row_multiplicity",
904 "producer_row_count_upper_bound",
905 "consumer_row_count_upper_bound",
906 "max_compatible_managed_capacity_bytes",
907 "minimum_compatible_required_floor_bytes",
908 "minimum_compatible_dominant_owner",
909 "minimum_compatible_dominant_owner_role",
910 "minimum_compatible_dominant_owner_bytes",
911 "minimum_compatible_dominant_owner_occurrence",
912 ];
913 let mut message = message.into();
914 message.push_str(": ");
915 message.push_str(source.message());
916 let wire = source.wire_facts().collect::<BTreeMap<_, _>>();
917 let facts = CLIENT_FACTS
918 .iter()
919 .filter_map(|key| wire.get(key).map(|value| format!("{key}={value}")))
920 .collect::<Vec<_>>();
921 if !facts.is_empty() {
922 message.push_str(" [");
923 message.push_str(&facts.join(", "));
924 message.push(']');
925 }
926 let detail = match (wire.get("column"), wire.get("field")) {
927 (Some(column), _) => Some(ClientDetail::Column { name: column.to_string() }),
928 (None, Some(field)) => Some(ClientDetail::Field { name: field.to_string() }),
929 (None, None) => None,
930 };
931 let error = Self::from_native(operation, message, source);
932 match detail {
933 Some(detail) => error.with_client_detail(detail),
934 None => error,
935 }
936 }
937
938 pub fn code(&self) -> ErrorCode {
940 self.code
941 }
942
943 pub fn kind(&self) -> ErrorKind {
945 self.code.kind()
946 }
947
948 pub fn status(&self) -> ErrorStatus {
950 self.code.status()
951 }
952
953 pub fn operation(&self) -> &'static str {
955 self.operation
956 }
957
958 pub fn message(&self) -> &str {
960 &self.message
961 }
962
963 pub fn facts(&self) -> &Facts {
965 &self.facts
966 }
967
968 pub fn fact_value(&self, key: &str) -> Option<&str> {
970 self.facts.get(key)
971 }
972
973 pub fn client_detail(&self) -> Option<&ClientDetail> {
975 self.client_detail.as_deref()
976 }
977
978 pub fn source_error(&self) -> Option<&(dyn StdError + 'static)> {
980 self.source.as_deref().map(|source| source as &(dyn StdError + 'static))
981 }
982}
983
984impl fmt::Display for Error {
985 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
986 f.write_str(&self.message)
987 }
988}
989
990impl fmt::Debug for Error {
991 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
992 let source = self.source.as_ref().map(|_| "<source omitted>");
993 f.debug_struct("Error")
994 .field("code", &self.code)
995 .field("kind", &self.kind())
996 .field("status", &self.status())
997 .field("operation", &self.operation)
998 .field("message", &self.message)
999 .field("facts", &self.facts)
1000 .field("source", &source)
1001 .finish()
1002 }
1003}
1004
1005impl StdError for Error {
1006 fn source(&self) -> Option<&(dyn StdError + 'static)> {
1007 self.source_error()
1008 }
1009}
1010
1011impl From<Error> for DataFusionError {
1015 fn from(error: Error) -> Self {
1016 Self::External(Box::new(error))
1017 }
1018}
1019
1020fn diagnostic_error_message(error: &(dyn StdError + 'static)) -> String {
1021 if let Some(error) = error.downcast_ref::<Error>() {
1022 return normalize_diagnostic_message(error.message());
1023 }
1024 if let Some(error) = error.downcast_ref::<QueryError>() {
1025 return normalize_diagnostic_message(error.message());
1026 }
1027 if let Some(error) = error.downcast_ref::<DataFusionError>() {
1028 return datafusion_error_human_message(error);
1029 }
1030 normalize_diagnostic_message(&error.to_string())
1031}
1032
1033fn log_diagnostic_error_message(error: &(dyn StdError + 'static)) -> String {
1034 let message = if let Some(error) = error.downcast_ref::<cudf::Error>() {
1035 normalize_diagnostic_message(error.message())
1036 } else {
1037 diagnostic_error_message(error)
1038 };
1039 sanitize_log_text(&message)
1040}
1041
1042fn diagnostic_source_path<'a>(error: &'a (dyn StdError + 'static)) -> Option<&'a str> {
1043 if let Some(error) = error.downcast_ref::<Error>() {
1044 return error.fact_value("path");
1045 }
1046 error.downcast_ref::<QueryError>().and_then(|error| error.fact_value("path"))
1047}
1048
1049pub fn datafusion_error_human_message(error: &DataFusionError) -> String {
1055 match error {
1056 DataFusionError::Context(message, _) => normalize_diagnostic_message(message),
1057 DataFusionError::Diagnostic(diagnostic, _) => normalize_diagnostic_message(&diagnostic.message),
1058 DataFusionError::External(source) => diagnostic_error_message(source.as_ref()),
1059 DataFusionError::SQL(parser, _) => normalize_diagnostic_message(&parser.to_string()),
1060 DataFusionError::Collection(errors) => match errors.first() {
1061 Some(first) => {
1062 let first = datafusion_error_human_message(first);
1063 normalize_diagnostic_message(&format!("DataFusion error collection ({} errors): {first}", errors.len()))
1064 },
1065 None => "DataFusion error collection (0 errors)".to_string(),
1066 },
1067 DataFusionError::Shared(source) => datafusion_error_human_message(source.as_ref()),
1068 _ => normalize_diagnostic_message(&error.message()),
1069 }
1070}
1071
1072fn normalize_diagnostic_message(message: &str) -> String {
1073 message.split_whitespace().collect::<Vec<_>>().join(" ")
1074}
1075
1076#[cfg(test)]
1077mod tests;