Skip to content

Commit 4e439b7

Browse files
committed
squash-merge: mssql-go-keyword
1 parent bd2edc3 commit 4e439b7

File tree

7 files changed

+365
-6
lines changed

7 files changed

+365
-6
lines changed

src/ast/mod.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4326,6 +4326,12 @@ pub enum Statement {
43264326
/// ```
43274327
/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_VACUUM_command.html)
43284328
Vacuum(VacuumStatement),
4329+
/// Go (MsSql)
4330+
///
4331+
/// GO is not a Transact-SQL statement; it is a command recognized by various tools as a batch delimiter
4332+
///
4333+
/// See: <https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go>
4334+
Go(GoStatement),
43294335
}
43304336

43314337
/// ```sql
@@ -6185,6 +6191,7 @@ impl fmt::Display for Statement {
61856191
Ok(())
61866192
}
61876193
Statement::Print(s) => write!(f, "{s}"),
6194+
Statement::Go(s) => write!(f, "{s}"),
61886195
Statement::Return(r) => write!(f, "{r}"),
61896196
Statement::List(command) => write!(f, "LIST {command}"),
61906197
Statement::Remove(command) => write!(f, "REMOVE {command}"),
@@ -10817,6 +10824,26 @@ impl fmt::Display for CreateTableLikeDefaults {
1081710824
}
1081810825
}
1081910826

10827+
/// Represents a `GO` statement.
10828+
///
10829+
/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go)
10830+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
10831+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
10832+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
10833+
pub struct GoStatement {
10834+
pub count: Option<u64>,
10835+
}
10836+
10837+
impl Display for GoStatement {
10838+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10839+
if let Some(count) = self.count {
10840+
write!(f, "GO {count}")
10841+
} else {
10842+
write!(f, "GO")
10843+
}
10844+
}
10845+
}
10846+
1082010847
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1082110848
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1082210849
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]

src/ast/spans.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,7 @@ impl Spanned for Statement {
540540
Statement::RaisError { .. } => Span::empty(),
541541
Statement::Print { .. } => Span::empty(),
542542
Statement::Return { .. } => Span::empty(),
543+
Statement::Go { .. } => Span::empty(),
543544
Statement::List(..) | Statement::Remove(..) => Span::empty(),
544545
Statement::ExportData(ExportData {
545546
options,

src/dialect/mssql.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,13 @@ impl Dialect for MsSqlDialect {
132132
&[GranteesType::Public]
133133
}
134134

135-
fn is_column_alias(&self, kw: &Keyword, _parser: &mut Parser) -> bool {
135+
fn is_column_alias(&self, kw: &Keyword, parser: &mut Parser) -> bool {
136+
// if we find maybe whitespace then a newline looking backward, then `GO` ISN'T a column alias
137+
// if we can't find a newline then we assume that `GO` IS a column alias
138+
if kw == &Keyword::GO && parser.prev_only_whitespace_until_newline() {
139+
return false;
140+
}
141+
136142
!keywords::RESERVED_FOR_COLUMN_ALIAS.contains(kw) && !RESERVED_FOR_COLUMN_ALIAS.contains(kw)
137143
}
138144

src/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ define_keywords!(
432432
GIN,
433433
GIST,
434434
GLOBAL,
435+
GO,
435436
GRANT,
436437
GRANTED,
437438
GRANTS,

src/parser/mod.rs

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,12 @@ impl<'a> Parser<'a> {
514514
if expecting_statement_delimiter && word.keyword == Keyword::END {
515515
break;
516516
}
517+
518+
// MSSQL: the `GO` keyword is a batch separator which also means it concludes the current statement
519+
// `GO` may not be followed by a semicolon, so turn off that expectation
520+
if expecting_statement_delimiter && word.keyword == Keyword::GO {
521+
expecting_statement_delimiter = false;
522+
}
517523
}
518524
// don't expect a semicolon statement delimiter after a newline when not otherwise required
519525
Token::Whitespace(Whitespace::Newline) => {
@@ -529,8 +535,10 @@ impl<'a> Parser<'a> {
529535
}
530536

531537
let statement = self.parse_statement()?;
538+
// MSSQL: the `GO` keyword is a batch separator which also means it concludes the current statement
539+
// `GO` may not be followed by a semicolon, so turn off that expectation
540+
expecting_statement_delimiter = !matches!(statement, Statement::Go(_));
532541
stmts.push(statement);
533-
expecting_statement_delimiter = self.options.require_semicolon_stmt_delimiter;
534542
}
535543
Ok(stmts)
536544
}
@@ -681,6 +689,10 @@ impl<'a> Parser<'a> {
681689
self.prev_token();
682690
self.parse_vacuum()
683691
}
692+
Keyword::GO => {
693+
self.prev_token();
694+
self.parse_go()
695+
}
684696
_ => self.expected("an SQL statement", next_token),
685697
},
686698
Token::LParen => {
@@ -4132,6 +4144,17 @@ impl<'a> Parser<'a> {
41324144
})
41334145
}
41344146

4147+
/// Return nth previous token, possibly whitespace
4148+
/// (or [`Token::EOF`] when before the beginning of the stream).
4149+
pub(crate) fn peek_prev_nth_token_no_skip_ref(&self, n: usize) -> &TokenWithSpan {
4150+
// 0 = next token, -1 = current token, -2 = previous token
4151+
let peek_index = self.index.saturating_sub(1).saturating_sub(n);
4152+
if peek_index == 0 {
4153+
return &EOF_TOKEN;
4154+
}
4155+
self.tokens.get(peek_index).unwrap_or(&EOF_TOKEN)
4156+
}
4157+
41354158
/// Return true if the next tokens exactly `expected`
41364159
///
41374160
/// Does not advance the current token.
@@ -4248,6 +4271,29 @@ impl<'a> Parser<'a> {
42484271
)
42494272
}
42504273

4274+
/// Look backwards in the token stream and expect that there was only whitespace tokens until the previous newline or beginning of string
4275+
pub(crate) fn prev_only_whitespace_until_newline(&mut self) -> bool {
4276+
let mut look_back_count = 1;
4277+
loop {
4278+
let prev_token = self.peek_prev_nth_token_no_skip_ref(look_back_count);
4279+
match prev_token.token {
4280+
Token::EOF => break true,
4281+
Token::Whitespace(ref w) => match w {
4282+
Whitespace::Newline => break true,
4283+
// special consideration required for single line comments since that string includes the newline
4284+
Whitespace::SingleLineComment { comment, prefix: _ } => {
4285+
if comment.ends_with('\n') {
4286+
break true;
4287+
}
4288+
look_back_count += 1;
4289+
}
4290+
_ => look_back_count += 1,
4291+
},
4292+
_ => break false,
4293+
};
4294+
}
4295+
}
4296+
42514297
/// If the current token is the `expected` keyword, consume it and returns
42524298
/// true. Otherwise, no tokens are consumed and returns false.
42534299
#[must_use]
@@ -17378,7 +17424,7 @@ impl<'a> Parser<'a> {
1737817424
}
1737917425
}
1738017426

17381-
/// /// Parse a `EXPORT DATA` statement.
17427+
/// Parse a `EXPORT DATA` statement.
1738217428
///
1738317429
/// See [Statement::ExportData]
1738417430
fn parse_export_data(&mut self) -> Result<Statement, ParserError> {
@@ -17436,6 +17482,71 @@ impl<'a> Parser<'a> {
1743617482
}))
1743717483
}
1743817484

17485+
/// Parse [Statement::Go]
17486+
fn parse_go(&mut self) -> Result<Statement, ParserError> {
17487+
self.expect_keyword_is(Keyword::GO)?;
17488+
17489+
// disambiguate between GO as batch delimiter & GO as identifier (etc)
17490+
// compare:
17491+
// ```sql
17492+
// select 1 go
17493+
// ```
17494+
// vs
17495+
// ```sql
17496+
// select 1
17497+
// go
17498+
// ```
17499+
if !self.prev_only_whitespace_until_newline() {
17500+
parser_err!(
17501+
"GO may only be preceded by whitespace on a line",
17502+
self.peek_token().span.start
17503+
)?;
17504+
}
17505+
17506+
let count = loop {
17507+
// using this peek function because we want to halt this statement parsing upon newline
17508+
let next_token = self.peek_token_no_skip();
17509+
match next_token.token {
17510+
Token::EOF => break None::<u64>,
17511+
Token::Whitespace(ref w) => match w {
17512+
Whitespace::Newline => break None,
17513+
_ => _ = self.next_token_no_skip(),
17514+
},
17515+
Token::Number(s, _) => {
17516+
let value = Some(Self::parse::<u64>(s, next_token.span.start)?);
17517+
self.advance_token();
17518+
break value;
17519+
}
17520+
_ => self.expected("literal int or newline", next_token)?,
17521+
};
17522+
};
17523+
17524+
loop {
17525+
let next_token = self.peek_token_no_skip();
17526+
match next_token.token {
17527+
Token::EOF => break,
17528+
Token::Whitespace(ref w) => match w {
17529+
Whitespace::Newline => break,
17530+
Whitespace::SingleLineComment { comment, prefix: _ } => {
17531+
if comment.ends_with('\n') {
17532+
break;
17533+
}
17534+
_ = self.next_token_no_skip();
17535+
}
17536+
_ => _ = self.next_token_no_skip(),
17537+
},
17538+
_ => {
17539+
parser_err!(
17540+
"GO must be followed by a newline or EOF",
17541+
self.peek_token().span.start
17542+
)?;
17543+
}
17544+
};
17545+
}
17546+
17547+
Ok(Statement::Go(GoStatement { count }))
17548+
}
17549+
1743917550
/// Consume the parser and return its underlying token buffer
1744017551
pub fn into_tokens(self) -> Vec<TokenWithSpan> {
1744117552
self.tokens
@@ -17784,6 +17895,31 @@ mod tests {
1778417895
})
1778517896
}
1778617897

17898+
#[test]
17899+
fn test_peek_prev_nth_token_no_skip_ref() {
17900+
all_dialects().run_parser_method(
17901+
"SELECT 1;\n-- a comment\nRAISERROR('test', 16, 0);",
17902+
|parser| {
17903+
parser.index = 1;
17904+
assert_eq!(parser.peek_prev_nth_token_no_skip_ref(0), &Token::EOF);
17905+
assert_eq!(parser.index, 1);
17906+
parser.index = 7;
17907+
assert_eq!(
17908+
parser.token_at(parser.index - 1).token,
17909+
Token::Word(Word {
17910+
value: "RAISERROR".to_string(),
17911+
quote_style: None,
17912+
keyword: Keyword::RAISERROR,
17913+
})
17914+
);
17915+
assert_eq!(
17916+
parser.peek_prev_nth_token_no_skip_ref(2),
17917+
&Token::Whitespace(Whitespace::Newline)
17918+
);
17919+
},
17920+
);
17921+
}
17922+
1778717923
#[cfg(test)]
1778817924
mod test_parse_data_type {
1778917925
use crate::ast::{

src/test_utils.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ impl TestedDialects {
153153
/// 2. re-serializing the result of parsing `sql` produces the same
154154
/// `canonical` sql string
155155
///
156-
/// For multiple statements, use [`statements_parse_to`].
156+
/// For multiple statements, use [`statements_parse_to`].
157157
pub fn one_statement_parses_to(&self, sql: &str, canonical: &str) -> Statement {
158158
let mut statements = self.parse_sql_statements(sql).expect(sql);
159159
assert_eq!(statements.len(), 1);
@@ -170,8 +170,15 @@ impl TestedDialects {
170170
}
171171

172172
/// The same as [`one_statement_parses_to`] but it works for a multiple statements
173-
pub fn statements_parse_to(&self, sql: &str, canonical: &str) -> Vec<Statement> {
173+
pub fn statements_parse_to(
174+
&self,
175+
sql: &str,
176+
statement_count: usize,
177+
canonical: &str,
178+
) -> Vec<Statement> {
174179
let statements = self.parse_sql_statements(sql).expect(sql);
180+
assert_eq!(statements.len(), statement_count);
181+
175182
if !canonical.is_empty() && sql != canonical {
176183
assert_eq!(self.parse_sql_statements(canonical).unwrap(), statements);
177184
} else {

0 commit comments

Comments
 (0)