Skip to content

Commit dc5556d

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 64f4b1f commit dc5556d

5 files changed

Lines changed: 162 additions & 0 deletions

File tree

src/ast/mod.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4833,6 +4833,12 @@ pub enum Statement {
48334833
/// ```
48344834
/// [PostgreSQL](https://www.postgresql.org/docs/current/sql-reset.html)
48354835
Reset(ResetStatement),
4836+
/// Go (MsSql)
4837+
///
4838+
/// GO is not a Transact-SQL statement; it is a command recognized by various tools as a batch delimiter
4839+
///
4840+
/// See: <https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go>
4841+
Go(GoStatement),
48364842
}
48374843

48384844
impl From<Analyze> for Statement {
@@ -6247,6 +6253,7 @@ impl fmt::Display for Statement {
62476253
Statement::Throw(s) => write!(f, "{s}"),
62486254
Statement::Print(s) => write!(f, "{s}"),
62496255
Statement::WaitFor(s) => write!(f, "{s}"),
6256+
Statement::Go(s) => write!(f, "{s}"),
62506257
Statement::Return(r) => write!(f, "{r}"),
62516258
Statement::List(command) => write!(f, "LIST {command}"),
62526259
Statement::Remove(command) => write!(f, "REMOVE {command}"),
@@ -11547,6 +11554,27 @@ impl fmt::Display for CreateTableLikeDefaults {
1154711554
}
1154811555
}
1154911556

11557+
/// Represents a `GO` statement.
11558+
///
11559+
/// [MsSql](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/sql-server-utilities-statements-go)
11560+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
11561+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
11562+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
11563+
pub struct GoStatement {
11564+
/// How many times the batch should be executed, if specified (e.g., `GO 10`).
11565+
pub count: Option<u64>,
11566+
}
11567+
11568+
impl Display for GoStatement {
11569+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
11570+
if let Some(count) = self.count {
11571+
write!(f, "GO {count}")
11572+
} else {
11573+
write!(f, "GO")
11574+
}
11575+
}
11576+
}
11577+
1155011578
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
1155111579
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
1155211580
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]

src/ast/spans.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,7 @@ impl Spanned for Statement {
485485
Statement::Print { .. } => Span::empty(),
486486
Statement::WaitFor(_) => Span::empty(),
487487
Statement::Return { .. } => Span::empty(),
488+
Statement::Go { .. } => Span::empty(),
488489
Statement::List(..) | Statement::Remove(..) => Span::empty(),
489490
Statement::ExportData(ExportData {
490491
options,

src/keywords.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ define_keywords!(
463463
GIN,
464464
GIST,
465465
GLOBAL,
466+
GO,
466467
GRANT,
467468
GRANTED,
468469
GRANTS,

src/parser/mod.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,12 @@ impl<'a> Parser<'a> {
512512
if expecting_statement_delimiter && word.keyword == Keyword::END {
513513
break;
514514
}
515+
// Treat batch delimiter as an end of statement
516+
if expecting_statement_delimiter && dialect_of!(self is MsSqlDialect) {
517+
if let Some(Statement::Go(GoStatement { count: _ })) = stmts.last() {
518+
expecting_statement_delimiter = false;
519+
}
520+
}
515521
}
516522
_ => {}
517523
}
@@ -715,6 +721,7 @@ impl<'a> Parser<'a> {
715721
self.parse_vacuum()
716722
}
717723
Keyword::RESET => self.parse_reset().map(Into::into),
724+
Keyword::GO => self.parse_go(),
718725
_ => self.expected("an SQL statement", next_token),
719726
},
720727
Token::LParen => {
@@ -19637,6 +19644,61 @@ impl<'a> Parser<'a> {
1963719644
}))
1963819645
}
1963919646

19647+
/// Parse [Statement::Go]
19648+
fn parse_go(&mut self) -> Result<Statement, ParserError> {
19649+
// previous token should be a newline (skipping non-newline whitespace)
19650+
// see also, `previous_token`
19651+
let mut look_back_count = 2;
19652+
loop {
19653+
let prev_index = self.index.saturating_sub(look_back_count);
19654+
if prev_index == 0 {
19655+
break;
19656+
}
19657+
let prev_token = self.token_at(prev_index);
19658+
match prev_token.token {
19659+
Token::Whitespace(ref w) => match w {
19660+
Whitespace::Newline => break,
19661+
_ => look_back_count += 1,
19662+
},
19663+
_ => {
19664+
if prev_token == self.get_current_token() {
19665+
// if we are at the start of the statement, we can skip this check
19666+
break;
19667+
}
19668+
19669+
self.expected("newline before GO", prev_token.clone())?
19670+
}
19671+
};
19672+
}
19673+
19674+
let count = loop {
19675+
// using this peek function because we want to halt this statement parsing upon newline
19676+
let next_token = self.peek_token_no_skip();
19677+
match next_token.token {
19678+
Token::EOF => break None::<u64>,
19679+
Token::Whitespace(ref w) => match w {
19680+
Whitespace::Newline => break None,
19681+
_ => _ = self.next_token_no_skip(),
19682+
},
19683+
Token::Number(s, _) => {
19684+
let value = Some(Self::parse::<u64>(s, next_token.span.start)?);
19685+
self.advance_token();
19686+
break value;
19687+
}
19688+
_ => self.expected("literal int or newline", next_token)?,
19689+
};
19690+
};
19691+
19692+
if self.peek_token().token == Token::SemiColon {
19693+
parser_err!(
19694+
"GO may not end with a semicolon",
19695+
self.peek_token().span.start
19696+
)?;
19697+
}
19698+
19699+
Ok(Statement::Go(GoStatement { count }))
19700+
}
19701+
1964019702
/// Consume the parser and return its underlying token buffer
1964119703
pub fn into_tokens(self) -> Vec<TokenWithSpan> {
1964219704
self.tokens

tests/sqlparser_mssql.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2854,3 +2854,73 @@ fn parse_mssql_update_with_output_into() {
28542854
"UPDATE employees SET salary = salary * 1.1 OUTPUT INSERTED.id, DELETED.salary, INSERTED.salary INTO @changes WHERE department = 'Engineering'",
28552855
);
28562856
}
2857+
2858+
#[test]
2859+
fn parse_mssql_go_keyword() {
2860+
let single_go_keyword = "USE some_database;\nGO";
2861+
let stmts = ms().parse_sql_statements(single_go_keyword).unwrap();
2862+
assert_eq!(stmts.len(), 2);
2863+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: None }),);
2864+
2865+
let go_with_count = "SELECT 1;\nGO 5";
2866+
let stmts = ms().parse_sql_statements(go_with_count).unwrap();
2867+
assert_eq!(stmts.len(), 2);
2868+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: Some(5) }));
2869+
2870+
let bare_go = "GO";
2871+
let stmts = ms().parse_sql_statements(bare_go).unwrap();
2872+
assert_eq!(stmts.len(), 1);
2873+
assert_eq!(stmts[0], Statement::Go(GoStatement { count: None }));
2874+
2875+
let go_then_statements = "/* whitespace */ GO\nRAISERROR('This is a test', 16, 1);";
2876+
let stmts = ms().parse_sql_statements(go_then_statements).unwrap();
2877+
assert_eq!(stmts.len(), 2);
2878+
assert_eq!(stmts[0], Statement::Go(GoStatement { count: None }));
2879+
assert_eq!(
2880+
stmts[1],
2881+
Statement::RaisError {
2882+
message: Box::new(Expr::Value(
2883+
(Value::SingleQuotedString("This is a test".to_string())).with_empty_span()
2884+
)),
2885+
severity: Box::new(Expr::Value(number("16").with_empty_span())),
2886+
state: Box::new(Expr::Value(number("1").with_empty_span())),
2887+
arguments: vec![],
2888+
options: vec![],
2889+
}
2890+
);
2891+
2892+
let multiple_gos = "SELECT 1;\nGO 5\nSELECT 2;\n GO";
2893+
let stmts = ms().parse_sql_statements(multiple_gos).unwrap();
2894+
assert_eq!(stmts.len(), 4);
2895+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: Some(5) }));
2896+
assert_eq!(stmts[3], Statement::Go(GoStatement { count: None }));
2897+
2898+
let comment_following_go = "USE some_database;\nGO -- okay";
2899+
let stmts = ms().parse_sql_statements(comment_following_go).unwrap();
2900+
assert_eq!(stmts.len(), 2);
2901+
assert_eq!(stmts[1], Statement::Go(GoStatement { count: None }));
2902+
2903+
let actually_column_alias = "SELECT NULL AS GO";
2904+
let stmt = ms().verified_only_select(actually_column_alias);
2905+
assert_eq!(
2906+
only(stmt.projection),
2907+
SelectItem::ExprWithAlias {
2908+
expr: Expr::Value(Value::Null.with_empty_span()),
2909+
alias: Ident::new("GO"),
2910+
}
2911+
);
2912+
2913+
let invalid_go_position = "SELECT 1; GO";
2914+
let err = ms().parse_sql_statements(invalid_go_position);
2915+
assert_eq!(
2916+
err.unwrap_err().to_string(),
2917+
"sql parser error: Expected: newline before GO, found: ;"
2918+
);
2919+
2920+
let invalid_go_count = "SELECT 1\nGO x";
2921+
let err = ms().parse_sql_statements(invalid_go_count);
2922+
assert_eq!(
2923+
err.unwrap_err().to_string(),
2924+
"sql parser error: Expected: end of statement, found: x"
2925+
);
2926+
}

0 commit comments

Comments
 (0)