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]