1use polars::prelude::PolarsError;
7use std::io;
8use std::path::Path;
9
10pub fn user_message_from_polars(err: &PolarsError) -> String {
12 use polars::prelude::PolarsError as PE;
13
14 match err {
15 PE::ColumnNotFound(msg) => format!(
16 "Column not found: {}. Check spelling and that the column exists.",
17 msg
18 ),
19 PE::Duplicate(msg) => format!(
20 "Duplicate column in result: {}. Use aliases to rename columns, e.g. `select my_date: timestamp.date`",
21 msg
22 ),
23 PE::IO { error, msg } => {
24 user_message_from_io(error.as_ref(), msg.as_ref().map(|m| m.as_ref()))
25 }
26 PE::NoData(msg) => format!("No data: {}", msg),
27 PE::SchemaMismatch(msg) => format!("Schema mismatch: {}", msg),
28 PE::ShapeMismatch(msg) => format!("Row shape mismatch: {}", msg),
29 PE::InvalidOperation(msg) => format!("Operation not allowed: {}", msg),
30 PE::OutOfBounds(msg) => format!("Index or row out of bounds: {}", msg),
31 PE::SchemaFieldNotFound(msg) => format!("Schema field not found: {}", msg),
32 PE::StructFieldNotFound(msg) => format!("Struct field not found: {}", msg),
33 PE::ComputeError(msg) => simplify_compute_message(msg),
34 PE::AssertionError(msg) => format!("Assertion failed: {}", msg),
35 PE::StringCacheMismatch(msg) => format!("String cache mismatch: {}", msg),
36 PE::SQLInterface(msg) | PE::SQLSyntax(msg) => msg.to_string(),
37 PE::Context { error, msg } => {
38 let inner = user_message_from_polars(error);
39 format!("{}: {}", msg, inner)
40 }
41 #[allow(unreachable_patterns)]
42 _ => err.to_string(),
43 }
44}
45
46pub fn user_message_from_io(err: &io::Error, context: Option<&str>) -> String {
48 use std::io::ErrorKind;
49
50 let base: String = match err.kind() {
51 ErrorKind::NotFound => "File or directory not found.".to_string(),
52 ErrorKind::PermissionDenied => "Permission denied. Check read access.".to_string(),
53 ErrorKind::ConnectionRefused => "Connection refused.".to_string(),
54 ErrorKind::ConnectionReset => "Connection reset.".to_string(),
55 ErrorKind::InvalidData | ErrorKind::InvalidInput => {
56 "Invalid or corrupted data.".to_string()
57 }
58 ErrorKind::UnexpectedEof => "Unexpected end of file.".to_string(),
59 ErrorKind::WouldBlock => "Operation would block.".to_string(),
60 ErrorKind::Interrupted => "Operation interrupted.".to_string(),
61 ErrorKind::OutOfMemory => "Out of memory.".to_string(),
62 ErrorKind::Other => {
63 let msg = err.to_string();
64 if msg.contains("No space left") || msg.contains("space left") {
65 return "No space left on device. Free up disk space and try again.".to_string();
66 }
67 if msg.contains("Is a directory") {
68 return "Path is a directory, not a file.".to_string();
69 }
70 return if context.is_some() {
71 format!("I/O error: {}", msg)
72 } else {
73 msg
74 };
75 }
76 _ => err.to_string(),
77 };
78
79 if let Some(ctx) = context {
80 if !ctx.is_empty() {
81 format!("{} {}", base, ctx)
82 } else {
83 base
84 }
85 } else {
86 base
87 }
88}
89
90#[derive(Debug, Clone, Copy)]
93pub enum ErrorKindForPython {
94 FileNotFound,
95 PermissionDenied,
96 Other,
97}
98
99pub fn error_for_python(report: &color_eyre::eyre::Report) -> (ErrorKindForPython, String) {
102 use std::io::ErrorKind;
103 for cause in report.chain() {
104 if let Some(io_err) = cause.downcast_ref::<io::Error>() {
105 let kind = match io_err.kind() {
106 ErrorKind::NotFound => ErrorKindForPython::FileNotFound,
107 ErrorKind::PermissionDenied => ErrorKindForPython::PermissionDenied,
108 _ => ErrorKindForPython::Other,
109 };
110 let msg = io_err.to_string();
111 return (kind, msg);
112 }
113 }
114 let display = report.to_string();
115 let msg = display
116 .lines()
117 .next()
118 .map(str::trim)
119 .unwrap_or("An error occurred")
120 .to_string();
121 (ErrorKindForPython::Other, msg)
122}
123
124pub fn user_message_from_report(report: &color_eyre::eyre::Report, path: Option<&Path>) -> String {
127 for cause in report.chain() {
128 if let Some(pe) = cause.downcast_ref::<PolarsError>() {
129 let msg = user_message_from_polars(pe);
130 return if let Some(p) = path {
131 format!("Failed to load {}: {}", p.display(), msg)
132 } else {
133 msg
134 };
135 }
136 if let Some(io_err) = cause.downcast_ref::<io::Error>() {
137 let msg = user_message_from_io(io_err, None);
138 return if let Some(p) = path {
139 format!("Failed to load {}: {}", p.display(), msg)
140 } else {
141 msg
142 };
143 }
144 }
145
146 let display = report.to_string();
148 let first_line = display.lines().next().unwrap_or("An error occurred");
149 let trimmed = first_line.trim();
150 if let Some(p) = path {
151 format!("Failed to load {}: {}", p.display(), trimmed)
152 } else {
153 trimmed.to_string()
154 }
155}
156
157fn simplify_compute_message(msg: &str) -> String {
159 if is_csv_parse_type_error(msg) {
160 return short_csv_parse_error_message(msg);
161 }
162 crate::query::sanitize_query_error(msg)
163}
164
165fn is_csv_parse_type_error(msg: &str) -> bool {
167 let m = msg.to_lowercase();
168 (m.contains("could not parse") && m.contains("as dtype"))
169 || m.contains("invalid primitive value")
170}
171
172fn extract_csv_parse_column(msg: &str) -> Option<String> {
174 let m = msg.to_lowercase();
175 for (needle, quote) in [("at column '", '\''), ("at column \"", '"')] {
176 if let Some(start) = m.find(needle) {
177 let after = &msg[start + needle.len()..];
178 let end = after.find(quote)?;
179 let name = after[..end].trim();
180 if !name.is_empty() {
181 return Some(name.to_string());
182 }
183 }
184 }
185 None
186}
187
188fn short_csv_parse_error_message(raw: &str) -> String {
189 let col = extract_csv_parse_column(raw);
190 let first = match &col {
191 Some(c) => format!(
192 "CSV parse error in column '{}': a value didn't match the inferred type.",
193 c
194 ),
195 None => "CSV parse error: a value didn't match the inferred column type.".to_string(),
196 };
197 format!(
198 "{}\n\
199 Try: --infer-schema-length 1000\n\
200 --null-value <value> (treat as null)\n\
201 --ignore-errors true (skip bad rows)",
202 first
203 )
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_user_message_from_io_not_found() {
212 let err = io::Error::new(io::ErrorKind::NotFound, "No such file");
213 let msg = user_message_from_io(&err, None);
214 assert!(
215 msg.contains("not found"),
216 "expected 'not found', got: {}",
217 msg
218 );
219 }
220
221 #[test]
222 fn test_user_message_from_io_permission_denied() {
223 let err = io::Error::new(io::ErrorKind::PermissionDenied, "Permission denied");
224 let msg = user_message_from_io(&err, None);
225 assert!(
226 msg.to_lowercase().contains("permission"),
227 "expected 'permission', got: {}",
228 msg
229 );
230 }
231
232 #[test]
233 fn test_user_message_from_polars_column_not_found() {
234 use polars::prelude::PolarsError;
235 let err = PolarsError::ColumnNotFound("foo".into());
236 let msg = user_message_from_polars(&err);
237 assert!(msg.contains("foo"), "expected 'foo', got: {}", msg);
238 assert!(
239 msg.contains("Column not found"),
240 "expected column not found, got: {}",
241 msg
242 );
243 }
244
245 #[test]
246 fn test_user_message_from_polars_duplicate() {
247 use polars::prelude::PolarsError;
248 let err = PolarsError::Duplicate("bar".into());
249 let msg = user_message_from_polars(&err);
250 assert!(
251 msg.contains("Duplicate"),
252 "expected 'Duplicate', got: {}",
253 msg
254 );
255 assert!(msg.contains("alias"), "expected alias hint, got: {}", msg);
256 }
257
258 #[test]
259 fn test_simplify_compute_message_alias_hint() {
260 let raw = "projections contained duplicate: 'x'. Try renaming with .alias(\"name\")";
261 let msg = simplify_compute_message(raw);
262 assert!(
263 !msg.contains(".alias("),
264 "should strip .alias( hint: {}",
265 msg
266 );
267 assert!(
268 msg.contains("Use aliases"),
269 "expected alias suggestion: {}",
270 msg
271 );
272 }
273
274 #[test]
275 fn test_simplify_compute_message_csv_parse_error() {
276 let raw = "could not parse `N/A` as dtype `i64` at column 'column' (column number 1)\n\n\
277 The current offset in the file is 292 bytes.\n\n\
278 You might want to try: ...\n\
279 Original error: ```invalid primitive value found during CSV parsing```";
280 let msg = simplify_compute_message(raw);
281 assert!(
282 msg.contains("CSV parse error"),
283 "expected short CSV message: {}",
284 msg
285 );
286 assert!(
287 msg.contains("column 'column'"),
288 "expected offending column in message: {}",
289 msg
290 );
291 assert!(
292 msg.contains("--infer-schema-length"),
293 "expected CLI hint: {}",
294 msg
295 );
296 assert!(
297 msg.contains("--null-value"),
298 "expected null-value hint: {}",
299 msg
300 );
301 assert!(
302 !msg.contains("Original error"),
303 "should not regurgitate Polars: {}",
304 msg
305 );
306 }
307}