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.
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 } ...`
StartTransaction { modes: Vec<TransactionMode> },
/// `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),
}
}
}

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> {
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)
}

View file

@ -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,

View file

@ -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 {

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()?),
"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<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> {
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 {})],
}
}