From 639d789ca1ddecd28bce1d5e72755568f3046d56 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Mon, 4 Dec 2023 22:42:06 +0100 Subject: [PATCH 1/2] add support for CALL statements see https://dev.mysql.com/doc/refman/8.0/en/call.html Adds support for MySQL's CALL statements (stored procedure invocation) This one is very important for SQLPage too, because it allows working around other unsupported syntax by wrapping logic in stored procedures. --- src/ast/mod.rs | 50 +++++++++++++++++++++++++++++++--------- src/parser/mod.rs | 27 ++++++++++++++++++++++ tests/sqlparser_mysql.rs | 8 +++++++ 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 4769ea9bd..fba7e5588 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1437,6 +1437,7 @@ pub enum Statement { file_format: Option, source: Box, }, + Call(Function), Copy { /// The source of 'COPY TO', or the target of 'COPY FROM' source: CopySource, @@ -1715,7 +1716,9 @@ pub enum Statement { /// /// Note: this is a PostgreSQL-specific statement, /// but may also compatible with other SQL. - Discard { object_type: DiscardObject }, + Discard { + object_type: DiscardObject, + }, /// SET `[ SESSION | LOCAL ]` ROLE role_name. Examples: [ANSI][1], [Postgresql][2], [MySQL][3], and [Oracle][4]. /// /// [1]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#set-role-statement @@ -1747,7 +1750,10 @@ pub enum Statement { /// /// Note: this is a PostgreSQL-specific statements /// `SET TIME ZONE ` is an alias for `SET timezone TO ` in PostgreSQL - SetTimeZone { local: bool, value: Expr }, + SetTimeZone { + local: bool, + value: Expr, + }, /// SET NAMES 'charset_name' [COLLATE 'collation_name'] /// /// Note: this is a MySQL-specific statement. @@ -1762,13 +1768,17 @@ pub enum Statement { /// SHOW FUNCTIONS /// /// Note: this is a Presto-specific statement. - ShowFunctions { filter: Option }, + ShowFunctions { + filter: Option, + }, /// ```sql /// SHOW /// ``` /// /// Note: this is a PostgreSQL-specific statement. - ShowVariable { variable: Vec }, + ShowVariable { + variable: Vec, + }, /// SHOW VARIABLES /// /// Note: this is a MySQL-specific statement. @@ -1806,11 +1816,15 @@ pub enum Statement { /// SHOW COLLATION /// /// Note: this is a MySQL-specific statement. - ShowCollation { filter: Option }, + ShowCollation { + filter: Option, + }, /// USE /// /// Note: This is a MySQL-specific statement. - Use { db_name: Ident }, + Use { + db_name: Ident, + }, /// `START [ TRANSACTION | WORK ] | START TRANSACTION } ...` /// If `begin` is false. /// @@ -1838,7 +1852,9 @@ pub enum Statement { if_exists: bool, }, /// `COMMIT [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ]` - Commit { chain: bool }, + Commit { + chain: bool, + }, /// `ROLLBACK [ TRANSACTION | WORK ] [ AND [ NO ] CHAIN ] [ TO [ SAVEPOINT ] savepoint_name ]` Rollback { chain: bool, @@ -1934,11 +1950,17 @@ pub enum Statement { /// `DEALLOCATE [ PREPARE ] { name | ALL }` /// /// Note: this is a PostgreSQL-specific statement. - Deallocate { name: Ident, prepare: bool }, + Deallocate { + name: Ident, + prepare: bool, + }, /// `EXECUTE name [ ( parameter [, ...] ) ]` /// /// Note: this is a PostgreSQL-specific statement. - Execute { name: Ident, parameters: Vec }, + Execute { + name: Ident, + parameters: Vec, + }, /// `PREPARE name [ ( data_type [, ...] ) ] AS statement` /// /// Note: this is a PostgreSQL-specific statement. @@ -1979,9 +2001,13 @@ pub enum Statement { format: Option, }, /// SAVEPOINT -- define a new savepoint within the current transaction - Savepoint { name: Ident }, + Savepoint { + name: Ident, + }, /// RELEASE \[ SAVEPOINT \] savepoint_name - ReleaseSavepoint { name: Ident }, + ReleaseSavepoint { + name: Ident, + }, // MERGE INTO statement, based on Snowflake. See Merge { // optional INTO keyword @@ -2303,6 +2329,8 @@ impl fmt::Display for Statement { Ok(()) } + Statement::Call(function) => write!(f, "CALL {function}"), + Statement::Copy { source, to, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d9d4761c3..5497d0f84 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -493,6 +493,7 @@ impl<'a> Parser<'a> { Keyword::UNCACHE => Ok(self.parse_uncache_table()?), Keyword::UPDATE => Ok(self.parse_update()?), Keyword::ALTER => Ok(self.parse_alter()?), + Keyword::CALL => Ok(self.parse_call()?), Keyword::COPY => Ok(self.parse_copy()?), Keyword::CLOSE => Ok(self.parse_close()?), Keyword::SET => Ok(self.parse_set()?), @@ -4773,6 +4774,32 @@ impl<'a> Parser<'a> { }) } + /// Parse a `CALL procedure_name(arg1, arg2, ...)` + /// or `CALL procedure_name` statement + pub fn parse_call(&mut self) -> Result { + let object_name = self.parse_object_name()?; + if self.peek_token().token == Token::LParen { + match self.parse_function(object_name)? { + Expr::Function(f) => Ok(Statement::Call(f)), + other => parser_err!( + format!("Expected a simple procedure call but found: {other}"), + self.peek_token().location + ), + } + } else { + Ok(Statement::Call(Function { + name: object_name, + args: vec![], + over: None, + distinct: false, + filter: None, + null_treatment: None, + special: true, + order_by: vec![], + })) + } + } + /// Parse a copy statement pub fn parse_copy(&mut self) -> Result { let source; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 0036d52c3..dba270f69 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1817,6 +1817,14 @@ fn parse_hex_string_introducer() { ) } +#[test] +fn parse_call() { + mysql().verified_stmt("CALL my_procedure()"); + mysql().verified_stmt("CALL my_procedure(1, 'a')"); + mysql().verified_stmt("CALL my_procedure(1, 'a', @my_var)"); + mysql().verified_stmt("CALL my_procedure"); +} + #[test] fn parse_string_introducers() { mysql().verified_stmt("SELECT _binary 'abc'"); From 4ba955eb0bd9a9d8940ca4df93c7632ead9f7a85 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Wed, 6 Dec 2023 09:47:30 +0100 Subject: [PATCH 2/2] Test CALL statements on all dialects... ... not just mysql fixes https://github.com/sqlparser-rs/sqlparser-rs/pull/1063#discussion_r1416479549 --- tests/sqlparser_common.rs | 23 +++++++++++++++++++++++ tests/sqlparser_mysql.rs | 8 -------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 1d0923b4f..fea90c4c6 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7949,6 +7949,29 @@ fn parse_create_type() { ); } +#[test] +fn parse_call() { + all_dialects().verified_stmt("CALL my_procedure()"); + all_dialects().verified_stmt("CALL my_procedure(1, 'a')"); + pg_and_generic().verified_stmt("CALL my_procedure(1, 'a', $1)"); + all_dialects().verified_stmt("CALL my_procedure"); + assert_eq!( + verified_stmt("CALL my_procedure('a')"), + Statement::Call(Function { + args: vec![FunctionArg::Unnamed(FunctionArgExpr::Expr(Expr::Value( + Value::SingleQuotedString("a".to_string()) + ))),], + name: ObjectName(vec![Ident::new("my_procedure")]), + filter: None, + null_treatment: None, + over: None, + distinct: false, + special: false, + order_by: vec![] + }) + ); +} + #[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_mysql.rs b/tests/sqlparser_mysql.rs index dba270f69..0036d52c3 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1817,14 +1817,6 @@ fn parse_hex_string_introducer() { ) } -#[test] -fn parse_call() { - mysql().verified_stmt("CALL my_procedure()"); - mysql().verified_stmt("CALL my_procedure(1, 'a')"); - mysql().verified_stmt("CALL my_procedure(1, 'a', @my_var)"); - mysql().verified_stmt("CALL my_procedure"); -} - #[test] fn parse_string_introducers() { mysql().verified_stmt("SELECT _binary 'abc'");