Skip to content

Commit e99ba87

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 3c61db5 commit e99ba87

File tree

5 files changed

+162
-1
lines changed

5 files changed

+162
-1
lines changed

src/ast/mod.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4317,6 +4317,12 @@ pub enum Statement {
43174317
/// ```
43184318
/// [Redshift](https://docs.aws.amazon.com/redshift/latest/dg/r_VACUUM_command.html)
43194319
Vacuum(VacuumStatement),
4320+
/// Go (MsSql)
4321+
///
4322+
/// GO is not a Transact-SQL statement; it is a command recognized by various tools as a batch delimiter
4323+
///
4324+
/// See: <https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go>
4325+
Go(GoStatement),
43204326
}
43214327

43224328
/// ```sql
@@ -6176,6 +6182,7 @@ impl fmt::Display for Statement {
61766182
Ok(())
61776183
}
61786184
Statement::Print(s) => write!(f, "{s}"),
6185+
Statement::Go(s) => write!(f, "{s}"),
61796186
Statement::Return(r) => write!(f, "{r}"),
61806187
Statement::List(command) => write!(f, "LIST {command}"),
61816188
Statement::Remove(command) => write!(f, "REMOVE {command}"),
@@ -10596,6 +10603,26 @@ impl fmt::Display for CreateTableLikeDefaults {
1059610603
}
1059710604
}
1059810605

10606+
/// Represents a `GO` statement.
10607+
///
10608+
/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go)
10609+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
10610+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
10611+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
10612+
pub struct GoStatement {
10613+
pub count: Option<u64>,
10614+
}
10615+
10616+
impl Display for GoStatement {
10617+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10618+
if let Some(count) = self.count {
10619+
write!(f, "GO {count}")
10620+
} else {
10621+
write!(f, "GO")
10622+
}
10623+
}
10624+
}
10625+
1059910626
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1060010627
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1060110628
#[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/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ define_keywords!(
426426
GIN,
427427
GIST,
428428
GLOBAL,
429+
GO,
429430
GRANT,
430431
GRANTED,
431432
GRANTS,

src/parser/mod.rs

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,12 @@ impl<'a> Parser<'a> {
495495
if expecting_statement_delimiter && word.keyword == Keyword::END {
496496
break;
497497
}
498+
// Treat batch delimiter as an end of statement
499+
if expecting_statement_delimiter && dialect_of!(self is MsSqlDialect) {
500+
if let Some(Statement::Go(GoStatement { count: _ })) = stmts.last() {
501+
expecting_statement_delimiter = false;
502+
}
503+
}
498504
}
499505
_ => {}
500506
}
@@ -656,6 +662,7 @@ impl<'a> Parser<'a> {
656662
self.prev_token();
657663
self.parse_vacuum()
658664
}
665+
Keyword::GO => self.parse_go(),
659666
_ => self.expected("an SQL statement", next_token),
660667
},
661668
Token::LParen => {
@@ -17279,7 +17286,7 @@ impl<'a> Parser<'a> {
1727917286
}
1728017287
}
1728117288

17282-
/// /// Parse a `EXPORT DATA` statement.
17289+
/// Parse a `EXPORT DATA` statement.
1728317290
///
1728417291
/// See [Statement::ExportData]
1728517292
fn parse_export_data(&mut self) -> Result<Statement, ParserError> {
@@ -17337,6 +17344,61 @@ impl<'a> Parser<'a> {
1733717344
}))
1733817345
}
1733917346

17347+
/// Parse [Statement::Go]
17348+
fn parse_go(&mut self) -> Result<Statement, ParserError> {
17349+
// previous token should be a newline (skipping non-newline whitespace)
17350+
// see also, `previous_token`
17351+
let mut look_back_count = 2;
17352+
loop {
17353+
let prev_index = self.index.saturating_sub(look_back_count);
17354+
if prev_index == 0 {
17355+
break;
17356+
}
17357+
let prev_token = self.token_at(prev_index);
17358+
match prev_token.token {
17359+
Token::Whitespace(ref w) => match w {
17360+
Whitespace::Newline => break,
17361+
_ => look_back_count += 1,
17362+
},
17363+
_ => {
17364+
if prev_token == self.get_current_token() {
17365+
// if we are at the start of the statement, we can skip this check
17366+
break;
17367+
}
17368+
17369+
self.expected("newline before GO", prev_token.clone())?
17370+
}
17371+
};
17372+
}
17373+
17374+
let count = loop {
17375+
// using this peek function because we want to halt this statement parsing upon newline
17376+
let next_token = self.peek_token_no_skip();
17377+
match next_token.token {
17378+
Token::EOF => break None::<u64>,
17379+
Token::Whitespace(ref w) => match w {
17380+
Whitespace::Newline => break None,
17381+
_ => _ = self.next_token_no_skip(),
17382+
},
17383+
Token::Number(s, _) => {
17384+
let value = Some(Self::parse::<u64>(s, next_token.span.start)?);
17385+
self.advance_token();
17386+
break value;
17387+
}
17388+
_ => self.expected("literal int or newline", next_token)?,
17389+
};
17390+
};
17391+
17392+
if self.peek_token().token == Token::SemiColon {
17393+
parser_err!(
17394+
"GO may not end with a semicolon",
17395+
self.peek_token().span.start
17396+
)?;
17397+
}
17398+
17399+
Ok(Statement::Go(GoStatement { count }))
17400+
}
17401+
1734017402
/// Consume the parser and return its underlying token buffer
1734117403
pub fn into_tokens(self) -> Vec<TokenWithSpan> {
1734217404
self.tokens

tests/sqlparser_mssql.rs

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

0 commit comments

Comments
 (0)