-
Notifications
You must be signed in to change notification settings - Fork 602
Add support for GO
batch delimiter in SQL Server
#1809
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
base: main
Are you sure you want to change the base?
Changes from all commits
25fd849
24d813b
f4750f0
3db7fba
ca0bb28
94abfb7
cfecd33
2b28275
bc55f6f
d2b15df
827dfe7
1c01035
b46fefc
7863bef
6726d42
fb41bfc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -393,6 +393,7 @@ define_keywords!( | |
GIN, | ||
GIST, | ||
GLOBAL, | ||
GO, | ||
GRANT, | ||
GRANTED, | ||
GRANTS, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -475,6 +475,10 @@ impl<'a> Parser<'a> { | |
if expecting_statement_delimiter && word.keyword == Keyword::END { | ||
break; | ||
} | ||
|
||
if expecting_statement_delimiter && word.keyword == Keyword::GO { | ||
expecting_statement_delimiter = false; | ||
} | ||
} | ||
_ => {} | ||
} | ||
|
@@ -484,8 +488,9 @@ impl<'a> Parser<'a> { | |
} | ||
|
||
let statement = self.parse_statement()?; | ||
// Treat batch delimiter as an end of statement, so no additional statement delimiter expected here | ||
expecting_statement_delimiter = !matches!(statement, Statement::Go(_)); | ||
stmts.push(statement); | ||
expecting_statement_delimiter = true; | ||
} | ||
Ok(stmts) | ||
} | ||
|
@@ -613,6 +618,10 @@ impl<'a> Parser<'a> { | |
Keyword::COMMENT if self.dialect.supports_comment_on() => self.parse_comment(), | ||
Keyword::PRINT => self.parse_print(), | ||
Keyword::RETURN => self.parse_return(), | ||
Keyword::GO => { | ||
self.prev_token(); | ||
self.parse_go() | ||
} | ||
_ => self.expected("an SQL statement", next_token), | ||
}, | ||
Token::LParen => { | ||
|
@@ -3934,6 +3943,17 @@ impl<'a> Parser<'a> { | |
}) | ||
} | ||
|
||
/// Return nth previous token, possibly whitespace | ||
/// (or [`Token::EOF`] when before the beginning of the stream). | ||
pub(crate) fn peek_prev_nth_token_no_skip_ref(&self, n: usize) -> &TokenWithSpan { | ||
// 0 = next token, -1 = current token, -2 = previous token | ||
let peek_index = self.index.saturating_sub(1).saturating_sub(n); | ||
if peek_index == 0 { | ||
return &EOF_TOKEN; | ||
} | ||
self.tokens.get(peek_index).unwrap_or(&EOF_TOKEN) | ||
} | ||
|
||
/// Return true if the next tokens exactly `expected` | ||
/// | ||
/// Does not advance the current token. | ||
|
@@ -4050,6 +4070,29 @@ impl<'a> Parser<'a> { | |
) | ||
} | ||
|
||
/// Look backwards in the token stream and expect that there was only whitespace tokens until the previous newline or beginning of string | ||
pub(crate) fn prev_only_whitespace_until_newline(&mut self) -> bool { | ||
let mut look_back_count = 1; | ||
loop { | ||
let prev_token = self.peek_prev_nth_token_no_skip_ref(look_back_count); | ||
match prev_token.token { | ||
Token::EOF => break true, | ||
Token::Whitespace(ref w) => match w { | ||
Whitespace::Newline => break true, | ||
// special consideration required for single line comments since that string includes the newline | ||
Whitespace::SingleLineComment { comment, prefix: _ } => { | ||
if comment.ends_with('\n') { | ||
break true; | ||
} | ||
look_back_count += 1; | ||
} | ||
_ => look_back_count += 1, | ||
}, | ||
_ => break false, | ||
}; | ||
} | ||
} | ||
|
||
/// If the current token is the `expected` keyword, consume it and returns | ||
/// true. Otherwise, no tokens are consumed and returns false. | ||
#[must_use] | ||
|
@@ -15225,6 +15268,71 @@ impl<'a> Parser<'a> { | |
} | ||
} | ||
|
||
/// Parse [Statement::Go] | ||
fn parse_go(&mut self) -> Result<Statement, ParserError> { | ||
iffyio marked this conversation as resolved.
Show resolved
Hide resolved
iffyio marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.expect_keyword_is(Keyword::GO)?; | ||
|
||
// disambiguate between GO as batch delimiter & GO as identifier (etc) | ||
// compare: | ||
// ```sql | ||
// select 1 go | ||
// ``` | ||
// vs | ||
// ```sql | ||
// select 1 | ||
// go | ||
// ``` | ||
if !self.prev_only_whitespace_until_newline() { | ||
parser_err!( | ||
"GO may only be preceded by whitespace on a line", | ||
self.peek_token().span.start | ||
)?; | ||
} | ||
|
||
let count = loop { | ||
// using this peek function because we want to halt this statement parsing upon newline | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we include the example you had in the comment earlier, explicitly highlighting why this statement is special? I think otherwise it would not be obvious to folks that come across the code later on why the current code is a special case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I can add that comment. However there is also test coverage for this behavior, so it should be protected from future refactoring There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done 👍 |
||
let next_token = self.peek_token_no_skip(); | ||
match next_token.token { | ||
Token::EOF => break None::<u64>, | ||
Token::Whitespace(ref w) => match w { | ||
Whitespace::Newline => break None, | ||
_ => _ = self.next_token_no_skip(), | ||
}, | ||
Token::Number(s, _) => { | ||
let value = Some(Self::parse::<u64>(s, next_token.span.start)?); | ||
self.advance_token(); | ||
break value; | ||
} | ||
_ => self.expected("literal int or newline", next_token)?, | ||
}; | ||
}; | ||
|
||
loop { | ||
let next_token = self.peek_token_no_skip(); | ||
match next_token.token { | ||
Token::EOF => break, | ||
Token::Whitespace(ref w) => match w { | ||
Whitespace::Newline => break, | ||
Whitespace::SingleLineComment { comment, prefix: _ } => { | ||
if comment.ends_with('\n') { | ||
break; | ||
} | ||
_ = self.next_token_no_skip(); | ||
} | ||
_ => _ = self.next_token_no_skip(), | ||
}, | ||
_ => { | ||
parser_err!( | ||
"GO must be followed by a newline or EOF", | ||
self.peek_token().span.start | ||
)?; | ||
} | ||
}; | ||
} | ||
|
||
Ok(Statement::Go(GoStatement { count })) | ||
} | ||
|
||
/// Consume the parser and return its underlying token buffer | ||
pub fn into_tokens(self) -> Vec<TokenWithSpan> { | ||
self.tokens | ||
|
@@ -15455,6 +15563,31 @@ mod tests { | |
}) | ||
} | ||
|
||
#[test] | ||
fn test_peek_prev_nth_token_no_skip_ref() { | ||
all_dialects().run_parser_method( | ||
"SELECT 1;\n-- a comment\nRAISERROR('test', 16, 0);", | ||
|parser| { | ||
parser.index = 1; | ||
assert_eq!(parser.peek_prev_nth_token_no_skip_ref(0), &Token::EOF); | ||
assert_eq!(parser.index, 1); | ||
parser.index = 7; | ||
assert_eq!( | ||
parser.token_at(parser.index - 1).token, | ||
Token::Word(Word { | ||
value: "RAISERROR".to_string(), | ||
quote_style: None, | ||
keyword: Keyword::RAISERROR, | ||
}) | ||
); | ||
assert_eq!( | ||
parser.peek_prev_nth_token_no_skip_ref(2), | ||
&Token::Whitespace(Whitespace::Newline) | ||
); | ||
}, | ||
); | ||
} | ||
|
||
#[cfg(test)] | ||
mod test_parse_data_type { | ||
use crate::ast::{ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
double checking: is there a scenario where a single line comment doesn't end with a new line? spontaneously sounds like that should always hold true so that the manual newline check would not be required
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually don't know, I was surprised to find that the newline is actually part of the comment text in this library. It seemed prudent to be defensive in this new code, since that comment parsing behavior maybe isn't particularly intentional.