Skip to content

Commit c9a846a

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 514d2ec commit c9a846a

File tree

5 files changed

+137
-1
lines changed

5 files changed

+137
-1
lines changed

src/ast/mod.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4054,6 +4054,12 @@ pub enum Statement {
40544054
arguments: Vec<Expr>,
40554055
options: Vec<RaisErrorOption>,
40564056
},
4057+
/// Go (MSSQL)
4058+
///
4059+
/// GO is not a Transact-SQL statement; it is a command recognized by various tools as a batch delimiter
4060+
///
4061+
/// See <https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go>
4062+
Go(GoStatement),
40574063
}
40584064

40594065
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
@@ -5745,7 +5751,7 @@ impl fmt::Display for Statement {
57455751
}
57465752
Ok(())
57475753
}
5748-
5754+
Statement::Go(s) => write!(f, "{s}"),
57495755
Statement::List(command) => write!(f, "LIST {command}"),
57505756
Statement::Remove(command) => write!(f, "REMOVE {command}"),
57515757
}
@@ -9211,6 +9217,23 @@ pub enum CopyIntoSnowflakeKind {
92119217
Location,
92129218
}
92139219

9220+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
9221+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9222+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
9223+
pub struct GoStatement {
9224+
pub count: Option<u64>,
9225+
}
9226+
9227+
impl Display for GoStatement {
9228+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
9229+
if let Some(count) = self.count {
9230+
write!(f, "GO {count}")
9231+
} else {
9232+
write!(f, "GO")
9233+
}
9234+
}
9235+
}
9236+
92149237
#[cfg(test)]
92159238
mod tests {
92169239
use super::*;

src/ast/spans.rs

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

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: 58 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
}
@@ -617,6 +623,7 @@ impl<'a> Parser<'a> {
617623
}
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(),
626+
Keyword::GO => self.parse_go(),
620627
_ => self.expected("an SQL statement", next_token),
621628
},
622629
Token::LParen => {
@@ -15058,6 +15065,57 @@ impl<'a> Parser<'a> {
1505815065
}
1505915066
}
1506015067

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

tests/sqlparser_mssql.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2053,3 +2053,56 @@ fn parse_drop_trigger() {
20532053
}
20542054
);
20552055
}
2056+
2057+
#[test]
2058+
fn parse_mssql_go_keyword() {
2059+
let single_go_keyword = "USE some_database;\nGO";
2060+
let stmts = ms().parse_sql_statements(single_go_keyword).unwrap();
2061+
assert_eq!(stmts.len(), 2);
2062+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: None }),);
2063+
2064+
let go_with_count = "SELECT 1;\nGO 5";
2065+
let stmts = ms().parse_sql_statements(go_with_count).unwrap();
2066+
assert_eq!(stmts.len(), 2);
2067+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: Some(5) }));
2068+
2069+
let bare_go = "GO";
2070+
let stmts = ms().parse_sql_statements(bare_go).unwrap();
2071+
assert_eq!(stmts.len(), 1);
2072+
assert_eq!(stmts[0], Statement::Go(GoStatement { count: None }));
2073+
2074+
let multiple_gos = "SELECT 1;\nGO 5\nSELECT 2;\n GO";
2075+
let stmts = ms().parse_sql_statements(multiple_gos).unwrap();
2076+
assert_eq!(stmts.len(), 4);
2077+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: Some(5) }));
2078+
assert_eq!(stmts[3], Statement::Go(GoStatement { count: None }));
2079+
2080+
let comment_following_go = "USE some_database;\nGO -- okay";
2081+
let stmts = ms().parse_sql_statements(comment_following_go).unwrap();
2082+
assert_eq!(stmts.len(), 2);
2083+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: None }));
2084+
2085+
let actually_column_alias = "SELECT NULL AS GO";
2086+
let stmt = ms().verified_only_select(actually_column_alias);
2087+
assert_eq!(
2088+
only(stmt.projection),
2089+
SelectItem::ExprWithAlias {
2090+
expr: Expr::Value(Value::Null.with_empty_span()),
2091+
alias: Ident::new("GO"),
2092+
}
2093+
);
2094+
2095+
let invalid_go_position = "SELECT 1; GO";
2096+
let err = ms().parse_sql_statements(invalid_go_position);
2097+
assert_eq!(
2098+
err.unwrap_err().to_string(),
2099+
"sql parser error: Expected: newline before GO, found: ;"
2100+
);
2101+
2102+
let invalid_go_count = "SELECT 1\nGO x";
2103+
let err = ms().parse_sql_statements(invalid_go_count);
2104+
assert_eq!(
2105+
err.unwrap_err().to_string(),
2106+
"sql parser error: Expected: end of statement, found: x"
2107+
);
2108+
}

0 commit comments

Comments
 (0)