Support MySQL SHOW COLUMNS statement

Co-authored-by: Nikhil Benesch <nikhil.benesch@gmail.com>
This commit is contained in:
Brandon W Maister 2019-07-31 11:21:12 -04:00 committed by Nikhil Benesch
parent 35a20091ea
commit f64928e994
No known key found for this signature in database
GPG key ID: FCF98542083C5A69
7 changed files with 248 additions and 3 deletions

View file

@ -440,6 +440,15 @@ pub enum Statement {
/// `RESTRICT` or no drop behavior at all was specified. /// `RESTRICT` or no drop behavior at all was specified.
cascade: bool, cascade: bool,
}, },
/// SHOW COLUMNS
///
/// Note: this is a MySQL-specific statement.
ShowColumns {
extended: bool,
full: bool,
table_name: ObjectName,
filter: Option<ShowStatementFilter>,
},
/// `{ BEGIN [ TRANSACTION | WORK ] | START TRANSACTION } ...` /// `{ BEGIN [ TRANSACTION | WORK ] | START TRANSACTION } ...`
StartTransaction { modes: Vec<TransactionMode> }, StartTransaction { modes: Vec<TransactionMode> },
/// `SET TRANSACTION ...` /// `SET TRANSACTION ...`
@ -451,6 +460,9 @@ pub enum Statement {
} }
impl fmt::Display for 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 { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
Statement::Query(s) => write!(f, "{}", s), Statement::Query(s) => write!(f, "{}", s),
@ -589,6 +601,25 @@ impl fmt::Display for Statement {
display_comma_separated(names), display_comma_separated(names),
if *cascade { " CASCADE" } else { "" }, 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 } => { Statement::StartTransaction { modes } => {
write!(f, "START TRANSACTION")?; write!(f, "START TRANSACTION")?;
if !modes.is_empty() { 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),
}
}
}

View file

@ -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> { impl<'a> fmt::Display for EscapeSingleQuoteString<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for c in self.0.chars() { for c in self.0.chars() {
@ -152,6 +153,7 @@ impl<'a> fmt::Display for EscapeSingleQuoteString<'a> {
Ok(()) Ok(())
} }
} }
fn escape_single_quote_string(s: &str) -> EscapeSingleQuoteString<'_> {
pub fn escape_single_quote_string(s: &str) -> EscapeSingleQuoteString<'_> {
EscapeSingleQuoteString(s) EscapeSingleQuoteString(s)
} }

View file

@ -102,6 +102,7 @@ define_keywords!(
COLLATE, COLLATE,
COLLECT, COLLECT,
COLUMN, COLUMN,
COLUMNS,
COMMIT, COMMIT,
COMMITTED, COMMITTED,
CONDITION, CONDITION,
@ -166,10 +167,12 @@ define_keywords!(
EXECUTE, EXECUTE,
EXISTS, EXISTS,
EXP, EXP,
EXTENDED,
EXTERNAL, EXTERNAL,
EXTRACT, EXTRACT,
FALSE, FALSE,
FETCH, FETCH,
FIELDS,
FIRST, FIRST,
FILTER, FILTER,
FIRST_VALUE, FIRST_VALUE,
@ -334,6 +337,7 @@ define_keywords!(
SERIALIZABLE, SERIALIZABLE,
SESSION_USER, SESSION_USER,
SET, SET,
SHOW,
SIMILAR, SIMILAR,
SMALLINT, SMALLINT,
SOME, SOME,

View file

@ -14,6 +14,7 @@ mod ansi;
mod generic; mod generic;
pub mod keywords; pub mod keywords;
mod mssql; mod mssql;
mod mysql;
mod postgresql; mod postgresql;
use std::fmt::Debug; use std::fmt::Debug;
@ -21,6 +22,7 @@ use std::fmt::Debug;
pub use self::ansi::AnsiDialect; pub use self::ansi::AnsiDialect;
pub use self::generic::GenericDialect; pub use self::generic::GenericDialect;
pub use self::mssql::MsSqlDialect; pub use self::mssql::MsSqlDialect;
pub use self::mysql::MySqlDialect;
pub use self::postgresql::PostgreSqlDialect; pub use self::postgresql::PostgreSqlDialect;
pub trait Dialect: Debug { pub trait Dialect: Debug {

33
src/dialect/mysql.rs Normal file
View file

@ -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')
}
}

View file

@ -125,6 +125,7 @@ impl Parser {
"UPDATE" => Ok(self.parse_update()?), "UPDATE" => Ok(self.parse_update()?),
"ALTER" => Ok(self.parse_alter()?), "ALTER" => Ok(self.parse_alter()?),
"COPY" => Ok(self.parse_copy()?), "COPY" => Ok(self.parse_copy()?),
"SHOW" => Ok(self.parse_show()?),
"START" => Ok(self.parse_start_transaction()?), "START" => Ok(self.parse_start_transaction()?),
"SET" => Ok(self.parse_set_transaction()?), "SET" => Ok(self.parse_set_transaction()?),
// `BEGIN` is a nonstandard but common alias for the // `BEGIN` is a nonstandard but common alias for the
@ -763,7 +764,11 @@ impl Parser {
#[must_use] #[must_use]
pub fn parse_one_of_keywords(&mut self, keywords: &[&'static str]) -> Option<&'static str> { pub fn parse_one_of_keywords(&mut self, keywords: &[&'static str]) -> Option<&'static str> {
for keyword in keywords { 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() { match self.peek_token() {
Some(Token::Word(ref k)) => keywords Some(Token::Word(ref k)) => keywords
@ -1588,6 +1593,48 @@ impl Parser {
}) })
} }
pub fn parse_show(&mut self) -> Result<Statement, ParserError> {
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<Statement, ParserError> {
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 <database> here. In other words, MySQL
// allows both FROM <table> FROM <database> and FROM <database>.<table>,
// 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<Option<ShowStatementFilter>, 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<TableWithJoins, ParserError> { pub fn parse_table_and_joins(&mut self) -> Result<TableWithJoins, ParserError> {
let relation = self.parse_table_factor()?; let relation = self.parse_table_factor()?;

110
tests/sqlparser_mysql.rs Normal file
View file

@ -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 {})],
}
}