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 1/2] 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 = [ From 4c18f7a4ab672cbd881641fd485d6313a0e4c086 Mon Sep 17 00:00:00 2001 From: "Guan-Ming (Wesley) Chiu" <105915352+guan404ming@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:51:28 +0800 Subject: [PATCH 2/2] Wrap Throw in named ThrowStatement struct Co-Authored-By: Ifeanyi Ubah <7816405+iffyio@users.noreply.github.com> --- src/ast/mod.rs | 70 +++++++++++++++++++++++++--------------- src/ast/spans.rs | 2 +- src/parser/mod.rs | 48 ++++++++++++++------------- tests/sqlparser_mssql.rs | 8 ++--- 4 files changed, 74 insertions(+), 54 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 7d572c241..9b3e8c2e1 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -2816,6 +2816,41 @@ impl fmt::Display for RaiseStatementValue { } } +/// A MSSQL `THROW` statement. +/// +/// ```sql +/// THROW [ error_number, message, state ] +/// ``` +/// +/// [MSSQL](https://learn.microsoft.com/en-us/sql/t-sql/language-elements/throw-transact-sql) +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub struct ThrowStatement { + /// Error number expression. + pub error_number: Option>, + /// Error message expression. + pub message: Option>, + /// State expression. + pub state: Option>, +} + +impl fmt::Display for ThrowStatement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let ThrowStatement { + error_number, + message, + state, + } = self; + + write!(f, "THROW")?; + if let (Some(error_number), Some(message), Some(state)) = (error_number, message, state) { + write!(f, " {error_number}, {message}, {state}")?; + } + Ok(()) + } +} + /// Represents an expression assignment within a variable `DECLARE` statement. /// /// Examples: @@ -4676,19 +4711,8 @@ 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>, - }, + /// A MSSQL `THROW` statement. + Throw(ThrowStatement), /// ```sql /// PRINT msg_str | @local_variable | string_expr /// ``` @@ -6133,19 +6157,7 @@ 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::Throw(s) => write!(f, "{s}"), Statement::Print(s) => write!(f, "{s}"), Statement::Return(r) => write!(f, "{r}"), Statement::List(command) => write!(f, "LIST {command}"), @@ -11676,6 +11688,12 @@ impl From for Statement { } } +impl From for Statement { + fn from(t: ThrowStatement) -> Self { + Self::Throw(t) + } +} + impl From for Statement { fn from(f: Function) -> Self { Self::Call(f) diff --git a/src/ast/spans.rs b/src/ast/spans.rs index a7ac7db59..9c50663a3 100644 --- a/src/ast/spans.rs +++ b/src/ast/spans.rs @@ -481,7 +481,7 @@ impl Spanned for Statement { Statement::UNLISTEN { .. } => Span::empty(), Statement::RenameTable { .. } => Span::empty(), Statement::RaisError { .. } => Span::empty(), - Statement::Throw { .. } => Span::empty(), + Statement::Throw(_) => Span::empty(), Statement::Print { .. } => Span::empty(), Statement::Return { .. } => Span::empty(), Statement::List(..) | Statement::Remove(..) => Span::empty(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0a0a2b484..53d1d2299 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -670,7 +670,10 @@ 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::THROW => { + self.prev_token(); + self.parse_throw().map(Into::into) + } Keyword::ROLLBACK => self.parse_rollback(), Keyword::ASSERT => self.parse_assert(), // `PREPARE`, `EXECUTE` and `DEALLOCATE` are Postgres-specific @@ -18261,28 +18264,27 @@ 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 MSSQL `THROW` statement. + /// + /// See [Statement::Throw] + pub fn parse_throw(&mut self) -> Result { + self.expect_keyword_is(Keyword::THROW)?; + + let error_number = self.maybe_parse(|p| p.parse_expr().map(Box::new))?; + let (message, state) = if error_number.is_some() { + self.expect_token(&Token::Comma)?; + let message = Box::new(self.parse_expr()?); + self.expect_token(&Token::Comma)?; + let state = Box::new(self.parse_expr()?); + (Some(message), Some(state)) + } else { + (None, None) + }; + + Ok(ThrowStatement { + error_number, + message, + state, }) } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index d577d8ab1..82e6f4621 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -1672,7 +1672,7 @@ fn test_parse_throw() { let s = ms().verified_stmt(sql); assert_eq!( s, - Statement::Throw { + Statement::Throw(ThrowStatement { error_number: Some(Box::new(Expr::Value( (Value::Number("51000".parse().unwrap(), false)).with_empty_span() ))), @@ -1682,7 +1682,7 @@ fn test_parse_throw() { state: Some(Box::new(Expr::Value( (Value::Number("1".parse().unwrap(), false)).with_empty_span() ))), - } + }) ); // THROW with variable references @@ -1694,11 +1694,11 @@ fn test_parse_throw() { let s = ms().verified_stmt(sql); assert_eq!( s, - Statement::Throw { + Statement::Throw(ThrowStatement { error_number: None, message: None, state: None, - } + }) ); }