1use datafusion::arrow::error::ArrowError;
16use datafusion::error::DataFusionError;
17use datafusion::sql::sqlparser::parser::ParserError;
18
19pub mod matched {
27 pub const OPTIMIZER_RULE_FAILED: &str = "Optimizer rule";
31
32 pub const TOKENIZER_UNTERMINATED: &str = "Unterminated";
37
38 pub const TOKENIZER_EOF: &str = "EOF";
43}
44
45pub fn user_message(err: &DataFusionError) -> String {
48 match err {
49 DataFusionError::ArrowError(e, _) => arrow_message(e),
50 DataFusionError::ParquetError(e) => format!("Parquet: {e}"),
51 DataFusionError::ObjectStore(e) => e.to_string(),
52 DataFusionError::IoError(e) => e.to_string(),
53 DataFusionError::SQL(e, _) => parser_message(e),
54 DataFusionError::NotImplemented(m)
55 | DataFusionError::Plan(m)
56 | DataFusionError::Configuration(m)
57 | DataFusionError::Execution(m)
58 | DataFusionError::ResourcesExhausted(m)
59 | DataFusionError::Substrait(m)
60 | DataFusionError::Ffi(m) => m.clone(),
61 DataFusionError::Internal(m) => format!("internal error: {m}"),
62 DataFusionError::SchemaError(e, _) => e.to_string(),
63 DataFusionError::ExecutionJoin(e) => e.to_string(),
64 DataFusionError::External(e) => external_message(e.as_ref()),
65 DataFusionError::Context(context, inner)
67 if context.starts_with(matched::OPTIMIZER_RULE_FAILED) =>
68 {
69 user_message(inner)
70 }
71 DataFusionError::Context(context, inner) => {
72 format!("{context}: {}", user_message(inner))
73 }
74 DataFusionError::Diagnostic(_, inner) => user_message(inner),
75 DataFusionError::Collection(errs) => errs
76 .first()
77 .map(user_message)
78 .unwrap_or_else(|| "unknown error".to_string()),
79 DataFusionError::Shared(inner) => user_message(inner),
80 }
81}
82
83fn parser_message(e: &ParserError) -> String {
84 match e {
85 ParserError::TokenizerError(m) | ParserError::ParserError(m) => {
86 format!("syntax error: {m}")
87 }
88 ParserError::RecursionLimitExceeded => {
89 "syntax error: the statement is nested too deeply".to_string()
90 }
91 }
92}
93
94fn arrow_message(e: &ArrowError) -> String {
95 match e {
96 ArrowError::ExternalError(source) => external_message(source.as_ref()),
97 other => other.to_string(),
98 }
99}
100
101fn external_message(e: &(dyn std::error::Error + Send + Sync + 'static)) -> String {
102 if let Some(df) = e.downcast_ref::<DataFusionError>() {
103 user_message(df)
104 } else if let Some(arrow) = e.downcast_ref::<ArrowError>() {
105 arrow_message(arrow)
106 } else {
107 e.to_string()
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use std::sync::Arc;
114
115 use datafusion::common::tree_node::Transformed;
116 use datafusion::common::DFSchema;
117 use datafusion::logical_expr::{EmptyRelation, LogicalPlan};
118 use datafusion::optimizer::{Optimizer, OptimizerConfig, OptimizerContext, OptimizerRule};
119 use datafusion::prelude::SessionContext;
120 use datafusion::sql::sqlparser::dialect::GenericDialect;
121 use datafusion::sql::sqlparser::tokenizer::Tokenizer;
122
123 use super::*;
124
125 #[derive(Debug)]
127 struct Failing;
128
129 impl OptimizerRule for Failing {
130 fn name(&self) -> &str {
131 "failing"
132 }
133
134 fn supports_rewrite(&self) -> bool {
135 true
136 }
137
138 fn rewrite(
139 &self,
140 _plan: LogicalPlan,
141 _config: &dyn OptimizerConfig,
142 ) -> datafusion::error::Result<Transformed<LogicalPlan>> {
143 Err(DataFusionError::Plan("the cause".into()))
144 }
145 }
146
147 #[test]
148 fn datafusion_still_labels_a_failed_optimizer_rule() {
149 let optimizer = Optimizer::with_rules(vec![Arc::new(Failing)]);
150 let config = OptimizerContext::new().with_skip_failing_rules(false);
151 let plan = LogicalPlan::EmptyRelation(EmptyRelation {
152 produce_one_row: false,
153 schema: Arc::new(DFSchema::empty()),
154 });
155 let err = optimizer
156 .optimize(plan, &config, |_, _| {})
157 .expect_err("the rule fails");
158 let DataFusionError::Context(label, _) = &err else {
159 panic!(
160 "DataFusion no longer wraps a failed optimizer rule's error in a context: {err:?}"
161 );
162 };
163 assert!(
164 label.starts_with(matched::OPTIMIZER_RULE_FAILED),
165 "DataFusion's label for a failed optimizer rule changed; update \
166 matched::OPTIMIZER_RULE_FAILED: {label}"
167 );
168 assert_eq!(user_message(&err), "the cause");
170 }
171
172 #[tokio::test]
175 async fn datafusion_still_returns_syntax_errors_typed() {
176 let err = SessionContext::new()
177 .sql("SELEC 1")
178 .await
179 .expect_err("a syntax error");
180 assert!(
181 matches!(err.find_root(), DataFusionError::SQL(..)),
182 "DataFusion no longer returns a syntax error as DataFusionError::SQL: {err:?}"
183 );
184 let message = user_message(&err);
185 assert!(
186 message.starts_with("syntax error: ") && !message.contains("ParserError"),
187 "{message}"
188 );
189 }
190
191 fn tokenizer_error(sql: &str) -> String {
193 Tokenizer::new(&GenericDialect {}, sql)
194 .tokenize()
195 .expect_err("the tokenizer fails")
196 .message
197 }
198
199 #[test]
200 fn sqlparser_still_says_unterminated_and_eof() {
201 for sql in ["SELECT 'abc", "SELECT 'it''s"] {
202 let message = tokenizer_error(sql);
203 assert!(
204 message.starts_with(matched::TOKENIZER_UNTERMINATED),
205 "sqlparser's message for an open string changed; update \
206 matched::TOKENIZER_UNTERMINATED: {sql}: {message}"
207 );
208 }
209 for sql in ["SELECT \"abc", "SELECT 1 /* a note"] {
210 let message = tokenizer_error(sql);
211 assert!(
212 message.contains(matched::TOKENIZER_EOF),
213 "sqlparser's message for an open quoted name or comment changed; update \
214 matched::TOKENIZER_EOF: {sql}: {message}"
215 );
216 }
217 }
218
219 #[test]
220 fn strips_variant_prefixes() {
221 let err = DataFusionError::Plan("table 'x' not found".into());
222 assert_eq!(user_message(&err), "table 'x' not found");
223 let err = DataFusionError::SQL(
224 Box::new(ParserError::ParserError("Expected: an expression".into())),
225 None,
226 );
227 assert_eq!(user_message(&err), "syntax error: Expected: an expression");
228 let err = DataFusionError::External(Box::new(DataFusionError::Execution("boom".into())));
229 assert_eq!(user_message(&err), "boom");
230 }
231}