Skip to content

Commit 25fd849

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 87d1907 commit 25fd849

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
@@ -4097,6 +4097,12 @@ pub enum Statement {
40974097
///
40984098
/// See [ReturnStatement]
40994099
Return(ReturnStatement),
4100+
/// Go (MsSql)
4101+
///
4102+
/// GO is not a Transact-SQL statement; it is a command recognized by various tools as a batch delimiter
4103+
///
4104+
/// See: <https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go>
4105+
Go(GoStatement),
41004106
}
41014107

41024108
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
@@ -5791,6 +5797,7 @@ impl fmt::Display for Statement {
57915797
Ok(())
57925798
}
57935799
Statement::Print(s) => write!(f, "{s}"),
5800+
Statement::Go(s) => write!(f, "{s}"),
57945801
Statement::Return(r) => write!(f, "{r}"),
57955802
Statement::List(command) => write!(f, "LIST {command}"),
57965803
Statement::Remove(command) => write!(f, "REMOVE {command}"),
@@ -9315,6 +9322,26 @@ pub enum ReturnStatementValue {
93159322
Expr(Expr),
93169323
}
93179324

9325+
/// Represents a `GO` statement.
9326+
///
9327+
/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go)
9328+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
9329+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9330+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
9331+
pub struct GoStatement {
9332+
pub count: Option<u64>,
9333+
}
9334+
9335+
impl Display for GoStatement {
9336+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9337+
if let Some(count) = self.count {
9338+
write!(f, "GO {count}")
9339+
} else {
9340+
write!(f, "GO")
9341+
}
9342+
}
9343+
}
9344+
93189345
#[cfg(test)]
93199346
mod tests {
93209347
use super::*;

src/ast/spans.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ impl Spanned for Statement {
522522
Statement::RaisError { .. } => Span::empty(),
523523
Statement::Print { .. } => Span::empty(),
524524
Statement::Return { .. } => Span::empty(),
525+
Statement::Go { .. } => Span::empty(),
525526
Statement::List(..) | Statement::Remove(..) => Span::empty(),
526527
}
527528
}

src/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ define_keywords!(
393393
GIN,
394394
GIST,
395395
GLOBAL,
396+
GO,
396397
GRANT,
397398
GRANTED,
398399
GRANTS,

src/parser/mod.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,12 @@ impl<'a> Parser<'a> {
475475
if expecting_statement_delimiter && word.keyword == Keyword::END {
476476
break;
477477
}
478+
// Treat batch delimiter as an end of statement
479+
if expecting_statement_delimiter && dialect_of!(self is MsSqlDialect) {
480+
if let Some(Statement::Go(GoStatement { count: _ })) = stmts.last() {
481+
expecting_statement_delimiter = false;
482+
}
483+
}
478484
}
479485
_ => {}
480486
}
@@ -613,6 +619,7 @@ impl<'a> Parser<'a> {
613619
Keyword::COMMENT if self.dialect.supports_comment_on() => self.parse_comment(),
614620
Keyword::PRINT => self.parse_print(),
615621
Keyword::RETURN => self.parse_return(),
622+
Keyword::GO => self.parse_go(),
616623
_ => self.expected("an SQL statement", next_token),
617624
},
618625
Token::LParen => {
@@ -15225,6 +15232,61 @@ impl<'a> Parser<'a> {
1522515232
}
1522615233
}
1522715234

15235+
/// Parse [Statement::Go]
15236+
fn parse_go(&mut self) -> Result<Statement, ParserError> {
15237+
// previous token should be a newline (skipping non-newline whitespace)
15238+
// see also, `previous_token`
15239+
let mut look_back_count = 2;
15240+
loop {
15241+
let prev_index = self.index.saturating_sub(look_back_count);
15242+
if prev_index == 0 {
15243+
break;
15244+
}
15245+
let prev_token = self.token_at(prev_index);
15246+
match prev_token.token {
15247+
Token::Whitespace(ref w) => match w {
15248+
Whitespace::Newline => break,
15249+
_ => look_back_count += 1,
15250+
},
15251+
_ => {
15252+
if prev_token == self.get_current_token() {
15253+
// if we are at the start of the statement, we can skip this check
15254+
break;
15255+
}
15256+
15257+
self.expected("newline before GO", prev_token.clone())?
15258+
}
15259+
};
15260+
}
15261+
15262+
let count = loop {
15263+
// using this peek function because we want to halt this statement parsing upon newline
15264+
let next_token = self.peek_token_no_skip();
15265+
match next_token.token {
15266+
Token::EOF => break None::<u64>,
15267+
Token::Whitespace(ref w) => match w {
15268+
Whitespace::Newline => break None,
15269+
_ => _ = self.next_token_no_skip(),
15270+
},
15271+
Token::Number(s, _) => {
15272+
let value = Some(Self::parse::<u64>(s, next_token.span.start)?);
15273+
self.advance_token();
15274+
break value;
15275+
}
15276+
_ => self.expected("literal int or newline", next_token)?,
15277+
};
15278+
};
15279+
15280+
if self.peek_token().token == Token::SemiColon {
15281+
parser_err!(
15282+
"GO may not end with a semicolon",
15283+
self.peek_token().span.start
15284+
)?;
15285+
}
15286+
15287+
Ok(Statement::Go(GoStatement { count }))
15288+
}
15289+
1522815290
/// Consume the parser and return its underlying token buffer
1522915291
pub fn into_tokens(self) -> Vec<TokenWithSpan> {
1523015292
self.tokens

tests/sqlparser_mssql.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2156,3 +2156,73 @@ fn parse_print() {
21562156
let _ = ms().verified_stmt("PRINT N'Hello, ⛄️!'");
21572157
let _ = ms().verified_stmt("PRINT @my_variable");
21582158
}
2159+
2160+
#[test]
2161+
fn parse_mssql_go_keyword() {
2162+
let single_go_keyword = "USE some_database;\nGO";
2163+
let stmts = ms().parse_sql_statements(single_go_keyword).unwrap();
2164+
assert_eq!(stmts.len(), 2);
2165+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: None }),);
2166+
2167+
let go_with_count = "SELECT 1;\nGO 5";
2168+
let stmts = ms().parse_sql_statements(go_with_count).unwrap();
2169+
assert_eq!(stmts.len(), 2);
2170+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: Some(5) }));
2171+
2172+
let bare_go = "GO";
2173+
let stmts = ms().parse_sql_statements(bare_go).unwrap();
2174+
assert_eq!(stmts.len(), 1);
2175+
assert_eq!(stmts[0], Statement::Go(GoStatement { count: None }));
2176+
2177+
let go_then_statements = "/* whitespace */ GO\nRAISERROR('This is a test', 16, 1);";
2178+
let stmts = ms().parse_sql_statements(go_then_statements).unwrap();
2179+
assert_eq!(stmts.len(), 2);
2180+
assert_eq!(stmts[0], Statement::Go(GoStatement { count: None }));
2181+
assert_eq!(
2182+
stmts[1],
2183+
Statement::RaisError {
2184+
message: Box::new(Expr::Value(
2185+
(Value::SingleQuotedString("This is a test".to_string())).with_empty_span()
2186+
)),
2187+
severity: Box::new(Expr::Value(number("16").with_empty_span())),
2188+
state: Box::new(Expr::Value(number("1").with_empty_span())),
2189+
arguments: vec![],
2190+
options: vec![],
2191+
}
2192+
);
2193+
2194+
let multiple_gos = "SELECT 1;\nGO 5\nSELECT 2;\n GO";
2195+
let stmts = ms().parse_sql_statements(multiple_gos).unwrap();
2196+
assert_eq!(stmts.len(), 4);
2197+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: Some(5) }));
2198+
assert_eq!(stmts[3], Statement::Go(GoStatement { count: None }));
2199+
2200+
let comment_following_go = "USE some_database;\nGO -- okay";
2201+
let stmts = ms().parse_sql_statements(comment_following_go).unwrap();
2202+
assert_eq!(stmts.len(), 2);
2203+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: None }));
2204+
2205+
let actually_column_alias = "SELECT NULL AS GO";
2206+
let stmt = ms().verified_only_select(actually_column_alias);
2207+
assert_eq!(
2208+
only(stmt.projection),
2209+
SelectItem::ExprWithAlias {
2210+
expr: Expr::Value(Value::Null.with_empty_span()),
2211+
alias: Ident::new("GO"),
2212+
}
2213+
);
2214+
2215+
let invalid_go_position = "SELECT 1; GO";
2216+
let err = ms().parse_sql_statements(invalid_go_position);
2217+
assert_eq!(
2218+
err.unwrap_err().to_string(),
2219+
"sql parser error: Expected: newline before GO, found: ;"
2220+
);
2221+
2222+
let invalid_go_count = "SELECT 1\nGO x";
2223+
let err = ms().parse_sql_statements(invalid_go_count);
2224+
assert_eq!(
2225+
err.unwrap_err().to_string(),
2226+
"sql parser error: Expected: end of statement, found: x"
2227+
);
2228+
}

0 commit comments

Comments
 (0)