From 379434abc35038bc5e839c2b6571a02b2fe130d2 Mon Sep 17 00:00:00 2001 From: "Guan-Ming (Wesley) Chiu" <105915352+guan404ming@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:09:26 +0800 Subject: [PATCH] Add MSSQL THROW statement support Signed-off-by: Guan-Ming (Wesley) Chiu <105915352+guan404ming@users.noreply.github.com> --- src/ast/mod.rs | 26 ++++++++++++++++++++++++++ src/ast/spans.rs | 1 + src/keywords.rs | 1 + src/parser/mod.rs | 26 ++++++++++++++++++++++++++ tests/sqlparser_mssql.rs | 37 +++++++++++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index a59519695..7d572c241 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -4676,6 +4676,19 @@ pub enum Statement { /// Additional `WITH` options for RAISERROR. options: Vec, }, + /// Throw (MSSQL) + /// ```sql + /// THROW [ error_number, message, state ] + /// ``` + /// See + Throw { + /// Error number expression. + error_number: Option>, + /// Error message expression. + message: Option>, + /// State expression. + state: Option>, + }, /// ```sql /// PRINT msg_str | @local_variable | string_expr /// ``` @@ -6120,6 +6133,19 @@ impl fmt::Display for Statement { } Ok(()) } + Statement::Throw { + error_number, + message, + state, + } => { + write!(f, "THROW")?; + if let (Some(error_number), Some(message), Some(state)) = + (error_number, message, state) + { + write!(f, " {error_number}, {message}, {state}")?; + } + Ok(()) + } Statement::Print(s) => write!(f, "{s}"), Statement::Return(r) => write!(f, "{r}"), Statement::List(command) => write!(f, "LIST {command}"), diff --git a/src/ast/spans.rs b/src/ast/spans.rs index 16a9a926f..a7ac7db59 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -481,6 +481,7 @@ impl Spanned for Statement { Statement::UNLISTEN { .. } => Span::empty(), Statement::RenameTable { .. } => Span::empty(), Statement::RaisError { .. } => Span::empty(), + Statement::Throw { .. } => Span::empty(), Statement::Print { .. } => Span::empty(), Statement::Return { .. } => Span::empty(), Statement::List(..) | Statement::Remove(..) => Span::empty(), diff --git a/src/keywords.rs b/src/keywords.rs index f84f4d213..7950b1918 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -1029,6 +1029,7 @@ define_keywords!( TEXT, TEXTFILE, THEN, + THROW, TIES, TIME, TIMEFORMAT, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 14ddd2b50..0a0a2b484 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -670,6 +670,7 @@ impl<'a> Parser<'a> { Keyword::RELEASE => self.parse_release(), Keyword::COMMIT => self.parse_commit(), Keyword::RAISERROR => Ok(self.parse_raiserror()?), + Keyword::THROW => Ok(self.parse_throw()?), Keyword::ROLLBACK => self.parse_rollback(), Keyword::ASSERT => self.parse_assert(), // `PREPARE`, `EXECUTE` and `DEALLOCATE` are Postgres-specific @@ -18260,6 +18261,31 @@ impl<'a> Parser<'a> { } } + /// Parse a MSSQL `THROW` statement + /// See + pub fn parse_throw(&mut self) -> Result { + // THROW with no arguments is a re-throw inside a CATCH block + if self.peek_token_ref().token == Token::SemiColon + || self.peek_token_ref().token == Token::EOF + { + return Ok(Statement::Throw { + error_number: None, + message: None, + state: None, + }); + } + let error_number = Box::new(self.parse_expr()?); + self.expect_token(&Token::Comma)?; + let message = Box::new(self.parse_expr()?); + self.expect_token(&Token::Comma)?; + let state = Box::new(self.parse_expr()?); + Ok(Statement::Throw { + error_number: Some(error_number), + message: Some(message), + state: Some(state), + }) + } + /// Parse a SQL `DEALLOCATE` statement pub fn parse_deallocate(&mut self) -> Result { let prepare = self.parse_keyword(Keyword::PREPARE); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 84b8658b0..d577d8ab1 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1665,6 +1665,43 @@ fn test_parse_raiserror() { let _ = ms().verified_stmt(sql); } +#[test] +fn test_parse_throw() { + // THROW with arguments + let sql = r#"THROW 51000, 'Record does not exist.', 1"#; + let s = ms().verified_stmt(sql); + assert_eq!( + s, + Statement::Throw { + error_number: Some(Box::new(Expr::Value( + (Value::Number("51000".parse().unwrap(), false)).with_empty_span() + ))), + message: Some(Box::new(Expr::Value( + (Value::SingleQuotedString("Record does not exist.".to_string())).with_empty_span() + ))), + state: Some(Box::new(Expr::Value( + (Value::Number("1".parse().unwrap(), false)).with_empty_span() + ))), + } + ); + + // THROW with variable references + let sql = r#"THROW @ErrorNumber, @ErrorMessage, @ErrorState"#; + let _ = ms().verified_stmt(sql); + + // Re-throw (no arguments) + let sql = r#"THROW"#; + let s = ms().verified_stmt(sql); + assert_eq!( + s, + Statement::Throw { + error_number: None, + message: None, + state: None, + } + ); +} + #[test] fn parse_use() { let valid_object_names = [