From 928eb66beefddb09b2983b0bfa583d0b42f7b629 Mon Sep 17 00:00:00 2001 From: Yoa Bot Date: Mon, 23 Feb 2026 13:09:05 +0100 Subject: [PATCH] MSSQL: support EXEC (@sql) dynamic SQL execution parse_execute() was consuming a second parameter list after already parsing the parenthesised name expression, causing parse failures on any token that immediately followed EXEC (@sql). Fixed by tracking whether the name was itself wrapped in parens; when it is, skip the parameter-list scan and leave no tokens consumed for the caller to mis-interpret. Adds test_exec_dynamic_sql covering both the standalone form and the case where a subsequent statement follows on the next line. --- src/parser/mod.rs | 23 ++++++++++++++++++++--- tests/sqlparser_mssql.rs | 23 +++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index bea566bbe..75db4d240 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -18583,6 +18583,9 @@ impl<'a> Parser<'a> { /// Parse a SQL `EXECUTE` statement pub fn parse_execute(&mut self) -> Result { + // Track whether the procedure/expression name itself was wrapped in parens, + // i.e. `EXEC (@sql)` (dynamic string execution) vs `EXEC sp_name`. + // When the name has parens there are no additional parameters. let name = if self.dialect.supports_execute_immediate() && self.parse_keyword(Keyword::IMMEDIATE) { @@ -18593,10 +18596,18 @@ impl<'a> Parser<'a> { if has_parentheses { self.expect_token(&Token::RParen)?; } - Some(name) + Some((name, has_parentheses)) }; - let has_parentheses = self.consume_token(&Token::LParen); + let name_had_parentheses = name.as_ref().map(|(_, p)| *p).unwrap_or(false); + + // Only look for a parameter list when the name was NOT wrapped in parens. + // `EXEC (@sql)` is dynamic SQL execution and takes no parameters here. + let has_parentheses = if name_had_parentheses { + false + } else { + self.consume_token(&Token::LParen) + }; let end_kws = &[Keyword::USING, Keyword::OUTPUT, Keyword::DEFAULT]; let end_token = match (has_parentheses, self.peek_token().token) { @@ -18606,12 +18617,18 @@ impl<'a> Parser<'a> { (false, _) => Token::SemiColon, }; - let parameters = self.parse_comma_separated0(Parser::parse_expr, end_token)?; + let parameters = if name_had_parentheses { + vec![] + } else { + self.parse_comma_separated0(Parser::parse_expr, end_token)? + }; if has_parentheses { self.expect_token(&Token::RParen)?; } + let name = name.map(|(n, _)| n); + let into = if self.parse_keyword(Keyword::INTO) { self.parse_comma_separated(Self::parse_identifier)? } else { diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 6c8412a4a..8bdb1c205 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -2783,3 +2783,26 @@ fn test_tsql_statement_keywords_not_implicit_aliases() { ); } } + +#[test] +fn test_exec_dynamic_sql() { + // EXEC (@sql) executes a dynamic SQL string held in a variable. + // It must parse as a single Execute statement and not attempt to parse + // parameters after the closing paren. + let stmts = tsql() + .parse_sql_statements("EXEC (@sql)") + .expect("EXEC (@sql) should parse"); + assert_eq!(stmts.len(), 1); + assert!( + matches!(&stmts[0], Statement::Execute { .. }), + "expected Execute, got: {:?}", + stmts[0] + ); + + // Verify that a statement following EXEC (@sql) on the next line is parsed + // as a separate statement and not consumed as a parameter. + let stmts = tsql() + .parse_sql_statements("EXEC (@sql)\nDROP TABLE #tmp") + .expect("EXEC (@sql) followed by DROP TABLE should parse"); + assert_eq!(stmts.len(), 2); +}