Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(parser): fix token location and print sql with cursor on parse error #16959

Merged
merged 10 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions e2e_test/error_ui/extended/main.slt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ selet 1;
db error: ERROR: Failed to prepare the statement

Caused by:
sql parser error: Expected an SQL statement, found: selet at line:1, column:6
Near "selet"
sql parser error: expected an SQL statement, found: selet at line 1, column 1
LINE 1: selet 1;
^


query error
Expand Down
5 changes: 3 additions & 2 deletions e2e_test/error_ui/simple/main.slt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ selet 1;
db error: ERROR: Failed to run the query

Caused by:
sql parser error: Expected an SQL statement, found: selet at line:1, column:6
Near "selet"
sql parser error: expected an SQL statement, found: selet at line 1, column 1
LINE 1: selet 1;
^


statement error
Expand Down
4 changes: 2 additions & 2 deletions e2e_test/source/basic/datagen.slt
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,9 @@ statement ok
drop table s1;

# Do NOT allow With clause to contain a comma only.
statement error Expected identifier.*
statement error expected identifier.*
create table s1 (v1 int) with (,) FORMAT PLAIN ENCODE JSON;

# Do NOT allow an empty With clause.
statement error Expected identifier.*
statement error expected identifier.*
create table s1 (v1 int) with () FORMAT PLAIN ENCODE JSON;
5 changes: 3 additions & 2 deletions e2e_test/source/basic/ddl.slt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ create source s;
db error: ERROR: Failed to run the query

Caused by:
sql parser error: Expected description of the format, found: ; at line:1, column:17
Near "create source s"
sql parser error: expected description of the format, found: ; at line 1, column 16
LINE 1: create source s;
^


statement error missing WITH clause
Expand Down
2 changes: 1 addition & 1 deletion src/sqlparser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ normal = ["workspace-hack"]
itertools = { workspace = true }
serde = { version = "1.0", features = ["derive"], optional = true }
thiserror = "1.0.61"
tokio = { version = "0.2", package = "madsim-tokio" }
tokio = { version = "0.2", package = "madsim-tokio", features = ["rt"] }
tracing = "0.1"
tracing-subscriber = "0.3"
winnow = { version = "0.6.8", git = "https://github.com/TennyZhuang/winnow.git", rev = "a6b1f04" }
Expand Down
62 changes: 40 additions & 22 deletions src/sqlparser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,22 +226,57 @@ impl Parser {
let mut tokenizer = Tokenizer::new(sql);
let tokens = tokenizer.tokenize_with_location()?;
let mut parser = Parser::new(tokens);
let ast = parser.parse_statements().map_err(|e| {
// append SQL context to the error message, e.g.:
// LINE 1: SELECT 1::int(2);
// ^
// XXX: the cursor location is not accurate
// it may be offset one token forward because the error token has been consumed
let loc = match parser.tokens.get(parser.index) {
Some(token) => token.location.clone(),
None => {
// get location of EOF
Location {
line: sql.lines().count() as u64,
column: sql.lines().last().map_or(0, |l| l.len() as u64) + 1,
}
}
};
let prefix = format!("LINE {}: ", loc.line);
let sql_line = sql.split('\n').nth(loc.line as usize - 1).unwrap();
let cursor = std::iter::repeat(' ')
.take(prefix.len() + loc.column as usize - 1)
.chain(['^'])
.collect::<String>();
ParserError::ParserError(format!(
"{}\n{}{}\n{}",
e.inner_msg(),
prefix,
sql_line,
cursor
))
})?;
Ok(ast)
}

/// Parse a list of semicolon-separated SQL statements.
pub fn parse_statements(&mut self) -> Result<Vec<Statement>, ParserError> {
let mut stmts = Vec::new();
let mut expecting_statement_delimiter = false;
loop {
// ignore empty statements (between successive statement delimiters)
while parser.consume_token(&Token::SemiColon) {
while self.consume_token(&Token::SemiColon) {
expecting_statement_delimiter = false;
}

if parser.peek_token() == Token::EOF {
if self.peek_token() == Token::EOF {
break;
}
if expecting_statement_delimiter {
return parser.expected("end of statement", parser.peek_token());
return self.expected("end of statement", self.peek_token());
}

let statement = parser.parse_statement()?;
let statement = self.parse_statement()?;
stmts.push(statement);
expecting_statement_delimiter = true;
}
Expand Down Expand Up @@ -1958,24 +1993,7 @@ impl Parser {

/// Report unexpected token
pub fn expected<T>(&self, expected: &str, found: TokenWithLocation) -> Result<T, ParserError> {
let start_off = self.index.saturating_sub(10);
let end_off = self.index.min(self.tokens.len());
let near_tokens = &self.tokens[start_off..end_off];
struct TokensDisplay<'a>(&'a [TokenWithLocation]);
impl<'a> fmt::Display for TokensDisplay<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for token in self.0 {
write!(f, "{}", token.token)?;
}
Ok(())
}
}
parser_err!(format!(
"Expected {}, found: {}\nNear \"{}\"",
expected,
found,
TokensDisplay(near_tokens),
))
parser_err!(format!("expected {}, found: {}", expected, found))
}

/// Look for an expected keyword and consume it if it exists
Expand Down
Loading
Loading