diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 030ebba9..5388852c 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -440,6 +440,15 @@ pub enum Statement { /// `RESTRICT` or no drop behavior at all was specified. cascade: bool, }, + /// SHOW COLUMNS + /// + /// Note: this is a MySQL-specific statement. + ShowColumns { + extended: bool, + full: bool, + table_name: ObjectName, + filter: Option, + }, /// `{ BEGIN [ TRANSACTION | WORK ] | START TRANSACTION } ...` StartTransaction { modes: Vec }, /// `SET TRANSACTION ...` @@ -451,6 +460,9 @@ pub enum Statement { } impl fmt::Display for Statement { + // Clippy thinks this function is too complicated, but it is painful to + // split up without extracting structs for each `Statement` variant. + #[allow(clippy::cognitive_complexity)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Statement::Query(s) => write!(f, "{}", s), @@ -589,6 +601,25 @@ impl fmt::Display for Statement { display_comma_separated(names), if *cascade { " CASCADE" } else { "" }, ), + Statement::ShowColumns { + extended, + full, + table_name, + filter, + } => { + f.write_str("SHOW ")?; + if *extended { + f.write_str("EXTENDED ")?; + } + if *full { + f.write_str("FULL ")?; + } + write!(f, "COLUMNS FROM {}", table_name)?; + if let Some(filter) = filter { + write!(f, " {}", filter)?; + } + Ok(()) + } Statement::StartTransaction { modes } => { write!(f, "START TRANSACTION")?; if !modes.is_empty() { @@ -780,3 +811,19 @@ impl fmt::Display for TransactionIsolationLevel { }) } } + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ShowStatementFilter { + Like(String), + Where(Expr), +} + +impl fmt::Display for ShowStatementFilter { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use ShowStatementFilter::*; + match self { + Like(pattern) => write!(f, "LIKE '{}'", value::escape_single_quote_string(pattern)), + Where(expr) => write!(f, "WHERE {}", expr), + } + } +} diff --git a/src/ast/value.rs b/src/ast/value.rs index b406dfe4..ad170437 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -139,7 +139,8 @@ impl fmt::Display for DateTimeField { } } -struct EscapeSingleQuoteString<'a>(&'a str); +pub struct EscapeSingleQuoteString<'a>(&'a str); + impl<'a> fmt::Display for EscapeSingleQuoteString<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for c in self.0.chars() { @@ -152,6 +153,7 @@ impl<'a> fmt::Display for EscapeSingleQuoteString<'a> { Ok(()) } } -fn escape_single_quote_string(s: &str) -> EscapeSingleQuoteString<'_> { + +pub fn escape_single_quote_string(s: &str) -> EscapeSingleQuoteString<'_> { EscapeSingleQuoteString(s) } diff --git a/src/dialect/keywords.rs b/src/dialect/keywords.rs index 3048fcd5..916d31e3 100644 --- a/src/dialect/keywords.rs +++ b/src/dialect/keywords.rs @@ -102,6 +102,7 @@ define_keywords!( COLLATE, COLLECT, COLUMN, + COLUMNS, COMMIT, COMMITTED, CONDITION, @@ -166,10 +167,12 @@ define_keywords!( EXECUTE, EXISTS, EXP, + EXTENDED, EXTERNAL, EXTRACT, FALSE, FETCH, + FIELDS, FIRST, FILTER, FIRST_VALUE, @@ -334,6 +337,7 @@ define_keywords!( SERIALIZABLE, SESSION_USER, SET, + SHOW, SIMILAR, SMALLINT, SOME, diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index f9959669..c9ddbedd 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -14,6 +14,7 @@ mod ansi; mod generic; pub mod keywords; mod mssql; +mod mysql; mod postgresql; use std::fmt::Debug; @@ -21,6 +22,7 @@ use std::fmt::Debug; pub use self::ansi::AnsiDialect; pub use self::generic::GenericDialect; pub use self::mssql::MsSqlDialect; +pub use self::mysql::MySqlDialect; pub use self::postgresql::PostgreSqlDialect; pub trait Dialect: Debug { diff --git a/src/dialect/mysql.rs b/src/dialect/mysql.rs new file mode 100644 index 00000000..e0b4e21c --- /dev/null +++ b/src/dialect/mysql.rs @@ -0,0 +1,33 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::dialect::Dialect; + +#[derive(Debug)] +pub struct MySqlDialect {} + +impl Dialect for MySqlDialect { + fn is_identifier_start(&self, ch: char) -> bool { + // See https://dev.mysql.com/doc/refman/8.0/en/identifiers.html. + // We don't yet support identifiers beginning with numbers, as that + // makes it hard to distinguish numeric literals. + (ch >= 'a' && ch <= 'z') + || (ch >= 'A' && ch <= 'Z') + || ch == '_' + || ch == '$' + || (ch >= '\u{0080}' && ch <= '\u{ffff}') + } + + fn is_identifier_part(&self, ch: char) -> bool { + self.is_identifier_start(ch) || (ch >= '0' && ch <= '9') + } +} diff --git a/src/parser.rs b/src/parser.rs index 2bfb8687..affa8a63 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -125,6 +125,7 @@ impl Parser { "UPDATE" => Ok(self.parse_update()?), "ALTER" => Ok(self.parse_alter()?), "COPY" => Ok(self.parse_copy()?), + "SHOW" => Ok(self.parse_show()?), "START" => Ok(self.parse_start_transaction()?), "SET" => Ok(self.parse_set_transaction()?), // `BEGIN` is a nonstandard but common alias for the @@ -763,7 +764,11 @@ impl Parser { #[must_use] pub fn parse_one_of_keywords(&mut self, keywords: &[&'static str]) -> Option<&'static str> { for keyword in keywords { - assert!(keywords::ALL_KEYWORDS.contains(keyword)); + assert!( + keywords::ALL_KEYWORDS.contains(keyword), + "{} is not contained in keyword list", + keyword + ); } match self.peek_token() { Some(Token::Word(ref k)) => keywords @@ -1588,6 +1593,48 @@ impl Parser { }) } + pub fn parse_show(&mut self) -> Result { + if self + .parse_one_of_keywords(&["EXTENDED", "FULL", "COLUMNS", "FIELDS"]) + .is_some() + { + self.prev_token(); + self.parse_show_columns() + } else { + self.expected("EXTENDED, FULL, COLUMNS, or FIELDS", self.peek_token()) + } + } + + fn parse_show_columns(&mut self) -> Result { + let extended = self.parse_keyword("EXTENDED"); + let full = self.parse_keyword("FULL"); + self.expect_one_of_keywords(&["COLUMNS", "FIELDS"])?; + self.expect_one_of_keywords(&["FROM", "IN"])?; + let table_name = self.parse_object_name()?; + // MySQL also supports FROM here. In other words, MySQL + // allows both FROM FROM and FROM .
, + // while we only support the latter for now. + let filter = self.parse_show_statement_filter()?; + Ok(Statement::ShowColumns { + extended, + full, + table_name, + filter, + }) + } + + fn parse_show_statement_filter(&mut self) -> Result, ParserError> { + if self.parse_keyword("LIKE") { + Ok(Some(ShowStatementFilter::Like( + self.parse_literal_string()?, + ))) + } else if self.parse_keyword("WHERE") { + Ok(Some(ShowStatementFilter::Where(self.parse_expr()?))) + } else { + Ok(None) + } + } + pub fn parse_table_and_joins(&mut self) -> Result { let relation = self.parse_table_factor()?; diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs new file mode 100644 index 00000000..a16a8be8 --- /dev/null +++ b/tests/sqlparser_mysql.rs @@ -0,0 +1,110 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![warn(clippy::all)] + +//! Test SQL syntax specific to MySQL. The parser based on the generic dialect +//! is also tested (on the inputs it can handle). + +use sqlparser::ast::*; +use sqlparser::dialect::{GenericDialect, MySqlDialect}; +use sqlparser::test_utils::*; + +#[test] +fn parse_identifiers() { + mysql().verified_stmt("SELECT $a$, àà"); +} + +#[test] +fn parse_show_columns() { + let table_name = ObjectName(vec!["mytable".to_string()]); + assert_eq!( + mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mytable"), + Statement::ShowColumns { + extended: false, + full: false, + table_name: table_name.clone(), + filter: None, + } + ); + assert_eq!( + mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mydb.mytable"), + Statement::ShowColumns { + extended: false, + full: false, + table_name: ObjectName(vec!["mydb".to_string(), "mytable".to_string()]), + filter: None, + } + ); + assert_eq!( + mysql_and_generic().verified_stmt("SHOW EXTENDED COLUMNS FROM mytable"), + Statement::ShowColumns { + extended: true, + full: false, + table_name: table_name.clone(), + filter: None, + } + ); + assert_eq!( + mysql_and_generic().verified_stmt("SHOW FULL COLUMNS FROM mytable"), + Statement::ShowColumns { + extended: false, + full: true, + table_name: table_name.clone(), + filter: None, + } + ); + assert_eq!( + mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mytable LIKE 'pattern'"), + Statement::ShowColumns { + extended: false, + full: false, + table_name: table_name.clone(), + filter: Some(ShowStatementFilter::Like("pattern".into())), + } + ); + assert_eq!( + mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mytable WHERE 1 = 2"), + Statement::ShowColumns { + extended: false, + full: false, + table_name: table_name.clone(), + filter: Some(ShowStatementFilter::Where( + mysql_and_generic().verified_expr("1 = 2") + )), + } + ); + mysql_and_generic() + .one_statement_parses_to("SHOW FIELDS FROM mytable", "SHOW COLUMNS FROM mytable"); + mysql_and_generic() + .one_statement_parses_to("SHOW COLUMNS IN mytable", "SHOW COLUMNS FROM mytable"); + mysql_and_generic() + .one_statement_parses_to("SHOW FIELDS IN mytable", "SHOW COLUMNS FROM mytable"); + + // unhandled things are truly unhandled + match mysql_and_generic().parse_sql_statements("SHOW COLUMNS FROM mytable FROM mydb") { + Err(_) => {} + Ok(val) => panic!("unexpected successful parse: {:?}", val), + } +} + +fn mysql() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(MySqlDialect {})], + } +} + +fn mysql_and_generic() -> TestedDialects { + TestedDialects { + dialects: vec![Box::new(MySqlDialect {}), Box::new(GenericDialect {})], + } +}