1use datafusion::arrow::error::ArrowError;
16use datafusion::error::DataFusionError;
17use datafusion::sql::sqlparser::parser::ParserError;
18
19pub mod matched {
27 pub const NO_FILES_FOR_TABLE: &str = "No files for table";
31
32 pub const NO_FILE_FORMAT_FOR_EXTENSION: &str = "No file_format found with extension";
36
37 pub const OPTIMIZER_RULE_FAILED: &str = "Optimizer rule";
41
42 pub const TOKENIZER_UNTERMINATED: &str = "Unterminated";
47
48 pub const TOKENIZER_EOF: &str = "EOF";
53}
54
55pub fn user_message(err: &DataFusionError) -> String {
58 match err {
59 DataFusionError::ArrowError(e, _) => arrow_message(e),
60 DataFusionError::ParquetError(e) => format!("Parquet: {e}"),
61 DataFusionError::ObjectStore(e) => e.to_string(),
62 DataFusionError::IoError(e) => e.to_string(),
63 DataFusionError::SQL(e, _) => parser_message(e),
64 DataFusionError::NotImplemented(m)
65 | DataFusionError::Plan(m)
66 | DataFusionError::Configuration(m)
67 | DataFusionError::Execution(m)
68 | DataFusionError::ResourcesExhausted(m)
69 | DataFusionError::Substrait(m)
70 | DataFusionError::Ffi(m) => m.clone(),
71 DataFusionError::Internal(m) => format!("internal error: {m}"),
72 DataFusionError::SchemaError(e, _) => e.to_string(),
73 DataFusionError::ExecutionJoin(e) => e.to_string(),
74 DataFusionError::External(e) => external_message(e.as_ref()),
75 DataFusionError::Context(context, inner)
77 if context.starts_with(matched::OPTIMIZER_RULE_FAILED) =>
78 {
79 user_message(inner)
80 }
81 DataFusionError::Context(context, inner) => {
82 format!("{context}: {}", user_message(inner))
83 }
84 DataFusionError::Diagnostic(_, inner) => user_message(inner),
85 DataFusionError::Collection(errs) => errs
86 .first()
87 .map(user_message)
88 .unwrap_or_else(|| "unknown error".to_string()),
89 DataFusionError::Shared(inner) => user_message(inner),
90 }
91}
92
93fn parser_message(e: &ParserError) -> String {
94 match e {
95 ParserError::TokenizerError(m) | ParserError::ParserError(m) => {
96 format!("syntax error: {m}")
97 }
98 ParserError::RecursionLimitExceeded => {
99 "syntax error: the statement is nested too deeply".to_string()
100 }
101 }
102}
103
104fn arrow_message(e: &ArrowError) -> String {
105 match e {
106 ArrowError::ExternalError(source) => external_message(source.as_ref()),
107 other => other.to_string(),
108 }
109}
110
111fn external_message(e: &(dyn std::error::Error + Send + Sync + 'static)) -> String {
112 if let Some(df) = e.downcast_ref::<DataFusionError>() {
113 user_message(df)
114 } else if let Some(arrow) = e.downcast_ref::<ArrowError>() {
115 arrow_message(arrow)
116 } else {
117 e.to_string()
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use std::sync::Arc;
124
125 use datafusion::common::tree_node::Transformed;
126 use datafusion::common::DFSchema;
127 use datafusion::datasource::listing::{
128 ListingTableConfig, ListingTableConfigExt, ListingTableUrl,
129 };
130 use datafusion::logical_expr::{EmptyRelation, LogicalPlan};
131 use datafusion::optimizer::{Optimizer, OptimizerConfig, OptimizerContext, OptimizerRule};
132 use datafusion::prelude::SessionContext;
133 use datafusion::sql::sqlparser::dialect::GenericDialect;
134 use datafusion::sql::sqlparser::tokenizer::Tokenizer;
135
136 use super::*;
137
138 async fn infer_options_error(location: &std::path::Path) -> String {
142 let ctx = SessionContext::new();
143 let url = ListingTableUrl::parse(location.to_str().unwrap()).unwrap();
144 let err = ListingTableConfig::new(url)
145 .infer_options(&ctx.state())
146 .await
147 .expect_err("infer_options fails");
148 err.to_string()
149 }
150
151 #[tokio::test]
152 async fn datafusion_still_says_no_files_for_table() {
153 let dir = tempfile::tempdir().unwrap();
154 let message = infer_options_error(dir.path()).await;
155 assert!(
156 message.contains(matched::NO_FILES_FOR_TABLE),
157 "DataFusion's message for a location with no file changed; update \
158 matched::NO_FILES_FOR_TABLE: {message}"
159 );
160 }
161
162 #[tokio::test]
163 async fn datafusion_still_says_no_file_format_for_extension() {
164 let dir = tempfile::tempdir().unwrap();
165 let file = dir.path().join("notes.txt");
166 std::fs::write(&file, "hello\n").unwrap();
167 let message = infer_options_error(&file).await;
168 assert!(
169 message.contains(matched::NO_FILE_FORMAT_FOR_EXTENSION),
170 "DataFusion's message for an unknown extension changed; update \
171 matched::NO_FILE_FORMAT_FOR_EXTENSION: {message}"
172 );
173 }
174
175 #[derive(Debug)]
177 struct Failing;
178
179 impl OptimizerRule for Failing {
180 fn name(&self) -> &str {
181 "failing"
182 }
183
184 fn supports_rewrite(&self) -> bool {
185 true
186 }
187
188 fn rewrite(
189 &self,
190 _plan: LogicalPlan,
191 _config: &dyn OptimizerConfig,
192 ) -> datafusion::error::Result<Transformed<LogicalPlan>> {
193 Err(DataFusionError::Plan("the cause".into()))
194 }
195 }
196
197 #[test]
198 fn datafusion_still_labels_a_failed_optimizer_rule() {
199 let optimizer = Optimizer::with_rules(vec![Arc::new(Failing)]);
200 let config = OptimizerContext::new().with_skip_failing_rules(false);
201 let plan = LogicalPlan::EmptyRelation(EmptyRelation {
202 produce_one_row: false,
203 schema: Arc::new(DFSchema::empty()),
204 });
205 let err = optimizer
206 .optimize(plan, &config, |_, _| {})
207 .expect_err("the rule fails");
208 let DataFusionError::Context(label, _) = &err else {
209 panic!(
210 "DataFusion no longer wraps a failed optimizer rule's error in a context: {err:?}"
211 );
212 };
213 assert!(
214 label.starts_with(matched::OPTIMIZER_RULE_FAILED),
215 "DataFusion's label for a failed optimizer rule changed; update \
216 matched::OPTIMIZER_RULE_FAILED: {label}"
217 );
218 assert_eq!(user_message(&err), "the cause");
220 }
221
222 #[tokio::test]
225 async fn datafusion_still_returns_syntax_errors_typed() {
226 let err = SessionContext::new()
227 .sql("SELEC 1")
228 .await
229 .expect_err("a syntax error");
230 assert!(
231 matches!(err.find_root(), DataFusionError::SQL(..)),
232 "DataFusion no longer returns a syntax error as DataFusionError::SQL: {err:?}"
233 );
234 let message = user_message(&err);
235 assert!(
236 message.starts_with("syntax error: ") && !message.contains("ParserError"),
237 "{message}"
238 );
239 }
240
241 fn tokenizer_error(sql: &str) -> String {
243 Tokenizer::new(&GenericDialect {}, sql)
244 .tokenize()
245 .expect_err("the tokenizer fails")
246 .message
247 }
248
249 #[test]
250 fn sqlparser_still_says_unterminated_and_eof() {
251 for sql in ["SELECT 'abc", "SELECT 'it''s"] {
252 let message = tokenizer_error(sql);
253 assert!(
254 message.starts_with(matched::TOKENIZER_UNTERMINATED),
255 "sqlparser's message for an open string changed; update \
256 matched::TOKENIZER_UNTERMINATED: {sql}: {message}"
257 );
258 }
259 for sql in ["SELECT \"abc", "SELECT 1 /* a note"] {
260 let message = tokenizer_error(sql);
261 assert!(
262 message.contains(matched::TOKENIZER_EOF),
263 "sqlparser's message for an open quoted name or comment changed; update \
264 matched::TOKENIZER_EOF: {sql}: {message}"
265 );
266 }
267 }
268
269 #[test]
270 fn strips_variant_prefixes() {
271 let err = DataFusionError::Plan("table 'x' not found".into());
272 assert_eq!(user_message(&err), "table 'x' not found");
273 let err = DataFusionError::SQL(
274 Box::new(ParserError::ParserError("Expected: an expression".into())),
275 None,
276 );
277 assert_eq!(user_message(&err), "syntax error: Expected: an expression");
278 let err = DataFusionError::External(Box::new(DataFusionError::Execution("boom".into())));
279 assert_eq!(user_message(&err), "boom");
280 }
281}