diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index 96071c228..a0accdf53 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -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), + /// Enums + Enum(Vec), + /// Set + Set(Vec), } 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, ")") + } } } } diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 78ec2e0c7..9fcf78d1c 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -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 ` Default(Expr), /// `{ PRIMARY KEY | UNIQUE }` - Unique { is_primary: bool }, + Unique { + is_primary: bool, + }, /// A referential integrity constraint (`[FOREIGN KEY REFERENCES /// () /// { [ON DELETE ] [ON UPDATE ] | @@ -356,6 +359,8 @@ pub enum ColumnOption { /// - MySQL's `AUTO_INCREMENT` or SQLite's `AUTOINCREMENT` /// - ... DialectSpecific(Vec), + 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)), } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 477a22890..1fccf0a04 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -717,6 +717,8 @@ pub enum Statement { query: Option>, without_rowid: bool, like: Option, + engine: Option, + default_charset: Option, }, /// SQLite's `CREATE VIRTUAL TABLE .. USING ()` CreateVirtualTable { @@ -1147,6 +1149,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: @@ -1272,6 +1276,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 { diff --git a/src/keywords.rs b/src/keywords.rs index 56f77cc23..06cd0b52c 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -118,6 +118,7 @@ define_keywords!( CHAR, CHARACTER, CHARACTER_LENGTH, + CHARSET, CHAR_LENGTH, CHECK, CLOB, @@ -193,6 +194,8 @@ define_keywords!( END_EXEC = "END-EXEC", END_FRAME, END_PARTITION, + ENGINE, + ENUM, EQUALS, ERROR, ESCAPE, diff --git a/src/parser.rs b/src/parser.rs index 6d917f027..8c5ea2faf 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1505,6 +1505,8 @@ impl<'a> Parser<'a> { query: None, without_rowid: false, like: None, + default_charset: None, + engine: None, }) } @@ -1679,6 +1681,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, @@ -1696,6 +1718,8 @@ impl<'a> Parser<'a> { query, without_rowid, like, + engine, + default_charset, }) } @@ -1761,8 +1785,15 @@ impl<'a> Parser<'a> { } pub fn parse_optional_column_option(&mut self) -> Result, 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) { @@ -2253,6 +2284,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()?; @@ -2263,6 +2296,23 @@ impl<'a> Parser<'a> { } } + pub fn parse_string_values(&mut self) -> Result, 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` diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index f67d05c34..197a07102 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -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)";