Skip to content

Commit ea7db7d

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 0d2976d commit ea7db7d

File tree

5 files changed

+163
-2
lines changed

5 files changed

+163
-2
lines changed

src/ast/mod.rs

+21-2
Original file line numberDiff line numberDiff line change
@@ -4050,6 +4050,12 @@ pub enum Statement {
40504050
arguments: Vec<Expr>,
40514051
options: Vec<RaisErrorOption>,
40524052
},
4053+
/// Go (SQL Server)
4054+
///
4055+
/// GO is not a Transact-SQL statement; it is a command recognized by various tools as a batch delimiter
4056+
///
4057+
/// See: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go
4058+
Go(GoStatement),
40534059
}
40544060

40554061
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
@@ -5736,8 +5742,14 @@ impl fmt::Display for Statement {
57365742
write!(f, " WITH {}", display_comma_separated(options))?;
57375743
}
57385744
Ok(())
5739-
}
5740-
5745+
},
5746+
Statement::Go(GoStatement { count }) => {
5747+
write!(f, "GO")?;
5748+
if let Some(count) = count {
5749+
write!(f, " {count}")?;
5750+
}
5751+
Ok(())
5752+
},
57415753
Statement::List(command) => write!(f, "LIST {command}"),
57425754
Statement::Remove(command) => write!(f, "REMOVE {command}"),
57435755
}
@@ -9203,6 +9215,13 @@ pub enum CopyIntoSnowflakeKind {
92039215
Location,
92049216
}
92059217

9218+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
9219+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
9220+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut), visit(with = "visit_statement"))]
9221+
pub struct GoStatement {
9222+
pub count: Option<u64>,
9223+
}
9224+
92069225
#[cfg(test)]
92079226
mod tests {
92089227
use super::*;

src/ast/spans.rs

+1
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,7 @@ impl Spanned for Statement {
518518
Statement::UNLISTEN { .. } => Span::empty(),
519519
Statement::RenameTable { .. } => Span::empty(),
520520
Statement::RaisError { .. } => Span::empty(),
521+
Statement::Go { .. } => Span::empty(),
521522
Statement::List(..) | Statement::Remove(..) => Span::empty(),
522523
}
523524
}

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

+57
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 => {
@@ -15017,6 +15024,56 @@ impl<'a> Parser<'a> {
1501715024
}
1501815025
}
1501915026

15027+
/// Parse [Statement::Go]
15028+
fn parse_go(&mut self) -> Result<Statement, ParserError> {
15029+
// previous token should be a newline (skipping non-newline whitespace)
15030+
// see also, `previous_token`
15031+
let mut look_back_count = 2;
15032+
loop {
15033+
let prev_token = self.token_at(self.index.saturating_sub(look_back_count));
15034+
match prev_token.token {
15035+
Token::Whitespace(ref w) => match w {
15036+
Whitespace::Newline => break,
15037+
_ => look_back_count += 1,
15038+
},
15039+
_ => {
15040+
if prev_token == self.get_current_token() {
15041+
// if we are at the start of the statement, we can skip this check
15042+
break;
15043+
}
15044+
15045+
self.expected("newline before GO", prev_token.clone())?
15046+
},
15047+
};
15048+
}
15049+
15050+
let count = loop {
15051+
// using this peek function because we want to halt this statement parsing upon newline
15052+
let next_token = self.peek_token_no_skip();
15053+
match next_token.token {
15054+
Token::EOF => break None::<u64>,
15055+
Token::Whitespace(ref w) => match w {
15056+
Whitespace::Newline => break None,
15057+
_ => _ = self.next_token_no_skip(),
15058+
},
15059+
Token::Number(s, _) => {
15060+
let value = Some(Self::parse::<u64>(s, next_token.span.start)?);
15061+
self.advance_token();
15062+
break value;
15063+
},
15064+
_ => self.expected("literal int or newline", next_token)?,
15065+
};
15066+
};
15067+
15068+
if self.peek_token().token == Token::SemiColon {
15069+
parser_err!("GO may not end with a semicolon", self.peek_token().span.start)?;
15070+
}
15071+
15072+
Ok(Statement::Go(GoStatement {
15073+
count,
15074+
}))
15075+
}
15076+
1502015077
/// Consume the parser and return its underlying token buffer
1502115078
pub fn into_tokens(self) -> Vec<TokenWithSpan> {
1502215079
self.tokens

tests/sqlparser_mssql.rs

+83
Original file line numberDiff line numberDiff line change
@@ -2036,3 +2036,86 @@ fn parse_mssql_merge_with_output() {
20362036
OUTPUT $action, deleted.ProductID INTO dsi.temp_products";
20372037
ms_and_generic().verified_stmt(stmt);
20382038
}
2039+
2040+
#[test]
2041+
fn parser_mssql_go_keyword() {
2042+
let single_go_keyword = "USE some_database;\nGO";
2043+
let stmts = ms().parse_sql_statements(single_go_keyword).unwrap();
2044+
assert_eq!(stmts.len(), 2);
2045+
assert_eq!(
2046+
stmts[1],
2047+
Statement::Go(GoStatement {
2048+
count: None,
2049+
}),
2050+
);
2051+
2052+
let go_with_count = "SELECT 1;\nGO 5";
2053+
let stmts = ms().parse_sql_statements(go_with_count).unwrap();
2054+
assert_eq!(stmts.len(), 2);
2055+
assert_eq!(
2056+
stmts[1],
2057+
Statement::Go(GoStatement {
2058+
count: Some(5),
2059+
})
2060+
);
2061+
2062+
let bare_go = "GO";
2063+
let stmts = ms().parse_sql_statements(bare_go).unwrap();
2064+
assert_eq!(stmts.len(), 1);
2065+
assert_eq!(
2066+
stmts[0],
2067+
Statement::Go(GoStatement {
2068+
count: None,
2069+
})
2070+
);
2071+
2072+
let multiple_gos = "SELECT 1;\nGO 5\nSELECT 2;\n GO";
2073+
let stmts = ms().parse_sql_statements(multiple_gos).unwrap();
2074+
assert_eq!(stmts.len(), 4);
2075+
assert_eq!(
2076+
stmts[1],
2077+
Statement::Go(GoStatement {
2078+
count: Some(5),
2079+
})
2080+
);
2081+
assert_eq!(
2082+
stmts[3],
2083+
Statement::Go(GoStatement {
2084+
count: None,
2085+
})
2086+
);
2087+
2088+
let comment_following_go = "USE some_database;\nGO -- okay";
2089+
let stmts = ms().parse_sql_statements(comment_following_go).unwrap();
2090+
assert_eq!(stmts.len(), 2);
2091+
assert_eq!(
2092+
stmts[1],
2093+
Statement::Go(GoStatement {
2094+
count: None,
2095+
})
2096+
);
2097+
2098+
let actually_column_alias = "SELECT NULL AS GO";
2099+
let stmt = ms().verified_only_select(actually_column_alias);
2100+
assert_eq!(
2101+
only(stmt.projection),
2102+
SelectItem::ExprWithAlias {
2103+
expr: Expr::Value(Value::Null.with_empty_span()),
2104+
alias: Ident::new("GO"),
2105+
}
2106+
);
2107+
2108+
let invalid_go_position = "SELECT 1; GO";
2109+
let err = ms().parse_sql_statements(invalid_go_position);
2110+
assert_eq!(
2111+
err.unwrap_err().to_string(),
2112+
"sql parser error: Expected: newline before GO, found: ;"
2113+
);
2114+
2115+
let invalid_go_count = "SELECT 1\nGO x";
2116+
let err = ms().parse_sql_statements(invalid_go_count);
2117+
assert_eq!(
2118+
err.unwrap_err().to_string(),
2119+
"sql parser error: Expected: end of statement, found: x"
2120+
);
2121+
}

0 commit comments

Comments
 (0)