Skip to content

Commit 6ac67c4

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 6ac67c4

File tree

5 files changed

+160
-1
lines changed

5 files changed

+160
-1
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,11 @@ impl Spanned for Statement {
519519
Statement::UNLISTEN { .. } => Span::empty(),
520520
Statement::RenameTable { .. } => Span::empty(),
521521
Statement::RaisError { .. } => Span::empty(),
522+
<<<<<<< HEAD
522523
Statement::Print { .. } => Span::empty(),
524+
=======
525+
Statement::Go { .. } => Span::empty(),
526+
>>>>>>> 6296781 (Add support for `GO` batch delimiter in SQL Server)
523527
Statement::List(..) | Statement::Remove(..) => Span::empty(),
524528
}
525529
}

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: 61 additions & 1 deletion
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 => {
@@ -15059,11 +15066,64 @@ impl<'a> Parser<'a> {
1505915066

1506015067
/// Parse [Statement::Print]
1506115068
fn parse_print(&mut self) -> Result<Statement, ParserError> {
15062-
Ok(Statement::Print(PrintStatement {
1506315069
message: Box::new(self.parse_expr()?),
1506415070
}))
1506515071
}
1506615072

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