Support CHARSET and ENGINE clauses on CREATE TABLE for mysql (#392)

* Add support for SET and ENUM types

See https://dev.mysql.com/doc/refman/8.0/en/set.html
and https://dev.mysql.com/doc/refman/8.0/en/enum.html

* Add support for mysql ENGINE and DEFAULT CHARSET in CREATE TABLE

See https://dev.mysql.com/doc/refman/8.0/en/create-table.html

* Add support for COMMENT and CHARACTER SET field attributes

See https://dev.mysql.com/doc/refman/8.0/en/create-table.html
This commit is contained in:
Jakob Truelsen 2022-02-08 16:08:56 +01:00 committed by GitHub
parent 34fedf311d
commit e4959696b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 187 additions and 4 deletions

View file

@ -11,7 +11,7 @@
// limitations under the License.
#[cfg(not(feature = "std"))]
use alloc::boxed::Box;
use alloc::{boxed::Box, string::String, vec::Vec};
use core::fmt;
#[cfg(feature = "serde")]
@ -19,6 +19,8 @@ use serde::{Deserialize, Serialize};
use crate::ast::ObjectName;
use super::value::escape_single_quote_string;
/// SQL data types
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@ -75,6 +77,10 @@ pub enum DataType {
Custom(ObjectName),
/// Arrays
Array(Box<DataType>),
/// Enums
Enum(Vec<String>),
/// Set
Set(Vec<String>),
}
impl fmt::Display for DataType {
@ -116,6 +122,26 @@ impl fmt::Display for DataType {
DataType::Bytea => write!(f, "BYTEA"),
DataType::Array(ty) => write!(f, "{}[]", ty),
DataType::Custom(ty) => write!(f, "{}", ty),
DataType::Enum(vals) => {
write!(f, "ENUM(")?;
for (i, v) in vals.iter().enumerate() {
if i != 0 {
write!(f, ", ")?;
}
write!(f, "'{}'", escape_single_quote_string(v))?;
}
write!(f, ")")
}
DataType::Set(vals) => {
write!(f, "SET(")?;
for (i, v) in vals.iter().enumerate() {
if i != 0 {
write!(f, ", ")?;
}
write!(f, "'{}'", escape_single_quote_string(v))?;
}
write!(f, ")")
}
}
}
}

View file

@ -14,12 +14,13 @@
//! (commonly referred to as Data Definition Language, or DDL)
#[cfg(not(feature = "std"))]
use alloc::{boxed::Box, string::ToString, vec::Vec};
use alloc::{boxed::Box, string::String, string::ToString, vec::Vec};
use core::fmt;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::ast::value::escape_single_quote_string;
use crate::ast::{display_comma_separated, display_separated, DataType, Expr, Ident, ObjectName};
use crate::tokenizer::Token;
@ -338,7 +339,9 @@ pub enum ColumnOption {
/// `DEFAULT <restricted-expr>`
Default(Expr),
/// `{ PRIMARY KEY | UNIQUE }`
Unique { is_primary: bool },
Unique {
is_primary: bool,
},
/// A referential integrity constraint (`[FOREIGN KEY REFERENCES
/// <foreign_table> (<referred_columns>)
/// { [ON DELETE <referential_action>] [ON UPDATE <referential_action>] |
@ -356,6 +359,8 @@ pub enum ColumnOption {
/// - MySQL's `AUTO_INCREMENT` or SQLite's `AUTOINCREMENT`
/// - ...
DialectSpecific(Vec<Token>),
CharacterSet(ObjectName),
Comment(String),
}
impl fmt::Display for ColumnOption {
@ -388,6 +393,8 @@ impl fmt::Display for ColumnOption {
}
Check(expr) => write!(f, "CHECK ({})", expr),
DialectSpecific(val) => write!(f, "{}", display_separated(val, " ")),
CharacterSet(n) => write!(f, "CHARACTER SET {}", n),
Comment(v) => write!(f, "COMMENT '{}'", escape_single_quote_string(v)),
}
}
}

View file

@ -734,6 +734,8 @@ pub enum Statement {
query: Option<Box<Query>>,
without_rowid: bool,
like: Option<ObjectName>,
engine: Option<String>,
default_charset: Option<String>,
},
/// SQLite's `CREATE VIRTUAL TABLE .. USING <module_name> (<module_args>)`
CreateVirtualTable {
@ -1182,6 +1184,8 @@ impl fmt::Display for Statement {
query,
without_rowid,
like,
default_charset,
engine,
} => {
// We want to allow the following options
// Empty column list, allowed by PostgreSQL:
@ -1307,6 +1311,12 @@ impl fmt::Display for Statement {
if let Some(query) = query {
write!(f, " AS {}", query)?;
}
if let Some(engine) = engine {
write!(f, " ENGINE={}", engine)?;
}
if let Some(default_charset) = default_charset {
write!(f, " DEFAULT CHARSET={}", default_charset)?;
}
Ok(())
}
Statement::CreateVirtualTable {

View file

@ -118,6 +118,7 @@ define_keywords!(
CHAR,
CHARACTER,
CHARACTER_LENGTH,
CHARSET,
CHAR_LENGTH,
CHECK,
CLOB,
@ -194,6 +195,8 @@ define_keywords!(
END_EXEC = "END-EXEC",
END_FRAME,
END_PARTITION,
ENGINE,
ENUM,
EQUALS,
ERROR,
ESCAPE,

View file

@ -1522,6 +1522,8 @@ impl<'a> Parser<'a> {
query: None,
without_rowid: false,
like: None,
default_charset: None,
engine: None,
})
}
@ -1696,6 +1698,26 @@ impl<'a> Parser<'a> {
None
};
let engine = if self.parse_keyword(Keyword::ENGINE) {
self.expect_token(&Token::Eq)?;
match self.next_token() {
Token::Word(w) => Some(w.value),
unexpected => self.expected("identifier", unexpected)?,
}
} else {
None
};
let default_charset = if self.parse_keywords(&[Keyword::DEFAULT, Keyword::CHARSET]) {
self.expect_token(&Token::Eq)?;
match self.next_token() {
Token::Word(w) => Some(w.value),
unexpected => self.expected("identifier", unexpected)?,
}
} else {
None
};
Ok(Statement::CreateTable {
name: table_name,
temporary,
@ -1713,6 +1735,8 @@ impl<'a> Parser<'a> {
query,
without_rowid,
like,
engine,
default_charset,
})
}
@ -1778,8 +1802,15 @@ impl<'a> Parser<'a> {
}
pub fn parse_optional_column_option(&mut self) -> Result<Option<ColumnOption>, ParserError> {
if self.parse_keywords(&[Keyword::NOT, Keyword::NULL]) {
if self.parse_keywords(&[Keyword::CHARACTER, Keyword::SET]) {
Ok(Some(ColumnOption::CharacterSet(self.parse_object_name()?)))
} else if self.parse_keywords(&[Keyword::NOT, Keyword::NULL]) {
Ok(Some(ColumnOption::NotNull))
} else if self.parse_keywords(&[Keyword::COMMENT]) {
match self.next_token() {
Token::SingleQuotedString(value, ..) => Ok(Some(ColumnOption::Comment(value))),
unexpected => self.expected("string", unexpected),
}
} else if self.parse_keyword(Keyword::NULL) {
Ok(Some(ColumnOption::Null))
} else if self.parse_keyword(Keyword::DEFAULT) {
@ -2301,6 +2332,8 @@ impl<'a> Parser<'a> {
let (precision, scale) = self.parse_optional_precision_scale()?;
Ok(DataType::Decimal(precision, scale))
}
Keyword::ENUM => Ok(DataType::Enum(self.parse_string_values()?)),
Keyword::SET => Ok(DataType::Set(self.parse_string_values()?)),
_ => {
self.prev_token();
let type_name = self.parse_object_name()?;
@ -2311,6 +2344,23 @@ impl<'a> Parser<'a> {
}
}
pub fn parse_string_values(&mut self) -> Result<Vec<String>, ParserError> {
self.expect_token(&Token::LParen)?;
let mut values = Vec::new();
loop {
match self.next_token() {
Token::SingleQuotedString(value) => values.push(value),
unexpected => self.expected("a string", unexpected)?,
}
match self.next_token() {
Token::Comma => (),
Token::RParen => break,
unexpected => self.expected(", or }", unexpected)?,
}
}
Ok(values)
}
/// Parse `AS identifier` (or simply `identifier` if it's not a reserved keyword)
/// Some examples with aliases: `SELECT 1 foo`, `SELECT COUNT(*) AS cnt`,
/// `SELECT ... FROM t1 foo, t2 bar`, `SELECT ... FROM (...) AS bar`

View file

@ -155,6 +155,93 @@ fn parse_create_table_auto_increment() {
}
}
#[test]
fn parse_create_table_set_enum() {
let sql = "CREATE TABLE foo (bar SET('a', 'b'), baz ENUM('a', 'b'))";
match mysql().verified_stmt(sql) {
Statement::CreateTable { name, columns, .. } => {
assert_eq!(name.to_string(), "foo");
assert_eq!(
vec![
ColumnDef {
name: Ident::new("bar"),
data_type: DataType::Set(vec!["a".to_string(), "b".to_string()]),
collation: None,
options: vec![],
},
ColumnDef {
name: Ident::new("baz"),
data_type: DataType::Enum(vec!["a".to_string(), "b".to_string()]),
collation: None,
options: vec![],
}
],
columns
);
}
_ => unreachable!(),
}
}
#[test]
fn parse_create_table_engine_default_charset() {
let sql = "CREATE TABLE foo (id INT(11)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3";
match mysql().verified_stmt(sql) {
Statement::CreateTable {
name,
columns,
engine,
default_charset,
..
} => {
assert_eq!(name.to_string(), "foo");
assert_eq!(
vec![ColumnDef {
name: Ident::new("id"),
data_type: DataType::Int(Some(11)),
collation: None,
options: vec![],
},],
columns
);
assert_eq!(engine, Some("InnoDB".to_string()));
assert_eq!(default_charset, Some("utf8mb3".to_string()));
}
_ => unreachable!(),
}
}
#[test]
fn parse_create_table_comment_character_set() {
let sql = "CREATE TABLE foo (s TEXT CHARACTER SET utf8mb4 COMMENT 'comment')";
match mysql().verified_stmt(sql) {
Statement::CreateTable { name, columns, .. } => {
assert_eq!(name.to_string(), "foo");
assert_eq!(
vec![ColumnDef {
name: Ident::new("s"),
data_type: DataType::Text,
collation: None,
options: vec![
ColumnOptionDef {
name: None,
option: ColumnOption::CharacterSet(ObjectName(vec![Ident::new(
"utf8mb4"
)]))
},
ColumnOptionDef {
name: None,
option: ColumnOption::Comment("comment".to_string())
}
],
},],
columns
);
}
_ => unreachable!(),
}
}
#[test]
fn parse_quote_identifiers() {
let sql = "CREATE TABLE `PRIMARY` (`BEGIN` INT PRIMARY KEY)";