Skip to content

Commit 6f0e803

Browse files
authored
MSSQL: support EXEC (@SQL) dynamic SQL execution (#2234)
1 parent 523d78e commit 6f0e803

File tree

2 files changed

+43
-3
lines changed

2 files changed

+43
-3
lines changed

src/parser/mod.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18583,6 +18583,9 @@ impl<'a> Parser<'a> {
1858318583

1858418584
/// Parse a SQL `EXECUTE` statement
1858518585
pub fn parse_execute(&mut self) -> Result<Statement, ParserError> {
18586+
// Track whether the procedure/expression name itself was wrapped in parens,
18587+
// i.e. `EXEC (@sql)` (dynamic string execution) vs `EXEC sp_name`.
18588+
// When the name has parens there are no additional parameters.
1858618589
let name = if self.dialect.supports_execute_immediate()
1858718590
&& self.parse_keyword(Keyword::IMMEDIATE)
1858818591
{
@@ -18593,10 +18596,18 @@ impl<'a> Parser<'a> {
1859318596
if has_parentheses {
1859418597
self.expect_token(&Token::RParen)?;
1859518598
}
18596-
Some(name)
18599+
Some((name, has_parentheses))
1859718600
};
1859818601

18599-
let has_parentheses = self.consume_token(&Token::LParen);
18602+
let name_had_parentheses = name.as_ref().map(|(_, p)| *p).unwrap_or(false);
18603+
18604+
// Only look for a parameter list when the name was NOT wrapped in parens.
18605+
// `EXEC (@sql)` is dynamic SQL execution and takes no parameters here.
18606+
let has_parentheses = if name_had_parentheses {
18607+
false
18608+
} else {
18609+
self.consume_token(&Token::LParen)
18610+
};
1860018611

1860118612
let end_kws = &[Keyword::USING, Keyword::OUTPUT, Keyword::DEFAULT];
1860218613
let end_token = match (has_parentheses, self.peek_token().token) {
@@ -18606,12 +18617,18 @@ impl<'a> Parser<'a> {
1860618617
(false, _) => Token::SemiColon,
1860718618
};
1860818619

18609-
let parameters = self.parse_comma_separated0(Parser::parse_expr, end_token)?;
18620+
let parameters = if name_had_parentheses {
18621+
vec![]
18622+
} else {
18623+
self.parse_comma_separated0(Parser::parse_expr, end_token)?
18624+
};
1861018625

1861118626
if has_parentheses {
1861218627
self.expect_token(&Token::RParen)?;
1861318628
}
1861418629

18630+
let name = name.map(|(n, _)| n);
18631+
1861518632
let into = if self.parse_keyword(Keyword::INTO) {
1861618633
self.parse_comma_separated(Self::parse_identifier)?
1861718634
} else {

tests/sqlparser_mssql.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2783,3 +2783,26 @@ fn test_tsql_statement_keywords_not_implicit_aliases() {
27832783
);
27842784
}
27852785
}
2786+
2787+
#[test]
2788+
fn test_exec_dynamic_sql() {
2789+
// EXEC (@sql) executes a dynamic SQL string held in a variable.
2790+
// It must parse as a single Execute statement and not attempt to parse
2791+
// parameters after the closing paren.
2792+
let stmts = tsql()
2793+
.parse_sql_statements("EXEC (@sql)")
2794+
.expect("EXEC (@sql) should parse");
2795+
assert_eq!(stmts.len(), 1);
2796+
assert!(
2797+
matches!(&stmts[0], Statement::Execute { .. }),
2798+
"expected Execute, got: {:?}",
2799+
stmts[0]
2800+
);
2801+
2802+
// Verify that a statement following EXEC (@sql) on the next line is parsed
2803+
// as a separate statement and not consumed as a parameter.
2804+
let stmts = tsql()
2805+
.parse_sql_statements("EXEC (@sql)\nDROP TABLE #tmp")
2806+
.expect("EXEC (@sql) followed by DROP TABLE should parse");
2807+
assert_eq!(stmts.len(), 2);
2808+
}

0 commit comments

Comments
 (0)