From b6f225d82c128c75021818f42d4b136cbee17a92 Mon Sep 17 00:00:00 2001 From: "Guan-Ming (Wesley) Chiu" <105915352+guan404ming@users.noreply.github.com> Date: Sat, 31 Jan 2026 02:50:44 +0800 Subject: [PATCH 1/4] MSSQL: Support standalone BEGIN...END blocks Signed-off-by: Guan-Ming (Wesley) Chiu <105915352+guan404ming@users.noreply.github.com> --- src/dialect/mssql.rs | 17 +++++++++- tests/sqlparser_mssql.rs | 71 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 9f8e726562..39a730bb64 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -145,7 +145,22 @@ impl Dialect for MsSqlDialect { } fn parse_statement(&self, parser: &mut Parser) -> Option> { - if parser.peek_keyword(Keyword::IF) { + if parser.parse_keyword(Keyword::BEGIN) { + if parser.peek_keyword(Keyword::TRANSACTION) + || parser.peek_keyword(Keyword::WORK) + || parser.peek_keyword(Keyword::TRY) + || parser.peek_keyword(Keyword::CATCH) + || parser.peek_keyword(Keyword::DEFERRED) + || parser.peek_keyword(Keyword::IMMEDIATE) + || parser.peek_keyword(Keyword::EXCLUSIVE) + || parser.peek_token_ref().token == Token::SemiColon + || parser.peek_token_ref().token == Token::EOF + { + parser.prev_token(); + return None; + } + return Some(parser.parse_begin_exception_end()); + } else if parser.peek_keyword(Keyword::IF) { Some(self.parse_if_stmt(parser)) } else if parser.parse_keywords(&[Keyword::CREATE, Keyword::TRIGGER]) { Some(self.parse_create_trigger(parser, false)) diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 7ef4ce85c2..d770008847 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2554,3 +2554,74 @@ fn test_sql_keywords_as_column_aliases() { } } } + +#[test] +fn parse_mssql_begin_end_block() { + // Single statement + let sql = "BEGIN SELECT 1; END"; + let stmt = ms().verified_stmt(sql); + match &stmt { + Statement::StartTransaction { + begin, + has_end_keyword, + statements, + transaction, + modifier, + .. + } => { + assert!(begin); + assert!(has_end_keyword); + assert!(transaction.is_none()); + assert!(modifier.is_none()); + assert_eq!(statements.len(), 1); + } + _ => panic!("Expected StartTransaction, got: {stmt:?}"), + } + + // Multiple statements + let sql = "BEGIN SELECT 1; SELECT 2; END"; + let stmt = ms().verified_stmt(sql); + match &stmt { + Statement::StartTransaction { + statements, + has_end_keyword, + .. + } => { + assert!(has_end_keyword); + assert_eq!(statements.len(), 2); + } + _ => panic!("Expected StartTransaction, got: {stmt:?}"), + } + + // DML inside BEGIN/END + let sql = "BEGIN INSERT INTO t VALUES (1); UPDATE t SET x = 2; END"; + let stmt = ms().verified_stmt(sql); + match &stmt { + Statement::StartTransaction { + statements, + has_end_keyword, + .. + } => { + assert!(has_end_keyword); + assert_eq!(statements.len(), 2); + } + _ => panic!("Expected StartTransaction, got: {stmt:?}"), + } + + // BEGIN TRANSACTION still works + let sql = "BEGIN TRANSACTION"; + let stmt = ms().verified_stmt(sql); + match &stmt { + Statement::StartTransaction { + begin, + has_end_keyword, + transaction, + .. + } => { + assert!(begin); + assert!(!has_end_keyword); + assert!(transaction.is_some()); + } + _ => panic!("Expected StartTransaction, got: {stmt:?}"), + } +} From bf35213b433ead3d4d945379403e75bbeabfdc3b Mon Sep 17 00:00:00 2001 From: "Guan-Ming (Wesley) Chiu" <105915352+guan404ming@users.noreply.github.com> Date: Sat, 31 Jan 2026 03:02:01 +0800 Subject: [PATCH 2/4] Fix lint error Signed-off-by: Guan-Ming (Wesley) Chiu <105915352+guan404ming@users.noreply.github.com> --- src/dialect/mssql.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 39a730bb64..1817de54fc 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -159,7 +159,7 @@ impl Dialect for MsSqlDialect { parser.prev_token(); return None; } - return Some(parser.parse_begin_exception_end()); + Some(parser.parse_begin_exception_end()) } else if parser.peek_keyword(Keyword::IF) { Some(self.parse_if_stmt(parser)) } else if parser.parse_keywords(&[Keyword::CREATE, Keyword::TRIGGER]) { From 74ce546de1b6c4f890b661fa716245b4fdb73762 Mon Sep 17 00:00:00 2001 From: "Guan-Ming (Wesley) Chiu" <105915352+guan404ming@users.noreply.github.com> Date: Tue, 3 Feb 2026 00:19:28 +0800 Subject: [PATCH 3/4] Refactor logic to mod.rs Signed-off-by: Guan-Ming (Wesley) Chiu <105915352+guan404ming@users.noreply.github.com> --- src/dialect/mssql.rs | 31 +++++++++++++++++++------------ src/parser/mod.rs | 13 +++++++++---- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/dialect/mssql.rs b/src/dialect/mssql.rs index 1817de54fc..acb5e49372 100644 --- a/src/dialect/mssql.rs +++ b/src/dialect/mssql.rs @@ -146,20 +146,27 @@ impl Dialect for MsSqlDialect { fn parse_statement(&self, parser: &mut Parser) -> Option> { if parser.parse_keyword(Keyword::BEGIN) { - if parser.peek_keyword(Keyword::TRANSACTION) - || parser.peek_keyword(Keyword::WORK) - || parser.peek_keyword(Keyword::TRY) - || parser.peek_keyword(Keyword::CATCH) - || parser.peek_keyword(Keyword::DEFERRED) - || parser.peek_keyword(Keyword::IMMEDIATE) - || parser.peek_keyword(Keyword::EXCLUSIVE) - || parser.peek_token_ref().token == Token::SemiColon - || parser.peek_token_ref().token == Token::EOF - { + // Check if this is a BEGIN...END block rather than BEGIN TRANSACTION + let is_block = parser + .maybe_parse(|p| { + if p.parse_transaction_modifier().is_some() + || p.parse_one_of_keywords(&[Keyword::TRANSACTION, Keyword::WORK]) + .is_some() + || matches!(p.peek_token_ref().token, Token::SemiColon | Token::EOF) + { + p.expected("statement", p.peek_token()) + } else { + Ok(()) + } + }) + .unwrap_or(None) + .is_some(); + if is_block { + Some(parser.parse_begin_exception_end()) + } else { parser.prev_token(); - return None; + None } - Some(parser.parse_begin_exception_end()) } else if parser.peek_keyword(Keyword::IF) { Some(self.parse_if_stmt(parser)) } else if parser.parse_keywords(&[Keyword::CREATE, Keyword::TRIGGER]) { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2763114314..3921a4c685 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17925,9 +17925,9 @@ impl<'a> Parser<'a> { }) } - /// Parse a 'BEGIN' statement - pub fn parse_begin(&mut self) -> Result { - let modifier = if !self.dialect.supports_start_transaction_modifier() { + /// Parse a transaction modifier keyword that can follow a BEGIN statement. + pub fn parse_transaction_modifier(&mut self) -> Option { + if !self.dialect.supports_start_transaction_modifier() { None } else if self.parse_keyword(Keyword::DEFERRED) { Some(TransactionModifier::Deferred) @@ -17941,7 +17941,12 @@ impl<'a> Parser<'a> { Some(TransactionModifier::Catch) } else { None - }; + } + } + + /// Parse a 'BEGIN' statement + pub fn parse_begin(&mut self) -> Result { + let modifier = self.parse_transaction_modifier(); let transaction = match self.parse_one_of_keywords(&[Keyword::TRANSACTION, Keyword::WORK]) { Some(Keyword::TRANSACTION) => Some(BeginTransactionKind::Transaction), Some(Keyword::WORK) => Some(BeginTransactionKind::Work), From b89d16cb7606eba8a2280ec858b5ac5bc1f65100 Mon Sep 17 00:00:00 2001 From: "Guan-Ming (Wesley) Chiu" <105915352+guan404ming@users.noreply.github.com> Date: Tue, 3 Feb 2026 23:23:15 +0800 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Ifeanyi Ubah --- src/parser/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 3921a4c685..74ce1d1722 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -17925,8 +17925,8 @@ impl<'a> Parser<'a> { }) } - /// Parse a transaction modifier keyword that can follow a BEGIN statement. - pub fn parse_transaction_modifier(&mut self) -> Option { + /// Parse a transaction modifier keyword that can follow a `BEGIN` statement. + pub(crate) fn parse_transaction_modifier(&mut self) -> Option { if !self.dialect.supports_start_transaction_modifier() { None } else if self.parse_keyword(Keyword::DEFERRED) {