Skip to content

Commit ba225c3

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 4a48729 commit ba225c3

File tree

5 files changed

+158
-0
lines changed

5 files changed

+158
-0
lines changed

src/ast/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4060,6 +4060,12 @@ pub enum Statement {
40604060
///
40614061
/// See: <https://learn.microsoft.com/en-us/sql/t-sql/statements/print-transact-sql>
40624062
Print(PrintStatement),
4063+
/// Go (MSSQL)
4064+
///
4065+
/// GO is not a Transact-SQL statement; it is a command recognized by various tools as a batch delimiter
4066+
///
4067+
/// See: <https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go>
4068+
Go(GoStatement),
40634069
}
40644070

40654071
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
@@ -5752,6 +5758,7 @@ impl fmt::Display for Statement {
57525758
Ok(())
57535759
}
57545760
Statement::Print(s) => write!(f, "{s}"),
5761+
Statement::Go(s) => write!(f, "{s}"),
57555762
Statement::List(command) => write!(f, "LIST {command}"),
57565763
Statement::Remove(command) => write!(f, "REMOVE {command}"),
57575764
}
@@ -9230,6 +9237,23 @@ impl fmt::Display for PrintStatement {
92309237
}
92319238
}
92329239

9240+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
9241+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9242+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
9243+
pub struct GoStatement {
9244+
pub count: Option<u64>,
9245+
}
9246+
9247+
impl Display for GoStatement {
9248+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9249+
if let Some(count) = self.count {
9250+
write!(f, "GO {count}")
9251+
} else {
9252+
write!(f, "GO")
9253+
}
9254+
}
9255+
}
9256+
92339257
#[cfg(test)]
92349258
mod tests {
92359259
use super::*;

src/ast/spans.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,7 @@ impl Spanned for Statement {
520520
Statement::RenameTable { .. } => Span::empty(),
521521
Statement::RaisError { .. } => Span::empty(),
522522
Statement::Print { .. } => Span::empty(),
523+
Statement::Go { .. } => Span::empty(),
523524
Statement::List(..) | Statement::Remove(..) => Span::empty(),
524525
}
525526
}

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
}
@@ -618,6 +624,7 @@ impl<'a> Parser<'a> {
618624
// `COMMENT` is snowflake specific https://docs.snowflake.com/en/sql-reference/sql/comment
619625
Keyword::COMMENT if self.dialect.supports_comment_on() => self.parse_comment(),
620626
Keyword::PRINT => self.parse_print(),
627+
Keyword::GO => self.parse_go(),
621628
_ => self.expected("an SQL statement", next_token),
622629
},
623630
Token::LParen => {
@@ -15064,6 +15071,61 @@ impl<'a> Parser<'a> {
1506415071
}))
1506515072
}
1506615073

15074+
/// Parse [Statement::Go]
15075+
fn parse_go(&mut self) -> Result<Statement, ParserError> {
15076+
// previous token should be a newline (skipping non-newline whitespace)
15077+
// see also, `previous_token`
15078+
let mut look_back_count = 2;
15079+
loop {
15080+
let prev_index = self.index.saturating_sub(look_back_count);
15081+
if prev_index == 0 {
15082+
break;
15083+
}
15084+
let prev_token = self.token_at(prev_index);
15085+
match prev_token.token {
15086+
Token::Whitespace(ref w) => match w {
15087+
Whitespace::Newline => break,
15088+
_ => look_back_count += 1,
15089+
},
15090+
_ => {
15091+
if prev_token == self.get_current_token() {
15092+
// if we are at the start of the statement, we can skip this check
15093+
break;
15094+
}
15095+
15096+
self.expected("newline before GO", prev_token.clone())?
15097+
}
15098+
};
15099+
}
15100+
15101+
let count = loop {
15102+
// using this peek function because we want to halt this statement parsing upon newline
15103+
let next_token = self.peek_token_no_skip();
15104+
match next_token.token {
15105+
Token::EOF => break None::<u64>,
15106+
Token::Whitespace(ref w) => match w {
15107+
Whitespace::Newline => break None,
15108+
_ => _ = self.next_token_no_skip(),
15109+
},
15110+
Token::Number(s, _) => {
15111+
let value = Some(Self::parse::<u64>(s, next_token.span.start)?);
15112+
self.advance_token();
15113+
break value;
15114+
}
15115+
_ => self.expected("literal int or newline", next_token)?,
15116+
};
15117+
};
15118+
15119+
if self.peek_token().token == Token::SemiColon {
15120+
parser_err!(
15121+
"GO may not end with a semicolon",
15122+
self.peek_token().span.start
15123+
)?;
15124+
}
15125+
15126+
Ok(Statement::Go(GoStatement { count }))
15127+
}
15128+
1506715129
/// Consume the parser and return its underlying token buffer
1506815130
pub fn into_tokens(self) -> Vec<TokenWithSpan> {
1506915131
self.tokens

tests/sqlparser_mssql.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2070,3 +2070,73 @@ fn parse_print() {
20702070
let _ = ms().verified_stmt("PRINT N'Hello, ⛄️!'");
20712071
let _ = ms().verified_stmt("PRINT @my_variable");
20722072
}
2073+
2074+
#[test]
2075+
fn parse_mssql_go_keyword() {
2076+
let single_go_keyword = "USE some_database;\nGO";
2077+
let stmts = ms().parse_sql_statements(single_go_keyword).unwrap();
2078+
assert_eq!(stmts.len(), 2);
2079+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: None }),);
2080+
2081+
let go_with_count = "SELECT 1;\nGO 5";
2082+
let stmts = ms().parse_sql_statements(go_with_count).unwrap();
2083+
assert_eq!(stmts.len(), 2);
2084+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: Some(5) }));
2085+
2086+
let bare_go = "GO";
2087+
let stmts = ms().parse_sql_statements(bare_go).unwrap();
2088+
assert_eq!(stmts.len(), 1);
2089+
assert_eq!(stmts[0], Statement::Go(GoStatement { count: None }));
2090+
2091+
let go_then_statements = "/* whitespace */ GO\nRAISERROR('This is a test', 16, 1);";
2092+
let stmts = ms().parse_sql_statements(go_then_statements).unwrap();
2093+
assert_eq!(stmts.len(), 2);
2094+
assert_eq!(stmts[0], Statement::Go(GoStatement { count: None }));
2095+
assert_eq!(
2096+
stmts[1],
2097+
Statement::RaisError {
2098+
message: Box::new(Expr::Value(
2099+
(Value::SingleQuotedString("This is a test".to_string())).with_empty_span()
2100+
)),
2101+
severity: Box::new(Expr::Value(number("16").with_empty_span())),
2102+
state: Box::new(Expr::Value(number("1").with_empty_span())),
2103+
arguments: vec![],
2104+
options: vec![],
2105+
}
2106+
);
2107+
2108+
let multiple_gos = "SELECT 1;\nGO 5\nSELECT 2;\n GO";
2109+
let stmts = ms().parse_sql_statements(multiple_gos).unwrap();
2110+
assert_eq!(stmts.len(), 4);
2111+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: Some(5) }));
2112+
assert_eq!(stmts[3], Statement::Go(GoStatement { count: None }));
2113+
2114+
let comment_following_go = "USE some_database;\nGO -- okay";
2115+
let stmts = ms().parse_sql_statements(comment_following_go).unwrap();
2116+
assert_eq!(stmts.len(), 2);
2117+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: None }));
2118+
2119+
let actually_column_alias = "SELECT NULL AS GO";
2120+
let stmt = ms().verified_only_select(actually_column_alias);
2121+
assert_eq!(
2122+
only(stmt.projection),
2123+
SelectItem::ExprWithAlias {
2124+
expr: Expr::Value(Value::Null.with_empty_span()),
2125+
alias: Ident::new("GO"),
2126+
}
2127+
);
2128+
2129+
let invalid_go_position = "SELECT 1; GO";
2130+
let err = ms().parse_sql_statements(invalid_go_position);
2131+
assert_eq!(
2132+
err.unwrap_err().to_string(),
2133+
"sql parser error: Expected: newline before GO, found: ;"
2134+
);
2135+
2136+
let invalid_go_count = "SELECT 1\nGO x";
2137+
let err = ms().parse_sql_statements(invalid_go_count);
2138+
assert_eq!(
2139+
err.unwrap_err().to_string(),
2140+
"sql parser error: Expected: end of statement, found: x"
2141+
);
2142+
}

0 commit comments

Comments
 (0)