Skip to content

Commit a75bb7f

Browse files
committed
Add support for GO batch delimiter in SQL Server
- per documentation, "not a statement" but acts like one in all other regards - since it's a batch delimiter and statements can't extend beyond a batch, it also acts as a statement delimiter
1 parent 6506814 commit a75bb7f

File tree

5 files changed

+161
-0
lines changed

5 files changed

+161
-0
lines changed

src/ast/mod.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4339,6 +4339,12 @@ pub enum Statement {
43394339
///
43404340
/// See [ReturnStatement]
43414341
Return(ReturnStatement),
4342+
/// Go (MsSql)
4343+
///
4344+
/// GO is not a Transact-SQL statement; it is a command recognized by various tools as a batch delimiter
4345+
///
4346+
/// See: <https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go>
4347+
Go(GoStatement),
43424348
}
43434349

43444350
/// ```sql
@@ -6166,6 +6172,7 @@ impl fmt::Display for Statement {
61666172
Ok(())
61676173
}
61686174
Statement::Print(s) => write!(f, "{s}"),
6175+
Statement::Go(s) => write!(f, "{s}"),
61696176
Statement::Return(r) => write!(f, "{r}"),
61706177
Statement::List(command) => write!(f, "LIST {command}"),
61716178
Statement::Remove(command) => write!(f, "REMOVE {command}"),
@@ -10074,6 +10081,26 @@ impl fmt::Display for MemberOf {
1007410081
}
1007510082
}
1007610083

10084+
/// Represents a `GO` statement.
10085+
///
10086+
/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go)
10087+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
10088+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
10089+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
10090+
pub struct GoStatement {
10091+
pub count: Option<u64>,
10092+
}
10093+
10094+
impl Display for GoStatement {
10095+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10096+
if let Some(count) = self.count {
10097+
write!(f, "GO {count}")
10098+
} else {
10099+
write!(f, "GO")
10100+
}
10101+
}
10102+
}
10103+
1007710104
#[cfg(test)]
1007810105
mod tests {
1007910106
use crate::tokenizer::Location;

src/ast/spans.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,7 @@ impl Spanned for Statement {
530530
Statement::RaisError { .. } => Span::empty(),
531531
Statement::Print { .. } => Span::empty(),
532532
Statement::Return { .. } => Span::empty(),
533+
Statement::Go { .. } => Span::empty(),
533534
Statement::List(..) | Statement::Remove(..) => Span::empty(),
534535
}
535536
}

src/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ define_keywords!(
404404
GIN,
405405
GIST,
406406
GLOBAL,
407+
GO,
407408
GRANT,
408409
GRANTED,
409410
GRANTS,

src/parser/mod.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,12 @@ impl<'a> Parser<'a> {
483483
if expecting_statement_delimiter && word.keyword == Keyword::END {
484484
break;
485485
}
486+
// Treat batch delimiter as an end of statement
487+
if expecting_statement_delimiter && dialect_of!(self is MsSqlDialect) {
488+
if let Some(Statement::Go(GoStatement { count: _ })) = stmts.last() {
489+
expecting_statement_delimiter = false;
490+
}
491+
}
486492
}
487493
_ => {}
488494
}
@@ -633,6 +639,7 @@ impl<'a> Parser<'a> {
633639
Keyword::COMMENT if self.dialect.supports_comment_on() => self.parse_comment(),
634640
Keyword::PRINT => self.parse_print(),
635641
Keyword::RETURN => self.parse_return(),
642+
Keyword::GO => self.parse_go(),
636643
_ => self.expected("an SQL statement", next_token),
637644
},
638645
Token::LParen => {
@@ -16378,6 +16385,61 @@ impl<'a> Parser<'a> {
1637816385
}
1637916386
}
1638016387

16388+
/// Parse [Statement::Go]
16389+
fn parse_go(&mut self) -> Result<Statement, ParserError> {
16390+
// previous token should be a newline (skipping non-newline whitespace)
16391+
// see also, `previous_token`
16392+
let mut look_back_count = 2;
16393+
loop {
16394+
let prev_index = self.index.saturating_sub(look_back_count);
16395+
if prev_index == 0 {
16396+
break;
16397+
}
16398+
let prev_token = self.token_at(prev_index);
16399+
match prev_token.token {
16400+
Token::Whitespace(ref w) => match w {
16401+
Whitespace::Newline => break,
16402+
_ => look_back_count += 1,
16403+
},
16404+
_ => {
16405+
if prev_token == self.get_current_token() {
16406+
// if we are at the start of the statement, we can skip this check
16407+
break;
16408+
}
16409+
16410+
self.expected("newline before GO", prev_token.clone())?
16411+
}
16412+
};
16413+
}
16414+
16415+
let count = loop {
16416+
// using this peek function because we want to halt this statement parsing upon newline
16417+
let next_token = self.peek_token_no_skip();
16418+
match next_token.token {
16419+
Token::EOF => break None::<u64>,
16420+
Token::Whitespace(ref w) => match w {
16421+
Whitespace::Newline => break None,
16422+
_ => _ = self.next_token_no_skip(),
16423+
},
16424+
Token::Number(s, _) => {
16425+
let value = Some(Self::parse::<u64>(s, next_token.span.start)?);
16426+
self.advance_token();
16427+
break value;
16428+
}
16429+
_ => self.expected("literal int or newline", next_token)?,
16430+
};
16431+
};
16432+
16433+
if self.peek_token().token == Token::SemiColon {
16434+
parser_err!(
16435+
"GO may not end with a semicolon",
16436+
self.peek_token().span.start
16437+
)?;
16438+
}
16439+
16440+
Ok(Statement::Go(GoStatement { count }))
16441+
}
16442+
1638116443
/// Consume the parser and return its underlying token buffer
1638216444
pub fn into_tokens(self) -> Vec<TokenWithSpan> {
1638316445
self.tokens

tests/sqlparser_mssql.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2507,3 +2507,73 @@ DECLARE @Y AS NVARCHAR(MAX)='y'
25072507
assert_eq!(stmts.len(), 2);
25082508
assert!(stmts.iter().all(|s| matches!(s, Statement::Declare { .. })));
25092509
}
2510+
2511+
#[test]
2512+
fn parse_mssql_go_keyword() {
2513+
let single_go_keyword = "USE some_database;\nGO";
2514+
let stmts = ms().parse_sql_statements(single_go_keyword).unwrap();
2515+
assert_eq!(stmts.len(), 2);
2516+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: None }),);
2517+
2518+
let go_with_count = "SELECT 1;\nGO 5";
2519+
let stmts = ms().parse_sql_statements(go_with_count).unwrap();
2520+
assert_eq!(stmts.len(), 2);
2521+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: Some(5) }));
2522+
2523+
let bare_go = "GO";
2524+
let stmts = ms().parse_sql_statements(bare_go).unwrap();
2525+
assert_eq!(stmts.len(), 1);
2526+
assert_eq!(stmts[0], Statement::Go(GoStatement { count: None }));
2527+
2528+
let go_then_statements = "/* whitespace */ GO\nRAISERROR('This is a test', 16, 1);";
2529+
let stmts = ms().parse_sql_statements(go_then_statements).unwrap();
2530+
assert_eq!(stmts.len(), 2);
2531+
assert_eq!(stmts[0], Statement::Go(GoStatement { count: None }));
2532+
assert_eq!(
2533+
stmts[1],
2534+
Statement::RaisError {
2535+
message: Box::new(Expr::Value(
2536+
(Value::SingleQuotedString("This is a test".to_string())).with_empty_span()
2537+
)),
2538+
severity: Box::new(Expr::Value(number("16").with_empty_span())),
2539+
state: Box::new(Expr::Value(number("1").with_empty_span())),
2540+
arguments: vec![],
2541+
options: vec![],
2542+
}
2543+
);
2544+
2545+
let multiple_gos = "SELECT 1;\nGO 5\nSELECT 2;\n GO";
2546+
let stmts = ms().parse_sql_statements(multiple_gos).unwrap();
2547+
assert_eq!(stmts.len(), 4);
2548+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: Some(5) }));
2549+
assert_eq!(stmts[3], Statement::Go(GoStatement { count: None }));
2550+
2551+
let comment_following_go = "USE some_database;\nGO -- okay";
2552+
let stmts = ms().parse_sql_statements(comment_following_go).unwrap();
2553+
assert_eq!(stmts.len(), 2);
2554+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: None }));
2555+
2556+
let actually_column_alias = "SELECT NULL AS GO";
2557+
let stmt = ms().verified_only_select(actually_column_alias);
2558+
assert_eq!(
2559+
only(stmt.projection),
2560+
SelectItem::ExprWithAlias {
2561+
expr: Expr::Value(Value::Null.with_empty_span()),
2562+
alias: Ident::new("GO"),
2563+
}
2564+
);
2565+
2566+
let invalid_go_position = "SELECT 1; GO";
2567+
let err = ms().parse_sql_statements(invalid_go_position);
2568+
assert_eq!(
2569+
err.unwrap_err().to_string(),
2570+
"sql parser error: Expected: newline before GO, found: ;"
2571+
);
2572+
2573+
let invalid_go_count = "SELECT 1\nGO x";
2574+
let err = ms().parse_sql_statements(invalid_go_count);
2575+
assert_eq!(
2576+
err.unwrap_err().to_string(),
2577+
"sql parser error: Expected: end of statement, found: x"
2578+
);
2579+
}

0 commit comments

Comments
 (0)