mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-08-03 13:58:15 +00:00
Support MySQL SHOW COLUMNS
statement
Co-authored-by: Nikhil Benesch <nikhil.benesch@gmail.com>
This commit is contained in:
parent
35a20091ea
commit
f64928e994
7 changed files with 248 additions and 3 deletions
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
33
src/dialect/mysql.rs
Normal 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')
|
||||
}
|
||||
}
|
|
@ -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
110
tests/sqlparser_mysql.rs
Normal 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 {})],
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue