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 crate::query::sanitize_query_error(msg)
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn test_user_message_from_io_not_found() {
168 let err = io::Error::new(io::ErrorKind::NotFound, "No such file");
169 let msg = user_message_from_io(&err, None);
170 assert!(
171 msg.contains("not found"),
172 "expected 'not found', got: {}",
173 msg
174 );
175 }
176
177 #[test]
178 fn test_user_message_from_io_permission_denied() {
179 let err = io::Error::new(io::ErrorKind::PermissionDenied, "Permission denied");
180 let msg = user_message_from_io(&err, None);
181 assert!(
182 msg.to_lowercase().contains("permission"),
183 "expected 'permission', got: {}",
184 msg
185 );
186 }
187
188 #[test]
189 fn test_user_message_from_polars_column_not_found() {
190 use polars::prelude::PolarsError;
191 let err = PolarsError::ColumnNotFound("foo".into());
192 let msg = user_message_from_polars(&err);
193 assert!(msg.contains("foo"), "expected 'foo', got: {}", msg);
194 assert!(
195 msg.contains("Column not found"),
196 "expected column not found, got: {}",
197 msg
198 );
199 }
200
201 #[test]
202 fn test_user_message_from_polars_duplicate() {
203 use polars::prelude::PolarsError;
204 let err = PolarsError::Duplicate("bar".into());
205 let msg = user_message_from_polars(&err);
206 assert!(
207 msg.contains("Duplicate"),
208 "expected 'Duplicate', got: {}",
209 msg
210 );
211 assert!(msg.contains("alias"), "expected alias hint, got: {}", msg);
212 }
213
214 #[test]
215 fn test_simplify_compute_message_alias_hint() {
216 let raw = "projections contained duplicate: 'x'. Try renaming with .alias(\"name\")";
217 let msg = simplify_compute_message(raw);
218 assert!(
219 !msg.contains(".alias("),
220 "should strip .alias( hint: {}",
221 msg
222 );
223 assert!(
224 msg.contains("Use aliases"),
225 "expected alias suggestion: {}",
226 msg
227 );
228 }
229}