1use super::token::Position;
20use std::fmt;
21
22#[derive(Debug, Clone, PartialEq)]
24pub struct ParseError {
25 pub message: String,
27 pub position: Position,
29 pub context: String,
31}
32
33impl ParseError {
34 pub fn new(message: impl Into<String>, position: Position) -> Self {
36 Self {
37 message: message.into(),
38 position,
39 context: String::new(),
40 }
41 }
42
43 pub fn with_context(
45 message: impl Into<String>,
46 position: Position,
47 context: impl Into<String>,
48 ) -> Self {
49 Self {
50 message: message.into(),
51 position,
52 context: context.into(),
53 }
54 }
55
56 pub fn format_error(&self) -> String {
58 if self.context.is_empty() {
59 return self.to_string();
60 }
61
62 let lines: Vec<&str> = self.context.lines().collect();
63 if self.position.line == 0 || self.position.line > lines.len() {
64 return self.to_string();
65 }
66
67 let line = lines[self.position.line - 1];
68 let pointer = " ".repeat(self.position.column.saturating_sub(1)) + "^";
69
70 format!("{}\n{}\n{}", self, line, pointer)
71 }
72}
73
74impl fmt::Display for ParseError {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 write!(f, "{} at position {}", self.message, self.position)
77 }
78}
79
80impl std::error::Error for ParseError {}
81
82#[derive(Debug, Clone)]
84pub struct ParseErrors {
85 pub errors: Vec<ParseError>,
87 pub sql: String,
89}
90
91impl ParseErrors {
92 pub fn new(sql: impl Into<String>) -> Self {
94 Self {
95 errors: Vec::new(),
96 sql: sql.into(),
97 }
98 }
99
100 pub fn from_errors(errors: Vec<ParseError>) -> Self {
102 Self {
103 errors,
104 sql: String::new(),
105 }
106 }
107
108 pub fn from_errors_with_sql(errors: Vec<ParseError>, sql: impl Into<String>) -> Self {
110 Self {
111 errors,
112 sql: sql.into(),
113 }
114 }
115
116 pub fn push(&mut self, error: ParseError) {
118 self.errors.push(error);
119 }
120
121 pub fn is_empty(&self) -> bool {
123 self.errors.is_empty()
124 }
125
126 pub fn len(&self) -> usize {
128 self.errors.len()
129 }
130
131 pub fn format_errors(&self) -> String {
133 if self.errors.is_empty() {
134 return String::new();
135 }
136
137 let mut result = format!(
138 "SQL parsing failed with {} error(s):\n\n",
139 self.errors.len()
140 );
141
142 for (i, err) in self.errors.iter().enumerate() {
143 result.push_str(&format!("Error {}: {}\n", i + 1, err.message));
144
145 let lines: Vec<&str> = self.sql.lines().collect();
147 if err.position.line > 0 && err.position.line <= lines.len() {
148 let line = lines[err.position.line - 1];
149 let prefix = format!("Line {}: ", err.position.line);
150 result.push_str(&format!("{}{}\n", prefix, line));
151 let pointer =
152 " ".repeat(prefix.chars().count() + err.position.column.saturating_sub(1));
153 result.push_str(&format!("{}^\n", pointer));
154 }
155
156 if let Some(suggestion) = get_suggestion(err) {
158 result.push_str(&format!("Suggestion: {}\n", suggestion));
159 }
160
161 result.push('\n');
162 }
163
164 result
165 }
166}
167
168impl fmt::Display for ParseErrors {
169 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170 if self.errors.is_empty() {
171 write!(f, "SQL parse error")
172 } else {
173 write!(f, "{}", self.errors[0])
174 }
175 }
176}
177
178impl std::error::Error for ParseErrors {}
179
180fn get_suggestion(err: &ParseError) -> Option<String> {
182 let msg = &err.message;
183 let ctx = &err.context;
184
185 if msg.contains("expected table name or subquery") {
187 return Some("You might be missing a column or table name, or using a reserved keyword without proper quoting. Try enclosing names in double quotes if they're reserved words.".to_string());
188 }
189
190 if ctx.contains("SELET") {
191 return Some("Did you mean 'SELECT'?".to_string());
192 }
193
194 if msg.contains("expected ')' or ','") {
195 return Some("You're missing a closing parenthesis. Make sure all opening parentheses are matched with closing ones.".to_string());
196 }
197
198 if msg.contains("expected next token to be PUNCTUATOR") {
199 return Some("A punctuation character like '(', ')', ',', ';' is expected here. Check for missing parentheses or commas in lists.".to_string());
200 }
201
202 if ctx.contains("LEFTJOIN") {
203 return Some(
204 "Did you mean 'LEFT JOIN'? LEFT JOIN needs a space between the words.".to_string(),
205 );
206 }
207
208 if msg.contains("expected next token to be IDENTIFIER") {
209 return Some("You might be missing a column or table name, or using a reserved keyword without proper quoting.".to_string());
210 }
211
212 if msg.contains("expected next token to be KEYWORD") {
213 return Some(
214 "A SQL keyword (like SELECT, FROM, WHERE, GROUP BY, etc.) is expected here."
215 .to_string(),
216 );
217 }
218
219 if msg.contains("expected next token to be OPERATOR") {
220 return Some("An operator such as =, <, >, <=, >=, <>, != is expected here.".to_string());
221 }
222
223 if msg.contains("expected next token to be NUMBER") {
224 return Some("A numeric value is expected here. Make sure you're providing a valid number without quotes.".to_string());
225 }
226
227 if msg.contains("expected next token to be STRING") {
228 return Some(
229 "A string value is expected here. String literals should be enclosed in single quotes."
230 .to_string(),
231 );
232 }
233
234 if msg.contains("unexpected token OPERATOR") {
236 return Some("You have an unexpected operator here. Check if you're missing a value or have an extra operator.".to_string());
237 }
238
239 if msg.contains("unexpected token PUNCTUATOR") {
240 return Some("There's an unexpected punctuation character here. Check for mismatched parentheses or extra commas.".to_string());
241 }
242
243 if msg.contains("unexpected token EOF") {
244 return Some("Your SQL statement is incomplete. You might be missing a closing parenthesis, quote, or the end of a clause.".to_string());
245 }
246
247 if msg.contains("SELET") || ctx.contains("SELET") {
249 return Some("Did you mean 'SELECT'?".to_string());
250 }
251
252 if msg.contains("UPDAT") || ctx.contains("UPDAT") {
253 return Some("Did you mean 'UPDATE'?".to_string());
254 }
255
256 if msg.contains("DELET") || ctx.contains("DELET") {
257 return Some("Did you mean 'DELETE'?".to_string());
258 }
259
260 if msg.contains("GROUPBY") || ctx.contains("GROUPBY") {
261 return Some(
262 "Did you mean 'GROUP BY'? GROUP BY needs a space between the words.".to_string(),
263 );
264 }
265
266 if msg.contains("ORDERBY") || ctx.contains("ORDERBY") {
267 return Some(
268 "Did you mean 'ORDER BY'? ORDER BY needs a space between the words.".to_string(),
269 );
270 }
271
272 if ctx.contains("JOIN") && !ctx.contains("ON") {
274 return Some(
275 "Your JOIN clause is missing the ON condition that specifies how tables are related."
276 .to_string(),
277 );
278 }
279
280 if msg.contains("missing ')'") {
282 return Some("You're missing a closing parenthesis.".to_string());
283 }
284
285 if msg.contains("missing '('") {
286 return Some("You're missing an opening parenthesis.".to_string());
287 }
288
289 Some("Check syntax near this location. Common issues include missing keywords, misplaced clauses, unclosed parentheses, or incorrect identifiers.".to_string())
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_parse_error_display() {
299 let err = ParseError::new("unexpected token", Position::new(10, 1, 11));
300 assert_eq!(
301 err.to_string(),
302 "unexpected token at position line 1, column 11"
303 );
304 }
305
306 #[test]
307 fn test_parse_error_with_context() {
308 let err = ParseError::with_context(
309 "unexpected token",
310 Position::new(7, 1, 8),
311 "SELECT * FORM users",
312 );
313 let formatted = err.format_error();
314 assert!(formatted.contains("SELECT * FORM users"));
315 assert!(formatted.contains("^"));
316 }
317
318 #[test]
319 fn test_parse_errors_collection() {
320 let mut errors = ParseErrors::new("SELECT SELET FROM");
321 assert!(errors.is_empty());
322
323 errors.push(ParseError::new("unexpected token", Position::new(7, 1, 8)));
324 assert_eq!(errors.len(), 1);
325 assert!(!errors.is_empty());
326 }
327
328 #[test]
329 fn test_suggestion_for_typo() {
330 let err = ParseError::with_context(
331 "unexpected identifier",
332 Position::new(0, 1, 1),
333 "SELET * FROM users",
334 );
335 let suggestion = get_suggestion(&err);
336 assert!(suggestion.is_some());
337 assert!(suggestion.unwrap().contains("SELECT"));
338 }
339
340 #[test]
341 fn test_suggestion_for_missing_identifier() {
342 let err = ParseError::new(
343 "expected next token to be IDENTIFIER",
344 Position::new(0, 1, 1),
345 );
346 let suggestion = get_suggestion(&err);
347 assert!(suggestion.is_some());
348 assert!(suggestion.unwrap().contains("column or table name"));
349 }
350}