This is an automated email from the ASF dual-hosted git repository.

iffyio pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-sqlparser-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 4fdf5e1b Fix MySQL parsing of GRANT, REVOKE, and CREATE VIEW (#1538)
4fdf5e1b is described below

commit 4fdf5e1b30820526d54bbcdc32d9d021e1947722
Author: Michael Victor Zink <[email protected]>
AuthorDate: Thu Jan 9 15:31:06 2025 -0800

    Fix MySQL parsing of GRANT, REVOKE, and CREATE VIEW (#1538)
---
 src/ast/mod.rs              | 138 +++++++++++++++++++++++++++---
 src/ast/spans.rs            |   1 +
 src/dialect/generic.rs      |   4 +
 src/dialect/mod.rs          |   5 ++
 src/dialect/mysql.rs        |   4 +
 src/keywords.rs             |   5 ++
 src/parser/mod.rs           | 168 ++++++++++++++++++++++++++++--------
 src/tokenizer.rs            |  66 +++++++++++++-
 tests/sqlparser_common.rs   |  58 +++++++++++--
 tests/sqlparser_mysql.rs    | 203 ++++++++++++++++++++++++++++++++++++++++++--
 tests/sqlparser_postgres.rs |   4 +-
 11 files changed, 589 insertions(+), 67 deletions(-)

diff --git a/src/ast/mod.rs b/src/ast/mod.rs
index 248bdcba..83c18267 100644
--- a/src/ast/mod.rs
+++ b/src/ast/mod.rs
@@ -2368,7 +2368,7 @@ pub enum Statement {
         identity: Option<TruncateIdentityOption>,
         /// Postgres-specific option
         /// [ CASCADE | RESTRICT ]
-        cascade: Option<TruncateCascadeOption>,
+        cascade: Option<CascadeOption>,
         /// ClickHouse-specific option
         /// [ ON CLUSTER cluster_name ]
         ///
@@ -2509,6 +2509,8 @@ pub enum Statement {
         /// if not None, has Clickhouse `TO` clause, specify the table into 
which to insert results
         /// 
<https://clickhouse.com/docs/en/sql-reference/statements/create/view#materialized-view>
         to: Option<ObjectName>,
+        /// MySQL: Optional parameters for the view algorithm, definer, and 
security context
+        params: Option<CreateViewParams>,
     },
     /// ```sql
     /// CREATE TABLE
@@ -3178,9 +3180,9 @@ pub enum Statement {
     Revoke {
         privileges: Privileges,
         objects: GrantObjects,
-        grantees: Vec<Ident>,
+        grantees: Vec<Grantee>,
         granted_by: Option<Ident>,
-        cascade: bool,
+        cascade: Option<CascadeOption>,
     },
     /// ```sql
     /// DEALLOCATE [ PREPARE ] { name | ALL }
@@ -3616,8 +3618,8 @@ impl fmt::Display for Statement {
                 }
                 if let Some(cascade) = cascade {
                     match cascade {
-                        TruncateCascadeOption::Cascade => write!(f, " 
CASCADE")?,
-                        TruncateCascadeOption::Restrict => write!(f, " 
RESTRICT")?,
+                        CascadeOption::Cascade => write!(f, " CASCADE")?,
+                        CascadeOption::Restrict => write!(f, " RESTRICT")?,
                     }
                 }
 
@@ -3946,11 +3948,19 @@ impl fmt::Display for Statement {
                 if_not_exists,
                 temporary,
                 to,
+                params,
             } => {
                 write!(
                     f,
-                    "CREATE {or_replace}{materialized}{temporary}VIEW 
{if_not_exists}{name}{to}",
+                    "CREATE {or_replace}",
                     or_replace = if *or_replace { "OR REPLACE " } else { "" },
+                )?;
+                if let Some(params) = params {
+                    params.fmt(f)?;
+                }
+                write!(
+                    f,
+                    "{materialized}{temporary}VIEW {if_not_exists}{name}{to}",
                     materialized = if *materialized { "MATERIALIZED " } else { 
"" },
                     name = name,
                     temporary = if *temporary { "TEMPORARY " } else { "" },
@@ -4701,7 +4711,9 @@ impl fmt::Display for Statement {
                 if let Some(grantor) = granted_by {
                     write!(f, " GRANTED BY {grantor}")?;
                 }
-                write!(f, " {}", if *cascade { "CASCADE" } else { "RESTRICT" 
})?;
+                if let Some(cascade) = cascade {
+                    write!(f, " {}", cascade)?;
+                }
                 Ok(())
             }
             Statement::Deallocate { name, prepare } => write!(
@@ -5103,16 +5115,25 @@ pub enum TruncateIdentityOption {
     Continue,
 }
 
-/// PostgreSQL cascade option for TRUNCATE table
+/// Cascade/restrict option for Postgres TRUNCATE table, MySQL GRANT/REVOKE, 
etc.
 /// [ CASCADE | RESTRICT ]
 #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
 #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
-pub enum TruncateCascadeOption {
+pub enum CascadeOption {
     Cascade,
     Restrict,
 }
 
+impl Display for CascadeOption {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            CascadeOption::Cascade => write!(f, "CASCADE"),
+            CascadeOption::Restrict => write!(f, "RESTRICT"),
+        }
+    }
+}
+
 /// Transaction started with [ TRANSACTION | WORK ]
 #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -5404,7 +5425,7 @@ impl fmt::Display for Action {
 #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
 pub struct Grantee {
     pub grantee_type: GranteesType,
-    pub name: Option<ObjectName>,
+    pub name: Option<GranteeName>,
 }
 
 impl fmt::Display for Grantee {
@@ -5437,7 +5458,7 @@ impl fmt::Display for Grantee {
             GranteesType::None => (),
         }
         if let Some(ref name) = self.name {
-            write!(f, "{}", name)?;
+            name.fmt(f)?;
         }
         Ok(())
     }
@@ -5458,6 +5479,28 @@ pub enum GranteesType {
     None,
 }
 
+/// Users/roles designated in a GRANT/REVOKE
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub enum GranteeName {
+    /// A bare identifier
+    ObjectName(ObjectName),
+    /// A MySQL user/host pair such as 'root'@'%'
+    UserHost { user: Ident, host: Ident },
+}
+
+impl fmt::Display for GranteeName {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            GranteeName::ObjectName(name) => name.fmt(f),
+            GranteeName::UserHost { user, host } => {
+                write!(f, "{}@{}", user, host)
+            }
+        }
+    }
+}
+
 /// Objects on which privileges are granted in a GRANT statement.
 #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@@ -7460,15 +7503,84 @@ pub enum MySQLColumnPosition {
 impl Display for MySQLColumnPosition {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         match self {
-            MySQLColumnPosition::First => Ok(write!(f, "FIRST")?),
+            MySQLColumnPosition::First => write!(f, "FIRST"),
             MySQLColumnPosition::After(ident) => {
                 let column_name = &ident.value;
-                Ok(write!(f, "AFTER {column_name}")?)
+                write!(f, "AFTER {column_name}")
             }
         }
     }
 }
 
+/// MySQL `CREATE VIEW` algorithm parameter: [ALGORITHM = {UNDEFINED | MERGE | 
TEMPTABLE}]
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub enum CreateViewAlgorithm {
+    Undefined,
+    Merge,
+    TempTable,
+}
+
+impl Display for CreateViewAlgorithm {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            CreateViewAlgorithm::Undefined => write!(f, "UNDEFINED"),
+            CreateViewAlgorithm::Merge => write!(f, "MERGE"),
+            CreateViewAlgorithm::TempTable => write!(f, "TEMPTABLE"),
+        }
+    }
+}
+/// MySQL `CREATE VIEW` security parameter: [SQL SECURITY { DEFINER | INVOKER 
}]
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub enum CreateViewSecurity {
+    Definer,
+    Invoker,
+}
+
+impl Display for CreateViewSecurity {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            CreateViewSecurity::Definer => write!(f, "DEFINER"),
+            CreateViewSecurity::Invoker => write!(f, "INVOKER"),
+        }
+    }
+}
+
+/// [MySQL] `CREATE VIEW` additional parameters
+///
+/// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/create-view.html
+#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
+pub struct CreateViewParams {
+    pub algorithm: Option<CreateViewAlgorithm>,
+    pub definer: Option<GranteeName>,
+    pub security: Option<CreateViewSecurity>,
+}
+
+impl Display for CreateViewParams {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let CreateViewParams {
+            algorithm,
+            definer,
+            security,
+        } = self;
+        if let Some(algorithm) = algorithm {
+            write!(f, "ALGORITHM = {algorithm} ")?;
+        }
+        if let Some(definers) = definer {
+            write!(f, "DEFINER = {definers} ")?;
+        }
+        if let Some(security) = security {
+            write!(f, "SQL SECURITY {security} ")?;
+        }
+        Ok(())
+    }
+}
+
 /// Engine of DB. Some warehouse has parameters of engine, e.g. [clickhouse]
 ///
 /// [clickhouse]: https://clickhouse.com/docs/en/engines/table-engines
diff --git a/src/ast/spans.rs b/src/ast/spans.rs
index 2ca65914..c61c584d 100644
--- a/src/ast/spans.rs
+++ b/src/ast/spans.rs
@@ -373,6 +373,7 @@ impl Spanned for Statement {
                 if_not_exists: _,
                 temporary: _,
                 to,
+                params: _,
             } => union_spans(
                 core::iter::once(name.span())
                     .chain(columns.iter().map(|i| i.span()))
diff --git a/src/dialect/generic.rs b/src/dialect/generic.rs
index e2a73de8..d696861b 100644
--- a/src/dialect/generic.rs
+++ b/src/dialect/generic.rs
@@ -135,4 +135,8 @@ impl Dialect for GenericDialect {
     fn supports_nested_comments(&self) -> bool {
         true
     }
+
+    fn supports_user_host_grantee(&self) -> bool {
+        true
+    }
 }
diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs
index 4c3f0b4b..4b1558ba 100644
--- a/src/dialect/mod.rs
+++ b/src/dialect/mod.rs
@@ -434,6 +434,11 @@ pub trait Dialect: Debug + Any {
         false
     }
 
+    /// Does the dialect support MySQL-style `'user'@'host'` grantee syntax?
+    fn supports_user_host_grantee(&self) -> bool {
+        false
+    }
+
     /// Dialect-specific infix parser override
     ///
     /// This method is called to parse the next infix expression.
diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs
index 3c3f2ee8..535b4298 100644
--- a/src/dialect/mysql.rs
+++ b/src/dialect/mysql.rs
@@ -107,6 +107,10 @@ impl Dialect for MySqlDialect {
     fn supports_insert_set(&self) -> bool {
         true
     }
+
+    fn supports_user_host_grantee(&self) -> bool {
+        true
+    }
 }
 
 /// `LOCK TABLES`
diff --git a/src/keywords.rs b/src/keywords.rs
index c50c2bd4..897e5b5c 100644
--- a/src/keywords.rs
+++ b/src/keywords.rs
@@ -84,6 +84,7 @@ define_keywords!(
     AFTER,
     AGAINST,
     AGGREGATION,
+    ALGORITHM,
     ALIAS,
     ALL,
     ALLOCATE,
@@ -248,6 +249,7 @@ define_keywords!(
     DEFERRED,
     DEFINE,
     DEFINED,
+    DEFINER,
     DELAYED,
     DELETE,
     DELIMITED,
@@ -423,6 +425,7 @@ define_keywords!(
     INTERSECTION,
     INTERVAL,
     INTO,
+    INVOKER,
     IS,
     ISODOW,
     ISOLATION,
@@ -780,6 +783,7 @@ define_keywords!(
     TBLPROPERTIES,
     TEMP,
     TEMPORARY,
+    TEMPTABLE,
     TERMINATED,
     TERSE,
     TEXT,
@@ -828,6 +832,7 @@ define_keywords!(
     UNBOUNDED,
     UNCACHE,
     UNCOMMITTED,
+    UNDEFINED,
     UNFREEZE,
     UNION,
     UNIQUE,
diff --git a/src/parser/mod.rs b/src/parser/mod.rs
index 3ffd78d0..70868335 100644
--- a/src/parser/mod.rs
+++ b/src/parser/mod.rs
@@ -805,13 +805,7 @@ impl<'a> Parser<'a> {
                 None
             };
 
-            cascade = if self.parse_keyword(Keyword::CASCADE) {
-                Some(TruncateCascadeOption::Cascade)
-            } else if self.parse_keyword(Keyword::RESTRICT) {
-                Some(TruncateCascadeOption::Restrict)
-            } else {
-                None
-            };
+            cascade = self.parse_cascade_option();
         };
 
         let on_cluster = self.parse_optional_on_cluster()?;
@@ -827,6 +821,16 @@ impl<'a> Parser<'a> {
         })
     }
 
+    fn parse_cascade_option(&mut self) -> Option<CascadeOption> {
+        if self.parse_keyword(Keyword::CASCADE) {
+            Some(CascadeOption::Cascade)
+        } else if self.parse_keyword(Keyword::RESTRICT) {
+            Some(CascadeOption::Restrict)
+        } else {
+            None
+        }
+    }
+
     pub fn parse_attach_duckdb_database_options(
         &mut self,
     ) -> Result<Vec<AttachDuckDBDatabaseOption>, ParserError> {
@@ -4147,11 +4151,12 @@ impl<'a> Parser<'a> {
             .is_some();
         let persistent = dialect_of!(self is DuckDbDialect)
             && self.parse_one_of_keywords(&[Keyword::PERSISTENT]).is_some();
+        let create_view_params = self.parse_create_view_params()?;
         if self.parse_keyword(Keyword::TABLE) {
             self.parse_create_table(or_replace, temporary, global, transient)
         } else if self.parse_keyword(Keyword::MATERIALIZED) || 
self.parse_keyword(Keyword::VIEW) {
             self.prev_token();
-            self.parse_create_view(or_replace, temporary)
+            self.parse_create_view(or_replace, temporary, create_view_params)
         } else if self.parse_keyword(Keyword::POLICY) {
             self.parse_create_policy()
         } else if self.parse_keyword(Keyword::EXTERNAL) {
@@ -5039,6 +5044,7 @@ impl<'a> Parser<'a> {
         &mut self,
         or_replace: bool,
         temporary: bool,
+        create_view_params: Option<CreateViewParams>,
     ) -> Result<Statement, ParserError> {
         let materialized = self.parse_keyword(Keyword::MATERIALIZED);
         self.expect_keyword_is(Keyword::VIEW)?;
@@ -5116,9 +5122,68 @@ impl<'a> Parser<'a> {
             if_not_exists,
             temporary,
             to,
+            params: create_view_params,
         })
     }
 
+    /// Parse optional parameters for the `CREATE VIEW` statement supported by 
[MySQL].
+    ///
+    /// [MySQL]: https://dev.mysql.com/doc/refman/9.1/en/create-view.html
+    fn parse_create_view_params(&mut self) -> Result<Option<CreateViewParams>, 
ParserError> {
+        let algorithm = if self.parse_keyword(Keyword::ALGORITHM) {
+            self.expect_token(&Token::Eq)?;
+            Some(
+                match self.expect_one_of_keywords(&[
+                    Keyword::UNDEFINED,
+                    Keyword::MERGE,
+                    Keyword::TEMPTABLE,
+                ])? {
+                    Keyword::UNDEFINED => CreateViewAlgorithm::Undefined,
+                    Keyword::MERGE => CreateViewAlgorithm::Merge,
+                    Keyword::TEMPTABLE => CreateViewAlgorithm::TempTable,
+                    _ => {
+                        self.prev_token();
+                        let found = self.next_token();
+                        return self
+                            .expected("UNDEFINED or MERGE or TEMPTABLE after 
ALGORITHM =", found);
+                    }
+                },
+            )
+        } else {
+            None
+        };
+        let definer = if self.parse_keyword(Keyword::DEFINER) {
+            self.expect_token(&Token::Eq)?;
+            Some(self.parse_grantee_name()?)
+        } else {
+            None
+        };
+        let security = if self.parse_keywords(&[Keyword::SQL, 
Keyword::SECURITY]) {
+            Some(
+                match self.expect_one_of_keywords(&[Keyword::DEFINER, 
Keyword::INVOKER])? {
+                    Keyword::DEFINER => CreateViewSecurity::Definer,
+                    Keyword::INVOKER => CreateViewSecurity::Invoker,
+                    _ => {
+                        self.prev_token();
+                        let found = self.next_token();
+                        return self.expected("DEFINER or INVOKER after SQL 
SECURITY", found);
+                    }
+                },
+            )
+        } else {
+            None
+        };
+        if algorithm.is_some() || definer.is_some() || security.is_some() {
+            Ok(Some(CreateViewParams {
+                algorithm,
+                definer,
+                security,
+            }))
+        } else {
+            Ok(None)
+        }
+    }
+
     pub fn parse_create_role(&mut self) -> Result<Statement, ParserError> {
         let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, 
Keyword::EXISTS]);
         let names = self.parse_comma_separated(|p| 
p.parse_object_name(false))?;
@@ -8872,13 +8937,13 @@ impl<'a> Parser<'a> {
         }
     }
 
-    /// Parse a possibly qualified, possibly quoted identifier, e.g.
-    /// `foo` or `myschema."table"
-    ///
-    /// The `in_table_clause` parameter indicates whether the object name is a 
table in a FROM, JOIN,
-    /// or similar table clause. Currently, this is used only to support 
unquoted hyphenated identifiers
-    /// in this context on BigQuery.
-    pub fn parse_object_name(&mut self, in_table_clause: bool) -> 
Result<ObjectName, ParserError> {
+    /// Parse a possibly qualified, possibly quoted identifier, optionally 
allowing for wildcards,
+    /// e.g. *, *.*, `foo`.*, or "foo"."bar"
+    fn parse_object_name_with_wildcards(
+        &mut self,
+        in_table_clause: bool,
+        allow_wildcards: bool,
+    ) -> Result<ObjectName, ParserError> {
         let mut idents = vec![];
 
         if dialect_of!(self is BigQueryDialect) && in_table_clause {
@@ -8891,19 +8956,41 @@ impl<'a> Parser<'a> {
             }
         } else {
             loop {
-                if self.dialect.supports_object_name_double_dot_notation()
-                    && idents.len() == 1
-                    && self.consume_token(&Token::Period)
-                {
-                    // Empty string here means default schema
-                    idents.push(Ident::new(""));
-                }
-                idents.push(self.parse_identifier()?);
+                let ident = if allow_wildcards && self.peek_token().token == 
Token::Mul {
+                    let span = self.next_token().span;
+                    Ident {
+                        value: Token::Mul.to_string(),
+                        quote_style: None,
+                        span,
+                    }
+                } else {
+                    if self.dialect.supports_object_name_double_dot_notation()
+                        && idents.len() == 1
+                        && self.consume_token(&Token::Period)
+                    {
+                        // Empty string here means default schema
+                        idents.push(Ident::new(""));
+                    }
+                    self.parse_identifier()?
+                };
+                idents.push(ident);
                 if !self.consume_token(&Token::Period) {
                     break;
                 }
             }
         }
+        Ok(ObjectName(idents))
+    }
+
+    /// Parse a possibly qualified, possibly quoted identifier, e.g.
+    /// `foo` or `myschema."table"
+    ///
+    /// The `in_table_clause` parameter indicates whether the object name is a 
table in a FROM, JOIN,
+    /// or similar table clause. Currently, this is used only to support 
unquoted hyphenated identifiers
+    /// in this context on BigQuery.
+    pub fn parse_object_name(&mut self, in_table_clause: bool) -> 
Result<ObjectName, ParserError> {
+        let ObjectName(mut idents) =
+            self.parse_object_name_with_wildcards(in_table_clause, false)?;
 
         // BigQuery accepts any number of quoted identifiers of a table name.
         // 
https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical#quoted_identifiers
@@ -11674,14 +11761,17 @@ impl<'a> Parser<'a> {
                     name: None,
                 }
             } else {
-                let mut name = self.parse_object_name(false)?;
+                let mut name = self.parse_grantee_name()?;
                 if self.consume_token(&Token::Colon) {
                     // Redshift supports namespace prefix for extenrnal users 
and groups:
                     // <Namespace>:<GroupName> or <Namespace>:<UserName>
                     // 
https://docs.aws.amazon.com/redshift/latest/mgmt/redshift-iam-access-control-native-idp.html
                     let ident = self.parse_identifier()?;
-                    if let Some(n) = name.0.first() {
-                        name = ObjectName(vec![Ident::new(format!("{}:{}", 
n.value, ident.value))]);
+                    if let GranteeName::ObjectName(namespace) = name {
+                        name = 
GranteeName::ObjectName(ObjectName(vec![Ident::new(format!(
+                            "{}:{}",
+                            namespace, ident
+                        ))]));
                     };
                 }
                 Grantee {
@@ -11764,7 +11854,8 @@ impl<'a> Parser<'a> {
         } else {
             let object_type =
                 self.parse_one_of_keywords(&[Keyword::SEQUENCE, 
Keyword::SCHEMA, Keyword::TABLE]);
-            let objects = self.parse_comma_separated(|p| 
p.parse_object_name(false));
+            let objects =
+                self.parse_comma_separated(|p| 
p.parse_object_name_with_wildcards(false, true));
             match object_type {
                 Some(Keyword::SCHEMA) => GrantObjects::Schemas(objects?),
                 Some(Keyword::SEQUENCE) => GrantObjects::Sequences(objects?),
@@ -11808,23 +11899,32 @@ impl<'a> Parser<'a> {
         }
     }
 
+    pub fn parse_grantee_name(&mut self) -> Result<GranteeName, ParserError> {
+        let mut name = self.parse_object_name(false)?;
+        if self.dialect.supports_user_host_grantee()
+            && name.0.len() == 1
+            && self.consume_token(&Token::AtSign)
+        {
+            let user = name.0.pop().unwrap();
+            let host = self.parse_identifier()?;
+            Ok(GranteeName::UserHost { user, host })
+        } else {
+            Ok(GranteeName::ObjectName(name))
+        }
+    }
+
     /// Parse a REVOKE statement
     pub fn parse_revoke(&mut self) -> Result<Statement, ParserError> {
         let (privileges, objects) = 
self.parse_grant_revoke_privileges_objects()?;
 
         self.expect_keyword_is(Keyword::FROM)?;
-        let grantees = self.parse_comma_separated(|p| p.parse_identifier())?;
+        let grantees = self.parse_grantees()?;
 
         let granted_by = self
             .parse_keywords(&[Keyword::GRANTED, Keyword::BY])
             .then(|| self.parse_identifier().unwrap());
 
-        let loc = self.peek_token().span.start;
-        let cascade = self.parse_keyword(Keyword::CASCADE);
-        let restrict = self.parse_keyword(Keyword::RESTRICT);
-        if cascade && restrict {
-            return parser_err!("Cannot specify both CASCADE and RESTRICT in 
REVOKE", loc);
-        }
+        let cascade = self.parse_cascade_option();
 
         Ok(Statement::Revoke {
             privileges,
diff --git a/src/tokenizer.rs b/src/tokenizer.rs
index b517ed66..15b13122 100644
--- a/src/tokenizer.rs
+++ b/src/tokenizer.rs
@@ -1432,6 +1432,18 @@ impl<'a> Tokenizer<'a> {
                             }
                         }
                         Some(' ') => Ok(Some(Token::AtSign)),
+                        // We break on quotes here, because no dialect allows 
identifiers starting
+                        // with @ and containing quotation marks (e.g. 
`@'foo'`) unless they are
+                        // quoted, which is tokenized as a quoted string, not 
here (e.g.
+                        // `"@'foo'"`). Further, at least two dialects parse 
`@` followed by a
+                        // quoted string as two separate tokens, which this 
allows. For example,
+                        // Postgres parses `@'1'` as the absolute value of '1' 
which is implicitly
+                        // cast to a numeric type. And when parsing 
MySQL-style grantees (e.g.
+                        // `GRANT ALL ON *.* to 'root'@'localhost'`), we also 
want separate tokens
+                        // for the user, the `@`, and the host.
+                        Some('\'') => Ok(Some(Token::AtSign)),
+                        Some('\"') => Ok(Some(Token::AtSign)),
+                        Some('`') => Ok(Some(Token::AtSign)),
                         Some(sch) if self.dialect.is_identifier_start('@') => {
                             self.tokenize_identifier_or_keyword([ch, *sch], 
chars)
                         }
@@ -1459,7 +1471,7 @@ impl<'a> Tokenizer<'a> {
                 }
                 '$' => Ok(Some(self.tokenize_dollar_preceded_value(chars)?)),
 
-                //whitespace check (including unicode chars) should be last as 
it covers some of the chars above
+                // whitespace check (including unicode chars) should be last 
as it covers some of the chars above
                 ch if ch.is_whitespace() => {
                     self.consume_and_return(chars, 
Token::Whitespace(Whitespace::Space))
                 }
@@ -3396,4 +3408,56 @@ mod tests {
         let expected = vec![Token::SingleQuotedString("''".to_string())];
         compare(expected, tokens);
     }
+
+    #[test]
+    fn test_mysql_users_grantees() {
+        let dialect = MySqlDialect {};
+
+        let sql = "CREATE USER `root`@`%`";
+        let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap();
+        let expected = vec![
+            Token::make_keyword("CREATE"),
+            Token::Whitespace(Whitespace::Space),
+            Token::make_keyword("USER"),
+            Token::Whitespace(Whitespace::Space),
+            Token::make_word("root", Some('`')),
+            Token::AtSign,
+            Token::make_word("%", Some('`')),
+        ];
+        compare(expected, tokens);
+    }
+
+    #[test]
+    fn test_postgres_abs_without_space_and_string_literal() {
+        let dialect = MySqlDialect {};
+
+        let sql = "SELECT @'1'";
+        let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap();
+        let expected = vec![
+            Token::make_keyword("SELECT"),
+            Token::Whitespace(Whitespace::Space),
+            Token::AtSign,
+            Token::SingleQuotedString("1".to_string()),
+        ];
+        compare(expected, tokens);
+    }
+
+    #[test]
+    fn test_postgres_abs_without_space_and_quoted_column() {
+        let dialect = MySqlDialect {};
+
+        let sql = r#"SELECT @"bar" FROM foo"#;
+        let tokens = Tokenizer::new(&dialect, sql).tokenize().unwrap();
+        let expected = vec![
+            Token::make_keyword("SELECT"),
+            Token::Whitespace(Whitespace::Space),
+            Token::AtSign,
+            Token::DoubleQuotedString("bar".to_string()),
+            Token::Whitespace(Whitespace::Space),
+            Token::make_keyword("FROM"),
+            Token::Whitespace(Whitespace::Space),
+            Token::make_word("foo", None),
+        ];
+        compare(expected, tokens);
+    }
 }
diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs
index 899194eb..df39c221 100644
--- a/tests/sqlparser_common.rs
+++ b/tests/sqlparser_common.rs
@@ -7192,6 +7192,7 @@ fn parse_create_view() {
             if_not_exists,
             temporary,
             to,
+            params,
         } => {
             assert_eq!("myschema.myview", name.to_string());
             assert_eq!(Vec::<ViewColumnDef>::new(), columns);
@@ -7204,7 +7205,8 @@ fn parse_create_view() {
             assert!(!late_binding);
             assert!(!if_not_exists);
             assert!(!temporary);
-            assert!(to.is_none())
+            assert!(to.is_none());
+            assert!(params.is_none());
         }
         _ => unreachable!(),
     }
@@ -7252,6 +7254,7 @@ fn parse_create_view_with_columns() {
             if_not_exists,
             temporary,
             to,
+            params,
         } => {
             assert_eq!("v", name.to_string());
             assert_eq!(
@@ -7274,7 +7277,8 @@ fn parse_create_view_with_columns() {
             assert!(!late_binding);
             assert!(!if_not_exists);
             assert!(!temporary);
-            assert!(to.is_none())
+            assert!(to.is_none());
+            assert!(params.is_none());
         }
         _ => unreachable!(),
     }
@@ -7297,6 +7301,7 @@ fn parse_create_view_temporary() {
             if_not_exists,
             temporary,
             to,
+            params,
         } => {
             assert_eq!("myschema.myview", name.to_string());
             assert_eq!(Vec::<ViewColumnDef>::new(), columns);
@@ -7309,7 +7314,8 @@ fn parse_create_view_temporary() {
             assert!(!late_binding);
             assert!(!if_not_exists);
             assert!(temporary);
-            assert!(to.is_none())
+            assert!(to.is_none());
+            assert!(params.is_none());
         }
         _ => unreachable!(),
     }
@@ -7332,6 +7338,7 @@ fn parse_create_or_replace_view() {
             if_not_exists,
             temporary,
             to,
+            params,
         } => {
             assert_eq!("v", name.to_string());
             assert_eq!(columns, vec![]);
@@ -7344,7 +7351,8 @@ fn parse_create_or_replace_view() {
             assert!(!late_binding);
             assert!(!if_not_exists);
             assert!(!temporary);
-            assert!(to.is_none())
+            assert!(to.is_none());
+            assert!(params.is_none());
         }
         _ => unreachable!(),
     }
@@ -7371,6 +7379,7 @@ fn parse_create_or_replace_materialized_view() {
             if_not_exists,
             temporary,
             to,
+            params,
         } => {
             assert_eq!("v", name.to_string());
             assert_eq!(columns, vec![]);
@@ -7383,7 +7392,8 @@ fn parse_create_or_replace_materialized_view() {
             assert!(!late_binding);
             assert!(!if_not_exists);
             assert!(!temporary);
-            assert!(to.is_none())
+            assert!(to.is_none());
+            assert!(params.is_none());
         }
         _ => unreachable!(),
     }
@@ -7406,6 +7416,7 @@ fn parse_create_materialized_view() {
             if_not_exists,
             temporary,
             to,
+            params,
         } => {
             assert_eq!("myschema.myview", name.to_string());
             assert_eq!(Vec::<ViewColumnDef>::new(), columns);
@@ -7418,7 +7429,8 @@ fn parse_create_materialized_view() {
             assert!(!late_binding);
             assert!(!if_not_exists);
             assert!(!temporary);
-            assert!(to.is_none())
+            assert!(to.is_none());
+            assert!(params.is_none());
         }
         _ => unreachable!(),
     }
@@ -7441,6 +7453,7 @@ fn parse_create_materialized_view_with_cluster_by() {
             if_not_exists,
             temporary,
             to,
+            params,
         } => {
             assert_eq!("myschema.myview", name.to_string());
             assert_eq!(Vec::<ViewColumnDef>::new(), columns);
@@ -7453,7 +7466,8 @@ fn parse_create_materialized_view_with_cluster_by() {
             assert!(!late_binding);
             assert!(!if_not_exists);
             assert!(!temporary);
-            assert!(to.is_none())
+            assert!(to.is_none());
+            assert!(params.is_none());
         }
         _ => unreachable!(),
     }
@@ -8574,14 +8588,40 @@ fn parse_grant() {
 
 #[test]
 fn test_revoke() {
-    let sql = "REVOKE ALL PRIVILEGES ON users, auth FROM analyst CASCADE";
+    let sql = "REVOKE ALL PRIVILEGES ON users, auth FROM analyst";
     match verified_stmt(sql) {
         Statement::Revoke {
             privileges,
             objects: GrantObjects::Tables(tables),
             grantees,
+            granted_by,
             cascade,
+        } => {
+            assert_eq!(
+                Privileges::All {
+                    with_privileges_keyword: true
+                },
+                privileges
+            );
+            assert_eq_vec(&["users", "auth"], &tables);
+            assert_eq_vec(&["analyst"], &grantees);
+            assert_eq!(cascade, None);
+            assert_eq!(None, granted_by);
+        }
+        _ => unreachable!(),
+    }
+}
+
+#[test]
+fn test_revoke_with_cascade() {
+    let sql = "REVOKE ALL PRIVILEGES ON users, auth FROM analyst CASCADE";
+    match all_dialects_except(|d| d.is::<MySqlDialect>()).verified_stmt(sql) {
+        Statement::Revoke {
+            privileges,
+            objects: GrantObjects::Tables(tables),
+            grantees,
             granted_by,
+            cascade,
         } => {
             assert_eq!(
                 Privileges::All {
@@ -8591,7 +8631,7 @@ fn test_revoke() {
             );
             assert_eq_vec(&["users", "auth"], &tables);
             assert_eq_vec(&["analyst"], &grantees);
-            assert!(cascade);
+            assert_eq!(cascade, Some(CascadeOption::Cascade));
             assert_eq!(None, granted_by);
         }
         _ => unreachable!(),
diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs
index 4a4e7961..62884afc 100644
--- a/tests/sqlparser_mysql.rs
+++ b/tests/sqlparser_mysql.rs
@@ -33,6 +33,14 @@ use test_utils::*;
 #[macro_use]
 mod test_utils;
 
+fn mysql() -> TestedDialects {
+    TestedDialects::new(vec![Box::new(MySqlDialect {})])
+}
+
+fn mysql_and_generic() -> TestedDialects {
+    TestedDialects::new(vec![Box::new(MySqlDialect {}), 
Box::new(GenericDialect {})])
+}
+
 #[test]
 fn parse_identifiers() {
     mysql().verified_stmt("SELECT $a$, àà");
@@ -2732,14 +2740,6 @@ fn 
parse_create_table_with_fulltext_definition_should_not_accept_constraint_name
     mysql_and_generic().verified_stmt("CREATE TABLE tb (c1 INT, CONSTRAINT 
cons FULLTEXT (c1))");
 }
 
-fn mysql() -> TestedDialects {
-    TestedDialects::new(vec![Box::new(MySqlDialect {})])
-}
-
-fn mysql_and_generic() -> TestedDialects {
-    TestedDialects::new(vec![Box::new(MySqlDialect {}), 
Box::new(GenericDialect {})])
-}
-
 #[test]
 fn parse_values() {
     mysql().verified_stmt("VALUES ROW(1, true, 'a')");
@@ -3001,6 +3001,193 @@ fn parse_bitstring_literal() {
     );
 }
 
+#[test]
+fn parse_grant() {
+    let sql = "GRANT ALL ON *.* TO 'jeffrey'@'%'";
+    let stmt = mysql().verified_stmt(sql);
+    if let Statement::Grant {
+        privileges,
+        objects,
+        grantees,
+        with_grant_option,
+        granted_by,
+    } = stmt
+    {
+        assert_eq!(
+            privileges,
+            Privileges::All {
+                with_privileges_keyword: false
+            }
+        );
+        assert_eq!(
+            objects,
+            GrantObjects::Tables(vec![ObjectName(vec!["*".into(), 
"*".into()])])
+        );
+        assert!(!with_grant_option);
+        assert!(granted_by.is_none());
+        if let [Grantee {
+            grantee_type: GranteesType::None,
+            name: Some(GranteeName::UserHost { user, host }),
+        }] = grantees.as_slice()
+        {
+            assert_eq!(user.value, "jeffrey");
+            assert_eq!(user.quote_style, Some('\''));
+            assert_eq!(host.value, "%");
+            assert_eq!(host.quote_style, Some('\''));
+        } else {
+            unreachable!()
+        }
+    } else {
+        unreachable!()
+    }
+}
+
+#[test]
+fn parse_revoke() {
+    let sql = "REVOKE ALL ON db1.* FROM 'jeffrey'@'%'";
+    let stmt = mysql_and_generic().verified_stmt(sql);
+    if let Statement::Revoke {
+        privileges,
+        objects,
+        grantees,
+        granted_by,
+        cascade,
+    } = stmt
+    {
+        assert_eq!(
+            privileges,
+            Privileges::All {
+                with_privileges_keyword: false
+            }
+        );
+        assert_eq!(
+            objects,
+            GrantObjects::Tables(vec![ObjectName(vec!["db1".into(), 
"*".into()])])
+        );
+        if let [Grantee {
+            grantee_type: GranteesType::None,
+            name: Some(GranteeName::UserHost { user, host }),
+        }] = grantees.as_slice()
+        {
+            assert_eq!(user.value, "jeffrey");
+            assert_eq!(user.quote_style, Some('\''));
+            assert_eq!(host.value, "%");
+            assert_eq!(host.quote_style, Some('\''));
+        } else {
+            unreachable!()
+        }
+        assert!(granted_by.is_none());
+        assert!(cascade.is_none());
+    } else {
+        unreachable!()
+    }
+}
+
+#[test]
+fn parse_create_view_algorithm_param() {
+    let sql = "CREATE ALGORITHM = MERGE VIEW foo AS SELECT 1";
+    let stmt = mysql().verified_stmt(sql);
+    if let Statement::CreateView {
+        params:
+            Some(CreateViewParams {
+                algorithm,
+                definer,
+                security,
+            }),
+        ..
+    } = stmt
+    {
+        assert_eq!(algorithm, Some(CreateViewAlgorithm::Merge));
+        assert!(definer.is_none());
+        assert!(security.is_none());
+    } else {
+        unreachable!()
+    }
+    mysql().verified_stmt("CREATE ALGORITHM = UNDEFINED VIEW foo AS SELECT 1");
+    mysql().verified_stmt("CREATE ALGORITHM = TEMPTABLE VIEW foo AS SELECT 1");
+}
+
+#[test]
+fn parse_create_view_definer_param() {
+    let sql = "CREATE DEFINER = 'jeffrey'@'localhost' VIEW foo AS SELECT 1";
+    let stmt = mysql().verified_stmt(sql);
+    if let Statement::CreateView {
+        params:
+            Some(CreateViewParams {
+                algorithm,
+                definer,
+                security,
+            }),
+        ..
+    } = stmt
+    {
+        assert!(algorithm.is_none());
+        if let Some(GranteeName::UserHost { user, host }) = definer {
+            assert_eq!(user.value, "jeffrey");
+            assert_eq!(user.quote_style, Some('\''));
+            assert_eq!(host.value, "localhost");
+            assert_eq!(host.quote_style, Some('\''));
+        } else {
+            unreachable!()
+        }
+        assert!(security.is_none());
+    } else {
+        unreachable!()
+    }
+}
+
+#[test]
+fn parse_create_view_security_param() {
+    let sql = "CREATE SQL SECURITY DEFINER VIEW foo AS SELECT 1";
+    let stmt = mysql().verified_stmt(sql);
+    if let Statement::CreateView {
+        params:
+            Some(CreateViewParams {
+                algorithm,
+                definer,
+                security,
+            }),
+        ..
+    } = stmt
+    {
+        assert!(algorithm.is_none());
+        assert!(definer.is_none());
+        assert_eq!(security, Some(CreateViewSecurity::Definer));
+    } else {
+        unreachable!()
+    }
+    mysql().verified_stmt("CREATE SQL SECURITY INVOKER VIEW foo AS SELECT 1");
+}
+
+#[test]
+fn parse_create_view_multiple_params() {
+    let sql = "CREATE ALGORITHM = UNDEFINED DEFINER = `root`@`%` SQL SECURITY 
INVOKER VIEW foo AS SELECT 1";
+    let stmt = mysql().verified_stmt(sql);
+    if let Statement::CreateView {
+        params:
+            Some(CreateViewParams {
+                algorithm,
+                definer,
+                security,
+            }),
+        ..
+    } = stmt
+    {
+        assert_eq!(algorithm, Some(CreateViewAlgorithm::Undefined));
+        if let Some(GranteeName::UserHost { user, host }) = definer {
+            assert_eq!(user.value, "root");
+            assert_eq!(user.quote_style, Some('`'));
+            assert_eq!(host.value, "%");
+            assert_eq!(host.quote_style, Some('`'));
+        } else {
+            unreachable!()
+        }
+        assert_eq!(security, Some(CreateViewSecurity::Invoker));
+    } else {
+        unreachable!()
+    }
+}
+
 #[test]
 fn parse_longblob_type() {
     let sql = "CREATE TABLE foo (bar LONGBLOB)";
diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs
index 68fc010c..6f6cf861 100644
--- a/tests/sqlparser_postgres.rs
+++ b/tests/sqlparser_postgres.rs
@@ -4171,7 +4171,7 @@ fn parse_truncate_with_options() {
             table: true,
             only: true,
             identity: Some(TruncateIdentityOption::Restart),
-            cascade: Some(TruncateCascadeOption::Cascade),
+            cascade: Some(CascadeOption::Cascade),
             on_cluster: None,
         },
         truncate
@@ -4203,7 +4203,7 @@ fn parse_truncate_with_table_list() {
             table: true,
             only: false,
             identity: Some(TruncateIdentityOption::Restart),
-            cascade: Some(TruncateCascadeOption::Cascade),
+            cascade: Some(CascadeOption::Cascade),
             on_cluster: None,
         },
         truncate


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to