From cf094406267ab65ac210ab97876651ab6f38de98 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Wed, 30 Oct 2024 13:50:50 +0100 Subject: [PATCH 1/2] improve support of T-SQL EXECUTE statements This adds the following features from the Microsoft SQL Server EXECUTE statement: Support EXEC as an alias to EXECUTE fixes #1275 Support schema qualified stored procedures fixes #1276 support unnamed parameters in mssql EXECUTE stored procedure calls fixes #1489 --- src/ast/mod.rs | 23 +++++++++++++++++------ src/parser/mod.rs | 20 +++++++++++++++----- tests/sqlparser_mssql.rs | 35 +++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 9 ++++++--- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0573240a2..8a5a36b0d 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3095,10 +3095,14 @@ pub enum Statement { /// EXECUTE name [ ( parameter [, ...] ) ] [USING ] /// ``` /// - /// Note: this is a PostgreSQL-specific statement. + /// Note: this statement is supported by Postgres and MSSQL, with slight differences in syntax. + /// + /// Postgres: + /// MSSQL: Execute { - name: Ident, + name: ObjectName, parameters: Vec, + has_parentheses: bool, using: Vec, }, /// ```sql @@ -4510,12 +4514,19 @@ impl fmt::Display for Statement { Statement::Execute { name, parameters, + has_parentheses, using, } => { - write!(f, "EXECUTE {name}")?; - if !parameters.is_empty() { - write!(f, "({})", display_comma_separated(parameters))?; - } + let (open, close) = if *has_parentheses { + ("(", ")") + } else { + (if parameters.is_empty() { "" } else { " " }, "") + }; + write!( + f, + "EXECUTE {name}{open}{}{close}", + display_comma_separated(parameters), + )?; if !using.is_empty() { write!(f, " USING {}", display_comma_separated(using))?; }; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a9a5b1df4..a2c57bb4d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -529,7 +529,7 @@ impl<'a> Parser<'a> { // `PREPARE`, `EXECUTE` and `DEALLOCATE` are Postgres-specific // syntaxes. They are used for Postgres prepared statement. Keyword::DEALLOCATE => self.parse_deallocate(), - Keyword::EXECUTE => self.parse_execute(), + Keyword::EXECUTE | Keyword::EXEC => self.parse_execute(), Keyword::PREPARE => self.parse_prepare(), Keyword::MERGE => self.parse_merge(), // `PRAGMA` is sqlite specific https://www.sqlite.org/pragma.html @@ -11726,11 +11726,20 @@ impl<'a> Parser<'a> { } pub fn parse_execute(&mut self) -> Result { - let name = self.parse_identifier(false)?; + let name = self.parse_object_name(false)?; - let mut parameters = vec![]; - if self.consume_token(&Token::LParen) { - parameters = self.parse_comma_separated(Parser::parse_expr)?; + let has_parentheses = self.consume_token(&Token::LParen); + + let end_token = match (has_parentheses, self.peek_token().token) { + (true, _) => Token::RParen, + (false, Token::EOF) => Token::EOF, + (false, Token::Word(w)) if w.keyword == Keyword::USING => Token::Word(w), + (false, _) => Token::SemiColon, + }; + + let parameters = self.parse_comma_separated0(Parser::parse_expr, end_token)?; + + if has_parentheses { self.expect_token(&Token::RParen)?; } @@ -11746,6 +11755,7 @@ impl<'a> Parser<'a> { Ok(Statement::Execute { name, parameters, + has_parentheses, using, }) } diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 0223e2915..eee8898e9 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -570,6 +570,41 @@ fn parse_substring_in_select() { } } +#[test] +fn parse_mssql_execute_stored_procedure() { + let expected = Statement::Execute { + name: ObjectName(vec![ + Ident { + value: "my_schema".to_string(), + quote_style: None, + }, + Ident { + value: "my_stored_procedure".to_string(), + quote_style: None, + }, + ]), + parameters: vec![ + Expr::Value(Value::NationalStringLiteral("param1".to_string())), + Expr::Value(Value::NationalStringLiteral("param2".to_string())), + ], + has_parentheses: false, + using: vec![], + }; + assert_eq!( + ms().verified_stmt("EXECUTE my_schema.my_stored_procedure N'param1', N'param2'"), + expected + ); + assert_eq!( + Parser::parse_sql( + &MsSqlDialect {}, + "EXEC my_schema.my_stored_procedure N'param1', N'param2';" + ) + .unwrap()[0], + expected, + "EXEC should be parsed the same as EXECUTE" + ); +} + #[test] fn parse_mssql_declare() { let sql = "DECLARE @foo CURSOR, @bar INT, @baz AS TEXT = 'foobar';"; diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index b9b3811ba..f4c7ab7cd 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -1538,8 +1538,9 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: "a".into(), + name: ObjectName(vec!["a".into()]), parameters: vec![], + has_parentheses: false, using: vec![] } ); @@ -1548,11 +1549,12 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: "a".into(), + name: ObjectName(vec!["a".into()]), parameters: vec![ Expr::Value(number("1")), Expr::Value(Value::SingleQuotedString("t".to_string())) ], + has_parentheses: true, using: vec![] } ); @@ -1562,8 +1564,9 @@ fn parse_execute() { assert_eq!( stmt, Statement::Execute { - name: "a".into(), + name: ObjectName(vec!["a".into()]), parameters: vec![], + has_parentheses: false, using: vec![ Expr::Cast { kind: CastKind::Cast, From dbf412c7c62a5c241392f63fa070c9d7656c4bf5 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Fri, 1 Nov 2024 15:43:39 +0100 Subject: [PATCH 2/2] move EXECUTE tests to sqlparser_common addresses https://github.com/apache/datafusion-sqlparser-rs/pull/1490?notification_referrer_id=NT_kwDOAAhutbIxMzEyMDE0NzcyNDo1NTI2Mjk#discussion_r1825434648 and https://github.com/apache/datafusion-sqlparser-rs/pull/1490?notification_referrer_id=NT_kwDOAAhutbIxMzEyMDE0NzcyNDo1NTI2Mjk#discussion_r1825432804 --- tests/sqlparser_common.rs | 39 +++++++++++++++++++++++++++++++++++++++ tests/sqlparser_mssql.rs | 35 ----------------------------------- 2 files changed, 39 insertions(+), 35 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index a2eb5070d..6c8c1884b 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -1395,6 +1395,10 @@ fn pg_and_generic() -> TestedDialects { ]) } +fn ms_and_generic() -> TestedDialects { + TestedDialects::new(vec![Box::new(MsSqlDialect {}), Box::new(GenericDialect {})]) +} + #[test] fn parse_json_ops_without_colon() { use self::BinaryOperator::*; @@ -9730,6 +9734,41 @@ fn parse_call() { ); } +#[test] +fn parse_execute_stored_procedure() { + let expected = Statement::Execute { + name: ObjectName(vec![ + Ident { + value: "my_schema".to_string(), + quote_style: None, + }, + Ident { + value: "my_stored_procedure".to_string(), + quote_style: None, + }, + ]), + parameters: vec![ + Expr::Value(Value::NationalStringLiteral("param1".to_string())), + Expr::Value(Value::NationalStringLiteral("param2".to_string())), + ], + has_parentheses: false, + using: vec![], + }; + assert_eq!( + // Microsoft SQL Server does not use parentheses around arguments for EXECUTE + ms_and_generic() + .verified_stmt("EXECUTE my_schema.my_stored_procedure N'param1', N'param2'"), + expected + ); + assert_eq!( + ms_and_generic().one_statement_parses_to( + "EXEC my_schema.my_stored_procedure N'param1', N'param2';", + "EXECUTE my_schema.my_stored_procedure N'param1', N'param2'", + ), + expected + ); +} + #[test] fn parse_create_table_collate() { pg_and_generic().verified_stmt("CREATE TABLE tbl (foo INT, bar TEXT COLLATE \"de_DE\")"); diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index eee8898e9..0223e2915 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -570,41 +570,6 @@ fn parse_substring_in_select() { } } -#[test] -fn parse_mssql_execute_stored_procedure() { - let expected = Statement::Execute { - name: ObjectName(vec![ - Ident { - value: "my_schema".to_string(), - quote_style: None, - }, - Ident { - value: "my_stored_procedure".to_string(), - quote_style: None, - }, - ]), - parameters: vec![ - Expr::Value(Value::NationalStringLiteral("param1".to_string())), - Expr::Value(Value::NationalStringLiteral("param2".to_string())), - ], - has_parentheses: false, - using: vec![], - }; - assert_eq!( - ms().verified_stmt("EXECUTE my_schema.my_stored_procedure N'param1', N'param2'"), - expected - ); - assert_eq!( - Parser::parse_sql( - &MsSqlDialect {}, - "EXEC my_schema.my_stored_procedure N'param1', N'param2';" - ) - .unwrap()[0], - expected, - "EXEC should be parsed the same as EXECUTE" - ); -} - #[test] fn parse_mssql_declare() { let sql = "DECLARE @foo CURSOR, @bar INT, @baz AS TEXT = 'foobar';";