Skip to content

Commit f64928e

Browse files
quodlibetorbenesch
andcommitted
Support MySQL SHOW COLUMNS statement
Co-authored-by: Nikhil Benesch <[email protected]>
1 parent 35a2009 commit f64928e

File tree

7 files changed

+248
-3
lines changed

7 files changed

+248
-3
lines changed

src/ast/mod.rs

+47
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,15 @@ pub enum Statement {
440440
/// `RESTRICT` or no drop behavior at all was specified.
441441
cascade: bool,
442442
},
443+
/// SHOW COLUMNS
444+
///
445+
/// Note: this is a MySQL-specific statement.
446+
ShowColumns {
447+
extended: bool,
448+
full: bool,
449+
table_name: ObjectName,
450+
filter: Option<ShowStatementFilter>,
451+
},
443452
/// `{ BEGIN [ TRANSACTION | WORK ] | START TRANSACTION } ...`
444453
StartTransaction { modes: Vec<TransactionMode> },
445454
/// `SET TRANSACTION ...`
@@ -451,6 +460,9 @@ pub enum Statement {
451460
}
452461

453462
impl fmt::Display for Statement {
463+
// Clippy thinks this function is too complicated, but it is painful to
464+
// split up without extracting structs for each `Statement` variant.
465+
#[allow(clippy::cognitive_complexity)]
454466
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
455467
match self {
456468
Statement::Query(s) => write!(f, "{}", s),
@@ -589,6 +601,25 @@ impl fmt::Display for Statement {
589601
display_comma_separated(names),
590602
if *cascade { " CASCADE" } else { "" },
591603
),
604+
Statement::ShowColumns {
605+
extended,
606+
full,
607+
table_name,
608+
filter,
609+
} => {
610+
f.write_str("SHOW ")?;
611+
if *extended {
612+
f.write_str("EXTENDED ")?;
613+
}
614+
if *full {
615+
f.write_str("FULL ")?;
616+
}
617+
write!(f, "COLUMNS FROM {}", table_name)?;
618+
if let Some(filter) = filter {
619+
write!(f, " {}", filter)?;
620+
}
621+
Ok(())
622+
}
592623
Statement::StartTransaction { modes } => {
593624
write!(f, "START TRANSACTION")?;
594625
if !modes.is_empty() {
@@ -780,3 +811,19 @@ impl fmt::Display for TransactionIsolationLevel {
780811
})
781812
}
782813
}
814+
815+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
816+
pub enum ShowStatementFilter {
817+
Like(String),
818+
Where(Expr),
819+
}
820+
821+
impl fmt::Display for ShowStatementFilter {
822+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
823+
use ShowStatementFilter::*;
824+
match self {
825+
Like(pattern) => write!(f, "LIKE '{}'", value::escape_single_quote_string(pattern)),
826+
Where(expr) => write!(f, "WHERE {}", expr),
827+
}
828+
}
829+
}

src/ast/value.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ impl fmt::Display for DateTimeField {
139139
}
140140
}
141141

142-
struct EscapeSingleQuoteString<'a>(&'a str);
142+
pub struct EscapeSingleQuoteString<'a>(&'a str);
143+
143144
impl<'a> fmt::Display for EscapeSingleQuoteString<'a> {
144145
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
145146
for c in self.0.chars() {
@@ -152,6 +153,7 @@ impl<'a> fmt::Display for EscapeSingleQuoteString<'a> {
152153
Ok(())
153154
}
154155
}
155-
fn escape_single_quote_string(s: &str) -> EscapeSingleQuoteString<'_> {
156+
157+
pub fn escape_single_quote_string(s: &str) -> EscapeSingleQuoteString<'_> {
156158
EscapeSingleQuoteString(s)
157159
}

src/dialect/keywords.rs

+4
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ define_keywords!(
102102
COLLATE,
103103
COLLECT,
104104
COLUMN,
105+
COLUMNS,
105106
COMMIT,
106107
COMMITTED,
107108
CONDITION,
@@ -166,10 +167,12 @@ define_keywords!(
166167
EXECUTE,
167168
EXISTS,
168169
EXP,
170+
EXTENDED,
169171
EXTERNAL,
170172
EXTRACT,
171173
FALSE,
172174
FETCH,
175+
FIELDS,
173176
FIRST,
174177
FILTER,
175178
FIRST_VALUE,
@@ -334,6 +337,7 @@ define_keywords!(
334337
SERIALIZABLE,
335338
SESSION_USER,
336339
SET,
340+
SHOW,
337341
SIMILAR,
338342
SMALLINT,
339343
SOME,

src/dialect/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ mod ansi;
1414
mod generic;
1515
pub mod keywords;
1616
mod mssql;
17+
mod mysql;
1718
mod postgresql;
1819

1920
use std::fmt::Debug;
2021

2122
pub use self::ansi::AnsiDialect;
2223
pub use self::generic::GenericDialect;
2324
pub use self::mssql::MsSqlDialect;
25+
pub use self::mysql::MySqlDialect;
2426
pub use self::postgresql::PostgreSqlDialect;
2527

2628
pub trait Dialect: Debug {

src/dialect/mysql.rs

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License");
2+
// you may not use this file except in compliance with the License.
3+
// You may obtain a copy of the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
13+
use crate::dialect::Dialect;
14+
15+
#[derive(Debug)]
16+
pub struct MySqlDialect {}
17+
18+
impl Dialect for MySqlDialect {
19+
fn is_identifier_start(&self, ch: char) -> bool {
20+
// See https://dev.mysql.com/doc/refman/8.0/en/identifiers.html.
21+
// We don't yet support identifiers beginning with numbers, as that
22+
// makes it hard to distinguish numeric literals.
23+
(ch >= 'a' && ch <= 'z')
24+
|| (ch >= 'A' && ch <= 'Z')
25+
|| ch == '_'
26+
|| ch == '$'
27+
|| (ch >= '\u{0080}' && ch <= '\u{ffff}')
28+
}
29+
30+
fn is_identifier_part(&self, ch: char) -> bool {
31+
self.is_identifier_start(ch) || (ch >= '0' && ch <= '9')
32+
}
33+
}

src/parser.rs

+48-1
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ impl Parser {
125125
"UPDATE" => Ok(self.parse_update()?),
126126
"ALTER" => Ok(self.parse_alter()?),
127127
"COPY" => Ok(self.parse_copy()?),
128+
"SHOW" => Ok(self.parse_show()?),
128129
"START" => Ok(self.parse_start_transaction()?),
129130
"SET" => Ok(self.parse_set_transaction()?),
130131
// `BEGIN` is a nonstandard but common alias for the
@@ -763,7 +764,11 @@ impl Parser {
763764
#[must_use]
764765
pub fn parse_one_of_keywords(&mut self, keywords: &[&'static str]) -> Option<&'static str> {
765766
for keyword in keywords {
766-
assert!(keywords::ALL_KEYWORDS.contains(keyword));
767+
assert!(
768+
keywords::ALL_KEYWORDS.contains(keyword),
769+
"{} is not contained in keyword list",
770+
keyword
771+
);
767772
}
768773
match self.peek_token() {
769774
Some(Token::Word(ref k)) => keywords
@@ -1588,6 +1593,48 @@ impl Parser {
15881593
})
15891594
}
15901595

1596+
pub fn parse_show(&mut self) -> Result<Statement, ParserError> {
1597+
if self
1598+
.parse_one_of_keywords(&["EXTENDED", "FULL", "COLUMNS", "FIELDS"])
1599+
.is_some()
1600+
{
1601+
self.prev_token();
1602+
self.parse_show_columns()
1603+
} else {
1604+
self.expected("EXTENDED, FULL, COLUMNS, or FIELDS", self.peek_token())
1605+
}
1606+
}
1607+
1608+
fn parse_show_columns(&mut self) -> Result<Statement, ParserError> {
1609+
let extended = self.parse_keyword("EXTENDED");
1610+
let full = self.parse_keyword("FULL");
1611+
self.expect_one_of_keywords(&["COLUMNS", "FIELDS"])?;
1612+
self.expect_one_of_keywords(&["FROM", "IN"])?;
1613+
let table_name = self.parse_object_name()?;
1614+
// MySQL also supports FROM <database> here. In other words, MySQL
1615+
// allows both FROM <table> FROM <database> and FROM <database>.<table>,
1616+
// while we only support the latter for now.
1617+
let filter = self.parse_show_statement_filter()?;
1618+
Ok(Statement::ShowColumns {
1619+
extended,
1620+
full,
1621+
table_name,
1622+
filter,
1623+
})
1624+
}
1625+
1626+
fn parse_show_statement_filter(&mut self) -> Result<Option<ShowStatementFilter>, ParserError> {
1627+
if self.parse_keyword("LIKE") {
1628+
Ok(Some(ShowStatementFilter::Like(
1629+
self.parse_literal_string()?,
1630+
)))
1631+
} else if self.parse_keyword("WHERE") {
1632+
Ok(Some(ShowStatementFilter::Where(self.parse_expr()?)))
1633+
} else {
1634+
Ok(None)
1635+
}
1636+
}
1637+
15911638
pub fn parse_table_and_joins(&mut self) -> Result<TableWithJoins, ParserError> {
15921639
let relation = self.parse_table_factor()?;
15931640

tests/sqlparser_mysql.rs

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License");
2+
// you may not use this file except in compliance with the License.
3+
// You may obtain a copy of the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS,
9+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
// See the License for the specific language governing permissions and
11+
// limitations under the License.
12+
13+
#![warn(clippy::all)]
14+
15+
//! Test SQL syntax specific to MySQL. The parser based on the generic dialect
16+
//! is also tested (on the inputs it can handle).
17+
18+
use sqlparser::ast::*;
19+
use sqlparser::dialect::{GenericDialect, MySqlDialect};
20+
use sqlparser::test_utils::*;
21+
22+
#[test]
23+
fn parse_identifiers() {
24+
mysql().verified_stmt("SELECT $a$, àà");
25+
}
26+
27+
#[test]
28+
fn parse_show_columns() {
29+
let table_name = ObjectName(vec!["mytable".to_string()]);
30+
assert_eq!(
31+
mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mytable"),
32+
Statement::ShowColumns {
33+
extended: false,
34+
full: false,
35+
table_name: table_name.clone(),
36+
filter: None,
37+
}
38+
);
39+
assert_eq!(
40+
mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mydb.mytable"),
41+
Statement::ShowColumns {
42+
extended: false,
43+
full: false,
44+
table_name: ObjectName(vec!["mydb".to_string(), "mytable".to_string()]),
45+
filter: None,
46+
}
47+
);
48+
assert_eq!(
49+
mysql_and_generic().verified_stmt("SHOW EXTENDED COLUMNS FROM mytable"),
50+
Statement::ShowColumns {
51+
extended: true,
52+
full: false,
53+
table_name: table_name.clone(),
54+
filter: None,
55+
}
56+
);
57+
assert_eq!(
58+
mysql_and_generic().verified_stmt("SHOW FULL COLUMNS FROM mytable"),
59+
Statement::ShowColumns {
60+
extended: false,
61+
full: true,
62+
table_name: table_name.clone(),
63+
filter: None,
64+
}
65+
);
66+
assert_eq!(
67+
mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mytable LIKE 'pattern'"),
68+
Statement::ShowColumns {
69+
extended: false,
70+
full: false,
71+
table_name: table_name.clone(),
72+
filter: Some(ShowStatementFilter::Like("pattern".into())),
73+
}
74+
);
75+
assert_eq!(
76+
mysql_and_generic().verified_stmt("SHOW COLUMNS FROM mytable WHERE 1 = 2"),
77+
Statement::ShowColumns {
78+
extended: false,
79+
full: false,
80+
table_name: table_name.clone(),
81+
filter: Some(ShowStatementFilter::Where(
82+
mysql_and_generic().verified_expr("1 = 2")
83+
)),
84+
}
85+
);
86+
mysql_and_generic()
87+
.one_statement_parses_to("SHOW FIELDS FROM mytable", "SHOW COLUMNS FROM mytable");
88+
mysql_and_generic()
89+
.one_statement_parses_to("SHOW COLUMNS IN mytable", "SHOW COLUMNS FROM mytable");
90+
mysql_and_generic()
91+
.one_statement_parses_to("SHOW FIELDS IN mytable", "SHOW COLUMNS FROM mytable");
92+
93+
// unhandled things are truly unhandled
94+
match mysql_and_generic().parse_sql_statements("SHOW COLUMNS FROM mytable FROM mydb") {
95+
Err(_) => {}
96+
Ok(val) => panic!("unexpected successful parse: {:?}", val),
97+
}
98+
}
99+
100+
fn mysql() -> TestedDialects {
101+
TestedDialects {
102+
dialects: vec![Box::new(MySqlDialect {})],
103+
}
104+
}
105+
106+
fn mysql_and_generic() -> TestedDialects {
107+
TestedDialects {
108+
dialects: vec![Box::new(MySqlDialect {}), Box::new(GenericDialect {})],
109+
}
110+
}

0 commit comments

Comments
 (0)