Skip to content

Commit 6296781

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 6296781

File tree

5 files changed

+158
-1
lines changed

5 files changed

+158
-1
lines changed

src/ast/mod.rs

+24-1
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

+1
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

+1
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

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

tests/sqlparser_mssql.rs

+70
Original file line numberDiff line numberDiff line change
@@ -2053,3 +2053,73 @@ 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 go_then_statements = "/* whitespace */ GO\nRAISERROR('This is a test', 16, 1);";
2075+
let stmts = ms().parse_sql_statements(go_then_statements).unwrap();
2076+
assert_eq!(stmts.len(), 2);
2077+
assert_eq!(stmts[0], Statement::Go(GoStatement { count: None }));
2078+
assert_eq!(
2079+
stmts[1],
2080+
Statement::RaisError {
2081+
message: Box::new(Expr::Value(
2082+
(Value::SingleQuotedString("This is a test".to_string())).with_empty_span()
2083+
)),
2084+
severity: Box::new(Expr::Value(number("16").with_empty_span())),
2085+
state: Box::new(Expr::Value(number("1").with_empty_span())),
2086+
arguments: vec![],
2087+
options: vec![],
2088+
}
2089+
);
2090+
2091+
let multiple_gos = "SELECT 1;\nGO 5\nSELECT 2;\n GO";
2092+
let stmts = ms().parse_sql_statements(multiple_gos).unwrap();
2093+
assert_eq!(stmts.len(), 4);
2094+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: Some(5) }));
2095+
assert_eq!(stmts[3], Statement::Go(GoStatement { count: None }));
2096+
2097+
let comment_following_go = "USE some_database;\nGO -- okay";
2098+
let stmts = ms().parse_sql_statements(comment_following_go).unwrap();
2099+
assert_eq!(stmts.len(), 2);
2100+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: None }));
2101+
2102+
let actually_column_alias = "SELECT NULL AS GO";
2103+
let stmt = ms().verified_only_select(actually_column_alias);
2104+
assert_eq!(
2105+
only(stmt.projection),
2106+
SelectItem::ExprWithAlias {
2107+
expr: Expr::Value(Value::Null.with_empty_span()),
2108+
alias: Ident::new("GO"),
2109+
}
2110+
);
2111+
2112+
let invalid_go_position = "SELECT 1; GO";
2113+
let err = ms().parse_sql_statements(invalid_go_position);
2114+
assert_eq!(
2115+
err.unwrap_err().to_string(),
2116+
"sql parser error: Expected: newline before GO, found: ;"
2117+
);
2118+
2119+
let invalid_go_count = "SELECT 1\nGO x";
2120+
let err = ms().parse_sql_statements(invalid_go_count);
2121+
assert_eq!(
2122+
err.unwrap_err().to_string(),
2123+
"sql parser error: Expected: end of statement, found: x"
2124+
);
2125+
}

0 commit comments

Comments
 (0)