diff --git a/CHANGELOG.md b/CHANGELOG.md index f63a963cb..6ae31adf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,24 @@ Given that the parser produces a typed AST, any changes to the AST will technica ## [Unreleased] Check https://github.com/sqlparser-rs/sqlparser-rs/commits/main for undocumented changes. + +## [0.26.0] 2022-10-19 + +### Added +* Support MySQL table option `{INDEX | KEY}` in CREATE TABLE definiton (#665) - Thanks @AugustoFKL +* Support `CREATE [ { TEMPORARY | TEMP } ] SEQUENCE [ IF NOT EXISTS ] ` (#678) - Thanks @sam-mmm +* Support `DROP SEQUENCE` statement (#673) - Thanks @sam-mmm +* Support for ANSI types `CHARACTER LARGE OBJECT[(p)]` and `CHAR LARGE OBJECT[(p)]` (#671) - Thanks @AugustoFKL +* Support `[CACHE|UNCACHE] TABLE` (#670) - Thanks @francis-du +* Support `CEIL(expr TO DateTimeField)` and `FLOOR(expr TO DateTimeField)` - Thanks @sarahyurick +* Support all ansii character string types, (#648) - Thanks @AugustoFKL + +### Changed +* Support expressions inside window frames (#655) - Thanks @mustafasrepo and @ozankabak +* Support unit on char length units for small character strings (#663) - Thanks @AugustoFKL +* Replace booleans on `SET ROLE` with a single enum. (#664) - Thanks @AugustoFKL +* Replace `Option`s with enum for `DECIMAL` precision (#654) - Thanks @AugustoFKL + ## [0.25.0] 2022-10-03 ### Added diff --git a/Cargo.toml b/Cargo.toml index 60fe478c8..b5b587a6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlparser" description = "Extensible SQL Lexer and Parser with support for ANSI SQL:2011" -version = "0.25.0" +version = "0.26.0" authors = ["Andy Grove "] homepage = "https://github.com/sqlparser-rs/sqlparser-rs" documentation = "https://docs.rs/sqlparser/" diff --git a/src/ast/data_type.rs b/src/ast/data_type.rs index baa23acf2..a66761167 100644 --- a/src/ast/data_type.rs +++ b/src/ast/data_type.rs @@ -39,7 +39,15 @@ pub enum DataType { Nvarchar(Option), /// Uuid type Uuid, - /// Large character object with optional length e.g. CLOB, CLOB(1000), [standard], [Oracle] + /// Large character object with optional length e.g. CHARACTER LARGE OBJECT, CHARACTER LARGE OBJECT(1000), [standard] + /// + /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type + CharacterLargeObject(Option), + /// Large character object with optional length e.g. CHAR LARGE OBJECT, CHAR LARGE OBJECT(1000), [standard] + /// + /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type + CharLargeObject(Option), + /// Large character object with optional length e.g. CLOB, CLOB(1000), [standard] /// /// [standard]: https://jakewheat.github.io/sql-overview/sql-2016-foundation-grammar.html#character-large-object-type /// [Oracle]: https://docs.oracle.com/javadb/10.10.1.2/ref/rrefclob.html @@ -121,7 +129,7 @@ pub enum DataType { /// Bytea Bytea, /// Custom type such as enums - Custom(ObjectName), + Custom(ObjectName, Vec), /// Arrays Array(Box), /// Enums @@ -145,6 +153,12 @@ impl fmt::Display for DataType { format_type_with_optional_length(f, "NVARCHAR", size, false) } DataType::Uuid => write!(f, "UUID"), + DataType::CharacterLargeObject(size) => { + format_type_with_optional_length(f, "CHARACTER LARGE OBJECT", size, false) + } + DataType::CharLargeObject(size) => { + format_type_with_optional_length(f, "CHAR LARGE OBJECT", size, false) + } DataType::Clob(size) => format_type_with_optional_length(f, "CLOB", size, false), DataType::Binary(size) => format_type_with_optional_length(f, "BINARY", size, false), DataType::Varbinary(size) => { @@ -203,7 +217,13 @@ impl fmt::Display for DataType { DataType::String => write!(f, "STRING"), DataType::Bytea => write!(f, "BYTEA"), DataType::Array(ty) => write!(f, "{}[]", ty), - DataType::Custom(ty) => write!(f, "{}", ty), + DataType::Custom(ty, modifiers) => { + if modifiers.is_empty() { + write!(f, "{}", ty) + } else { + write!(f, "{}({})", ty, modifiers.join(", ")) + } + } DataType::Enum(vals) => { write!(f, "ENUM(")?; for (i, v) in vals.iter().enumerate() { diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index 1847f2518..cae0f597b 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -247,6 +247,24 @@ pub enum TableConstraint { name: Option, expr: Box, }, + /// MySQLs [index definition][1] for index creation. Not present on ANSI so, for now, the usage + /// is restricted to MySQL, as no other dialects that support this syntax were found. + /// + /// `{INDEX | KEY} [index_name] [index_type] (key_part,...) [index_option]...` + /// + /// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html + Index { + /// Whether this index starts with KEY (true) or INDEX (false), to maintain the same syntax. + display_as_key: bool, + /// Index name. + name: Option, + /// Optional [index type][1]. + /// + /// [1]: IndexType + index_type: Option, + /// Referred column identifier list. + columns: Vec, + }, } impl fmt::Display for TableConstraint { @@ -290,6 +308,48 @@ impl fmt::Display for TableConstraint { TableConstraint::Check { name, expr } => { write!(f, "{}CHECK ({})", display_constraint_name(name), expr) } + TableConstraint::Index { + display_as_key, + name, + index_type, + columns, + } => { + write!(f, "{}", if *display_as_key { "KEY" } else { "INDEX" })?; + if let Some(name) = name { + write!(f, " {}", name)?; + } + if let Some(index_type) = index_type { + write!(f, " USING {}", index_type)?; + } + write!(f, " ({})", display_comma_separated(columns))?; + + Ok(()) + } + } + } +} + +/// Indexing method used by that index. +/// +/// This structure isn't present on ANSI, but is found at least in [MySQL CREATE TABLE][1], +/// [MySQL CREATE INDEX][2], and [Postgresql CREATE INDEX][3] statements. +/// +/// [1]: https://dev.mysql.com/doc/refman/8.0/en/create-table.html +/// [2]: https://dev.mysql.com/doc/refman/8.0/en/create-index.html +/// [3]: https://www.postgresql.org/docs/14/sql-createindex.html +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum IndexType { + BTree, + Hash, + // TODO add Postgresql's possible indexes +} + +impl fmt::Display for IndexType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::BTree => write!(f, "BTREE"), + Self::Hash => write!(f, "HASH"), } } } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 019622433..c83ead544 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -26,7 +26,7 @@ pub use self::data_type::{ CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, }; pub use self::ddl::{ - AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, + AlterColumnOperation, AlterTableOperation, ColumnDef, ColumnOption, ColumnOptionDef, IndexType, ReferentialAction, TableConstraint, }; pub use self::operator::{BinaryOperator, UnaryOperator}; @@ -906,9 +906,9 @@ pub enum WindowFrameBound { /// `CURRENT ROW` CurrentRow, /// ` PRECEDING` or `UNBOUNDED PRECEDING` - Preceding(Option), + Preceding(Option>), /// ` FOLLOWING` or `UNBOUNDED FOLLOWING`. - Following(Option), + Following(Option>), } impl fmt::Display for WindowFrameBound { @@ -1184,6 +1184,9 @@ pub enum Statement { /// Whether `CASCADE` was specified. This will be `false` when /// `RESTRICT` or no drop behavior at all was specified. cascade: bool, + /// Whether `RESTRICT` was specified. This will be `false` when + /// `CASCADE` or no drop behavior at all was specified. + restrict: bool, /// Hive allows you specify whether the table's stored data will be /// deleted along with the dropped table purge: bool, @@ -1430,6 +1433,32 @@ pub enum Statement { // Specifies the actions to perform when values match or do not match. clauses: Vec, }, + /// CACHE [ FLAG ] TABLE [ OPTIONS('K1' = 'V1', 'K2' = V2) ] [ AS ] [ ] + /// Based on Spark SQL,see + Cache { + // Table flag + table_flag: Option, + // Table name + table_name: ObjectName, + has_as: bool, + // Table confs + options: Vec, + // Cache table as a Query + query: Option, + }, + /// UNCACHE TABLE [ IF EXISTS ] + UNCache { + // Table name + table_name: ObjectName, + if_exists: bool, + }, + ///CreateSequence -- define a new sequence + /// CREATE [ { TEMPORARY | TEMP } ] SEQUENCE [ IF NOT EXISTS ] + CreateSequence { + temporary: bool, + if_not_exists: bool, + name: ObjectName, + }, } impl fmt::Display for Statement { @@ -2124,14 +2153,16 @@ impl fmt::Display for Statement { if_exists, names, cascade, + restrict, purge, } => write!( f, - "DROP {}{} {}{}{}", + "DROP {}{} {}{}{}{}", object_type, if *if_exists { " IF EXISTS" } else { "" }, display_comma_separated(names), if *cascade { " CASCADE" } else { "" }, + if *restrict { " RESTRICT" } else { "" }, if *purge { " PURGE" } else { "" } ), Statement::Discard { object_type } => { @@ -2397,6 +2428,66 @@ impl fmt::Display for Statement { write!(f, "ON {} ", on)?; write!(f, "{}", display_separated(clauses, " ")) } + Statement::Cache { + table_name, + table_flag, + has_as, + options, + query, + } => { + if table_flag.is_some() { + write!( + f, + "CACHE {table_flag} TABLE {table_name}", + table_flag = table_flag.clone().unwrap(), + table_name = table_name, + )?; + } else { + write!(f, "CACHE TABLE {table_name}", table_name = table_name,)?; + } + + if !options.is_empty() { + write!(f, " OPTIONS({})", display_comma_separated(options))?; + } + + let has_query = query.is_some(); + if *has_as && has_query { + write!(f, " AS {query}", query = query.clone().unwrap()) + } else if !has_as && has_query { + write!(f, " {query}", query = query.clone().unwrap()) + } else if *has_as && !has_query { + write!(f, " AS") + } else { + Ok(()) + } + } + Statement::UNCache { + table_name, + if_exists, + } => { + if *if_exists { + write!( + f, + "UNCACHE TABLE IF EXISTS {table_name}", + table_name = table_name + ) + } else { + write!(f, "UNCACHE TABLE {table_name}", table_name = table_name) + } + } + Statement::CreateSequence { + temporary, + if_not_exists, + name, + } => { + write!( + f, + "CREATE {temporary}SEQUENCE {if_not_exists}{name}", + if_not_exists = if *if_not_exists { "IF NOT EXISTS " } else { "" }, + temporary = if *temporary { "TEMPORARY " } else { "" }, + name = name + ) + } } } } @@ -2844,6 +2935,7 @@ pub enum ObjectType { Index, Schema, Role, + Sequence, } impl fmt::Display for ObjectType { @@ -2854,6 +2946,7 @@ impl fmt::Display for ObjectType { ObjectType::Index => "INDEX", ObjectType::Schema => "SCHEMA", ObjectType::Role => "ROLE", + ObjectType::Sequence => "SEQUENCE", }) } } diff --git a/src/keywords.rs b/src/keywords.rs index 0b6b06b33..e29ebdfdb 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -105,6 +105,7 @@ define_keywords!( BLOB, BOOLEAN, BOTH, + BTREE, BY, BYPASSRLS, BYTEA, @@ -265,6 +266,7 @@ define_keywords!( GROUP, GROUPING, GROUPS, + HASH, HAVING, HEADER, HIVEVAR, @@ -383,6 +385,7 @@ define_keywords!( OPEN, OPERATOR, OPTION, + OPTIONS, OR, ORC, ORDER, @@ -554,6 +557,7 @@ define_keywords!( TYPE, UESCAPE, UNBOUNDED, + UNCACHE, UNCOMMITTED, UNION, UNIQUE, diff --git a/src/parser.rs b/src/parser.rs index 0c0bd9c5a..108427889 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -170,12 +170,14 @@ impl<'a> Parser<'a> { Keyword::TRUNCATE => Ok(self.parse_truncate()?), Keyword::MSCK => Ok(self.parse_msck()?), Keyword::CREATE => Ok(self.parse_create()?), + Keyword::CACHE => Ok(self.parse_cache_table()?), Keyword::DROP => Ok(self.parse_drop()?), Keyword::DISCARD => Ok(self.parse_discard()?), Keyword::DECLARE => Ok(self.parse_declare()?), Keyword::FETCH => Ok(self.parse_fetch_statement()?), Keyword::DELETE => Ok(self.parse_delete()?), Keyword::INSERT => Ok(self.parse_insert()?), + Keyword::UNCACHE => Ok(self.parse_uncache_table()?), Keyword::UPDATE => Ok(self.parse_update()?), Keyword::ALTER => Ok(self.parse_alter()?), Keyword::COPY => Ok(self.parse_copy()?), @@ -623,7 +625,6 @@ impl<'a> Parser<'a> { } else { None }; - Ok(Expr::Function(Function { name, args, @@ -685,7 +686,10 @@ impl<'a> Parser<'a> { let rows = if self.parse_keyword(Keyword::UNBOUNDED) { None } else { - Some(self.parse_literal_uint()?) + Some(Box::new(match self.peek_token() { + Token::SingleQuotedString(_) => self.parse_interval()?, + _ => self.parse_expr()?, + })) }; if self.parse_keyword(Keyword::PRECEDING) { Ok(WindowFrameBound::Preceding(rows)) @@ -1900,11 +1904,122 @@ impl<'a> Parser<'a> { self.parse_create_function(temporary) } else if self.parse_keyword(Keyword::ROLE) { self.parse_create_role() + } else if self.parse_keyword(Keyword::SEQUENCE) { + self.parse_create_sequence(temporary) } else { self.expected("an object type after CREATE", self.peek_token()) } } + /// Parse a CACHE TABLE statement + pub fn parse_cache_table(&mut self) -> Result { + let (mut table_flag, mut options, mut has_as, mut query) = (None, vec![], false, None); + if self.parse_keyword(Keyword::TABLE) { + let table_name = self.parse_object_name()?; + if self.peek_token() != Token::EOF { + if let Token::Word(word) = self.peek_token() { + if word.keyword == Keyword::OPTIONS { + options = self.parse_options(Keyword::OPTIONS)? + } + }; + + if self.peek_token() != Token::EOF { + let (a, q) = self.parse_as_query()?; + has_as = a; + query = Some(q); + } + + Ok(Statement::Cache { + table_flag, + table_name, + has_as, + options, + query, + }) + } else { + Ok(Statement::Cache { + table_flag, + table_name, + has_as, + options, + query, + }) + } + } else { + table_flag = Some(self.parse_object_name()?); + if self.parse_keyword(Keyword::TABLE) { + let table_name = self.parse_object_name()?; + if self.peek_token() != Token::EOF { + if let Token::Word(word) = self.peek_token() { + if word.keyword == Keyword::OPTIONS { + options = self.parse_options(Keyword::OPTIONS)? + } + }; + + if self.peek_token() != Token::EOF { + let (a, q) = self.parse_as_query()?; + has_as = a; + query = Some(q); + } + + Ok(Statement::Cache { + table_flag, + table_name, + has_as, + options, + query, + }) + } else { + Ok(Statement::Cache { + table_flag, + table_name, + has_as, + options, + query, + }) + } + } else { + if self.peek_token() == Token::EOF { + self.prev_token(); + } + self.expected("a `TABLE` keyword", self.peek_token()) + } + } + } + + /// Parse 'AS' before as query,such as `WITH XXX AS SELECT XXX` oer `CACHE TABLE AS SELECT XXX` + pub fn parse_as_query(&mut self) -> Result<(bool, Query), ParserError> { + match self.peek_token() { + Token::Word(word) => match word.keyword { + Keyword::AS => { + self.next_token(); + Ok((true, self.parse_query()?)) + } + _ => Ok((false, self.parse_query()?)), + }, + _ => self.expected("a QUERY statement", self.peek_token()), + } + } + + /// Parse a UNCACHE TABLE statement + pub fn parse_uncache_table(&mut self) -> Result { + let has_table = self.parse_keyword(Keyword::TABLE); + if has_table { + let if_exists = self.parse_keywords(&[Keyword::IF, Keyword::EXISTS]); + let table_name = self.parse_object_name()?; + if self.peek_token() == Token::EOF { + Ok(Statement::UNCache { + table_name, + if_exists, + }) + } else { + self.expected("an `EOF`", self.peek_token()) + } + } else { + self.expected("a `TABLE` keyword", self.peek_token()) + } + } + /// SQLite-specific `CREATE VIRTUAL TABLE` pub fn parse_create_virtual_table(&mut self) -> Result { self.expect_keyword(Keyword::TABLE)?; @@ -2328,9 +2443,11 @@ impl<'a> Parser<'a> { ObjectType::Role } else if self.parse_keyword(Keyword::SCHEMA) { ObjectType::Schema + } else if self.parse_keyword(Keyword::SEQUENCE) { + ObjectType::Sequence } else { return self.expected( - "TABLE, VIEW, INDEX, ROLE, or SCHEMA after DROP", + "TABLE, VIEW, INDEX, ROLE, SCHEMA, or SEQUENCE after DROP", self.peek_token(), ); }; @@ -2352,6 +2469,7 @@ impl<'a> Parser<'a> { if_exists, names, cascade, + restrict, purge, }) } @@ -2885,6 +3003,31 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; Ok(Some(TableConstraint::Check { name, expr })) } + Token::Word(w) + if (w.keyword == Keyword::INDEX || w.keyword == Keyword::KEY) + && dialect_of!(self is GenericDialect | MySqlDialect) => + { + let display_as_key = w.keyword == Keyword::KEY; + + let name = match self.peek_token() { + Token::Word(word) if word.keyword == Keyword::USING => None, + _ => self.maybe_parse(|parser| parser.parse_identifier()), + }; + + let index_type = if self.parse_keyword(Keyword::USING) { + Some(self.parse_index_type()?) + } else { + None + }; + let columns = self.parse_parenthesized_column_list(Mandatory)?; + + Ok(Some(TableConstraint::Index { + display_as_key, + name, + index_type, + columns, + })) + } unexpected => { if name.is_some() { self.expected("PRIMARY, UNIQUE, FOREIGN, or CHECK", unexpected) @@ -2907,6 +3050,16 @@ impl<'a> Parser<'a> { } } + pub fn parse_index_type(&mut self) -> Result { + if self.parse_keyword(Keyword::BTREE) { + Ok(IndexType::BTree) + } else if self.parse_keyword(Keyword::HASH) { + Ok(IndexType::Hash) + } else { + self.expected("index type {BTREE | HASH}", self.peek_token()) + } + } + pub fn parse_sql_option(&mut self) -> Result { let name = self.parse_identifier()?; self.expect_token(&Token::Eq)?; @@ -3433,6 +3586,10 @@ impl<'a> Parser<'a> { Ok(DataType::CharacterVarying( self.parse_optional_character_length()?, )) + } else if self.parse_keywords(&[Keyword::LARGE, Keyword::OBJECT]) { + Ok(DataType::CharacterLargeObject( + self.parse_optional_precision()?, + )) } else { Ok(DataType::Character(self.parse_optional_character_length()?)) } @@ -3442,6 +3599,8 @@ impl<'a> Parser<'a> { Ok(DataType::CharVarying( self.parse_optional_character_length()?, )) + } else if self.parse_keywords(&[Keyword::LARGE, Keyword::OBJECT]) { + Ok(DataType::CharLargeObject(self.parse_optional_precision()?)) } else { Ok(DataType::Char(self.parse_optional_character_length()?)) } @@ -3502,7 +3661,11 @@ impl<'a> Parser<'a> { _ => { self.prev_token(); let type_name = self.parse_object_name()?; - Ok(DataType::Custom(type_name)) + if let Some(modifiers) = self.parse_optional_type_modifiers()? { + Ok(DataType::Custom(type_name, modifiers)) + } else { + Ok(DataType::Custom(type_name, vec![])) + } } }, unexpected => self.expected("a data type name", unexpected), @@ -3748,6 +3911,31 @@ impl<'a> Parser<'a> { } } + pub fn parse_optional_type_modifiers(&mut self) -> Result>, ParserError> { + if self.consume_token(&Token::LParen) { + let mut modifiers = Vec::new(); + loop { + match self.next_token() { + Token::Word(w) => modifiers.push(w.to_string()), + Token::Number(n, _) => modifiers.push(n), + Token::SingleQuotedString(s) => modifiers.push(s), + + Token::Comma => { + continue; + } + Token::RParen => { + break; + } + unexpected => self.expected("type modifiers", unexpected)?, + } + } + + Ok(Some(modifiers)) + } else { + Ok(None) + } + } + pub fn parse_delete(&mut self) -> Result { self.expect_keyword(Keyword::FROM)?; let table_name = self.parse_table_factor()?; @@ -5292,6 +5480,20 @@ impl<'a> Parser<'a> { clauses, }) } + + /// https://www.postgresql.org/docs/current/sql-createsequence.html + /// CREATE [ { TEMPORARY | TEMP } ] SEQUENCE [ IF NOT EXISTS ] + pub fn parse_create_sequence(&mut self, temporary: bool) -> Result { + //[ IF NOT EXISTS ] + let if_not_exists = self.parse_keywords(&[Keyword::IF, Keyword::NOT, Keyword::EXISTS]); + //name + let name = self.parse_object_name()?; + Ok(Statement::CreateSequence { + temporary, + if_not_exists, + name, + }) + } } impl Word { @@ -5367,7 +5569,7 @@ mod tests { #[cfg(test)] mod test_parse_data_type { use crate::ast::{ - CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, TimezoneInfo, + CharLengthUnits, CharacterLength, DataType, ExactNumberInfo, ObjectName, TimezoneInfo, }; use crate::dialect::{AnsiDialect, GenericDialect}; use crate::test_utils::TestedDialects; @@ -5511,6 +5713,69 @@ mod tests { ); } + #[test] + fn test_ansii_character_large_object_types() { + // Character large object types: + let dialect = TestedDialects { + dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], + }; + + test_parse_data_type!( + dialect, + "CHARACTER LARGE OBJECT", + DataType::CharacterLargeObject(None) + ); + test_parse_data_type!( + dialect, + "CHARACTER LARGE OBJECT(20)", + DataType::CharacterLargeObject(Some(20)) + ); + + test_parse_data_type!( + dialect, + "CHAR LARGE OBJECT", + DataType::CharLargeObject(None) + ); + test_parse_data_type!( + dialect, + "CHAR LARGE OBJECT(20)", + DataType::CharLargeObject(Some(20)) + ); + + test_parse_data_type!(dialect, "CLOB", DataType::Clob(None)); + test_parse_data_type!(dialect, "CLOB(20)", DataType::Clob(Some(20))); + } + + #[test] + fn test_parse_custom_types() { + let dialect = TestedDialects { + dialects: vec![Box::new(GenericDialect {}), Box::new(AnsiDialect {})], + }; + test_parse_data_type!( + dialect, + "GEOMETRY", + DataType::Custom(ObjectName(vec!["GEOMETRY".into()]), vec![]) + ); + + test_parse_data_type!( + dialect, + "GEOMETRY(POINT)", + DataType::Custom( + ObjectName(vec!["GEOMETRY".into()]), + vec!["POINT".to_string()] + ) + ); + + test_parse_data_type!( + dialect, + "GEOMETRY(POINT, 4326)", + DataType::Custom( + ObjectName(vec!["GEOMETRY".into()]), + vec!["POINT".to_string(), "4326".to_string()] + ) + ); + } + #[test] fn test_ansii_exact_numeric_types() { // Exact numeric types: @@ -5608,4 +5873,100 @@ mod tests { SchemaName::NamedAuthorization(dummy_name.clone(), dummy_authorization.clone()), ); } + + #[test] + fn mysql_parse_index_table_constraint() { + macro_rules! test_parse_table_constraint { + ($dialect:expr, $input:expr, $expected:expr $(,)?) => {{ + $dialect.run_parser_method(&*$input, |parser| { + let constraint = parser.parse_optional_table_constraint().unwrap().unwrap(); + // Validate that the structure is the same as expected + assert_eq!(constraint, $expected); + // Validate that the input and the expected structure serialization are the same + assert_eq!(constraint.to_string(), $input.to_string()); + }); + }}; + } + + let dialect = TestedDialects { + dialects: vec![Box::new(GenericDialect {}), Box::new(MySqlDialect {})], + }; + + test_parse_table_constraint!( + dialect, + "INDEX (c1)", + TableConstraint::Index { + display_as_key: false, + name: None, + index_type: None, + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "KEY (c1)", + TableConstraint::Index { + display_as_key: true, + name: None, + index_type: None, + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX 'index' (c1, c2)", + TableConstraint::Index { + display_as_key: false, + name: Some(Ident::with_quote('\'', "index")), + index_type: None, + columns: vec![Ident::new("c1"), Ident::new("c2")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX USING BTREE (c1)", + TableConstraint::Index { + display_as_key: false, + name: None, + index_type: Some(IndexType::BTree), + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX USING HASH (c1)", + TableConstraint::Index { + display_as_key: false, + name: None, + index_type: Some(IndexType::Hash), + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX idx_name USING BTREE (c1)", + TableConstraint::Index { + display_as_key: false, + name: Some(Ident::new("idx_name")), + index_type: Some(IndexType::BTree), + columns: vec![Ident::new("c1")], + } + ); + + test_parse_table_constraint!( + dialect, + "INDEX idx_name USING HASH (c1)", + TableConstraint::Index { + display_as_key: false, + name: Some(Ident::new("idx_name")), + index_type: Some(IndexType::Hash), + columns: vec![Ident::new("c1")], + } + ); + } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index aa4013394..bae310ef0 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -202,19 +202,19 @@ fn parse_update_with_table_alias() { name: ObjectName(vec![Ident::new("users")]), alias: Some(TableAlias { name: Ident::new("u"), - columns: vec![] + columns: vec![], }), args: None, with_hints: vec![], }, - joins: vec![] + joins: vec![], }, table ); assert_eq!( vec![Assignment { id: vec![Ident::new("u"), Ident::new("username")], - value: Expr::Value(Value::SingleQuotedString("new_user".to_string())) + value: Expr::Value(Value::SingleQuotedString("new_user".to_string())), }], assignments ); @@ -222,12 +222,12 @@ fn parse_update_with_table_alias() { Some(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("u"), - Ident::new("username") + Ident::new("username"), ])), op: BinaryOperator::Eq, right: Box::new(Expr::Value(Value::SingleQuotedString( "old_user".to_string() - ))) + ))), }), selection ); @@ -259,7 +259,7 @@ fn parse_delete_statement() { name: ObjectName(vec![Ident::with_quote('"', "table")]), alias: None, args: None, - with_hints: vec![] + with_hints: vec![], }, table_name ); @@ -284,7 +284,7 @@ fn parse_where_delete_statement() { name: ObjectName(vec![Ident::new("foo")]), alias: None, args: None, - with_hints: vec![] + with_hints: vec![], }, table_name, ); @@ -319,10 +319,10 @@ fn parse_where_delete_with_alias_statement() { name: ObjectName(vec![Ident::new("basket")]), alias: Some(TableAlias { name: Ident::new("a"), - columns: vec![] + columns: vec![], }), args: None, - with_hints: vec![] + with_hints: vec![], }, table_name, ); @@ -332,10 +332,10 @@ fn parse_where_delete_with_alias_statement() { name: ObjectName(vec![Ident::new("basket")]), alias: Some(TableAlias { name: Ident::new("b"), - columns: vec![] + columns: vec![], }), args: None, - with_hints: vec![] + with_hints: vec![], }), using ); @@ -343,12 +343,12 @@ fn parse_where_delete_with_alias_statement() { Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("a"), - Ident::new("id") + Ident::new("id"), ])), op: Lt, right: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("b"), - Ident::new("id") + Ident::new("id"), ])), }, selection.unwrap(), @@ -458,7 +458,7 @@ fn parse_select_into() { temporary: false, unlogged: false, table: false, - name: ObjectName(vec![Ident::new("table0")]) + name: ObjectName(vec![Ident::new("table0")]), }, only(&select.into) ); @@ -677,7 +677,7 @@ fn parse_select_with_date_column_name() { assert_eq!( &Expr::Identifier(Ident { value: "date".into(), - quote_style: None + quote_style: None, }), expr_from_projection(only(&select.projection)), ); @@ -695,7 +695,7 @@ fn parse_escaped_single_quote_string_predicate() { op: NotEq, right: Box::new(Expr::Value(Value::SingleQuotedString( "Jim's salary".to_string() - ))) + ))), }), ast.selection, ); @@ -727,8 +727,8 @@ fn parse_compound_expr_1() { right: Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("b"))), op: Multiply, - right: Box::new(Identifier(Ident::new("c"))) - }) + right: Box::new(Identifier(Ident::new("c"))), + }), }, verified_expr(sql) ); @@ -744,10 +744,10 @@ fn parse_compound_expr_2() { left: Box::new(BinaryOp { left: Box::new(Identifier(Ident::new("a"))), op: Multiply, - right: Box::new(Identifier(Ident::new("b"))) + right: Box::new(Identifier(Ident::new("b"))), }), op: Plus, - right: Box::new(Identifier(Ident::new("c"))) + right: Box::new(Identifier(Ident::new("c"))), }, verified_expr(sql) ); @@ -800,11 +800,12 @@ fn parse_is_distinct_from() { assert_eq!( IsDistinctFrom( Box::new(Identifier(Ident::new("a"))), - Box::new(Identifier(Ident::new("b"))) + Box::new(Identifier(Ident::new("b"))), ), verified_expr(sql) ); } + #[test] fn parse_is_not_distinct_from() { use self::Expr::*; @@ -812,7 +813,7 @@ fn parse_is_not_distinct_from() { assert_eq!( IsNotDistinctFrom( Box::new(Identifier(Ident::new("a"))), - Box::new(Identifier(Ident::new("b"))) + Box::new(Identifier(Ident::new("b"))), ), verified_expr(sql) ); @@ -865,7 +866,7 @@ fn parse_not_precedence() { expr: Box::new(Expr::Value(Value::SingleQuotedString("a".into()))), negated: true, pattern: Box::new(Expr::Value(Value::SingleQuotedString("b".into()))), - escape_char: None + escape_char: None, }), }, ); @@ -898,7 +899,7 @@ fn parse_like() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None + escape_char: None, }, select.selection.unwrap() ); @@ -914,7 +915,7 @@ fn parse_like() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\') + escape_char: Some('\\'), }, select.selection.unwrap() ); @@ -931,7 +932,7 @@ fn parse_like() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None + escape_char: None, })), select.selection.unwrap() ); @@ -953,12 +954,12 @@ fn parse_null_like() { expr: Box::new(Expr::Identifier(Ident::new("column1"))), negated: false, pattern: Box::new(Expr::Value(Value::Null)), - escape_char: None + escape_char: None, }, alias: Ident { value: "col_null".to_owned(), - quote_style: None - } + quote_style: None, + }, }, select.projection[0] ); @@ -968,12 +969,12 @@ fn parse_null_like() { expr: Box::new(Expr::Value(Value::Null)), negated: false, pattern: Box::new(Expr::Identifier(Ident::new("column1"))), - escape_char: None + escape_char: None, }, alias: Ident { value: "null_col".to_owned(), - quote_style: None - } + quote_style: None, + }, }, select.projection[1] ); @@ -992,7 +993,7 @@ fn parse_ilike() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None + escape_char: None, }, select.selection.unwrap() ); @@ -1008,7 +1009,7 @@ fn parse_ilike() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('^') + escape_char: Some('^'), }, select.selection.unwrap() ); @@ -1025,7 +1026,7 @@ fn parse_ilike() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None + escape_char: None, })), select.selection.unwrap() ); @@ -1047,7 +1048,7 @@ fn parse_similar_to() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: None + escape_char: None, }, select.selection.unwrap() ); @@ -1063,7 +1064,7 @@ fn parse_similar_to() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\') + escape_char: Some('\\'), }, select.selection.unwrap() ); @@ -1079,7 +1080,7 @@ fn parse_similar_to() { expr: Box::new(Expr::Identifier(Ident::new("name"))), negated, pattern: Box::new(Expr::Value(Value::SingleQuotedString("%a".to_string()))), - escape_char: Some('\\') + escape_char: Some('\\'), })), select.selection.unwrap() ); @@ -1336,14 +1337,14 @@ fn parse_tuples() { vec![ SelectItem::UnnamedExpr(Expr::Tuple(vec![ Expr::Value(number("1")), - Expr::Value(number("2")) + Expr::Value(number("2")), ])), SelectItem::UnnamedExpr(Expr::Nested(Box::new(Expr::Value(number("1"))))), SelectItem::UnnamedExpr(Expr::Tuple(vec![ Expr::Value(Value::SingleQuotedString("foo".into())), Expr::Value(number("3")), - Expr::Identifier(Ident::new("baz")) - ])) + Expr::Identifier(Ident::new("baz")), + ])), ], select.projection ); @@ -1477,7 +1478,7 @@ fn parse_select_group_by_grouping_sets() { vec![Expr::Identifier(Ident::new("brand"))], vec![Expr::Identifier(Ident::new("size"))], vec![], - ]) + ]), ], select.group_by ); @@ -1496,7 +1497,7 @@ fn parse_select_group_by_rollup() { Expr::Rollup(vec![ vec![Expr::Identifier(Ident::new("brand"))], vec![Expr::Identifier(Ident::new("size"))], - ]) + ]), ], select.group_by ); @@ -1515,7 +1516,7 @@ fn parse_select_group_by_cube() { Expr::Cube(vec![ vec![Expr::Identifier(Ident::new("brand"))], vec![Expr::Identifier(Ident::new("size"))], - ]) + ]), ], select.group_by ); @@ -1535,7 +1536,7 @@ fn parse_select_having() { special: false, })), op: BinaryOperator::Gt, - right: Box::new(Expr::Value(number("1"))) + right: Box::new(Expr::Value(number("1"))), }), select.having ); @@ -1560,15 +1561,15 @@ fn parse_select_qualify() { order_by: vec![OrderByExpr { expr: Expr::Identifier(Ident::new("o")), asc: None, - nulls_first: None + nulls_first: None, }], - window_frame: None + window_frame: None, }), distinct: false, - special: false + special: false, })), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("1"))) + right: Box::new(Expr::Value(number("1"))), }), select.qualify ); @@ -1579,7 +1580,7 @@ fn parse_select_qualify() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("row_num"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(number("1"))) + right: Box::new(Expr::Value(number("1"))), }), select.qualify ); @@ -1600,7 +1601,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::BigInt(None) + data_type: DataType::BigInt(None), }, expr_from_projection(only(&select.projection)) ); @@ -1610,7 +1611,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::TinyInt(None) + data_type: DataType::TinyInt(None), }, expr_from_projection(only(&select.projection)) ); @@ -1642,7 +1643,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Nvarchar(Some(50)) + data_type: DataType::Nvarchar(Some(50)), }, expr_from_projection(only(&select.projection)) ); @@ -1652,7 +1653,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Clob(None) + data_type: DataType::Clob(None), }, expr_from_projection(only(&select.projection)) ); @@ -1662,7 +1663,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Clob(Some(50)) + data_type: DataType::Clob(Some(50)), }, expr_from_projection(only(&select.projection)) ); @@ -1672,7 +1673,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Binary(Some(50)) + data_type: DataType::Binary(Some(50)), }, expr_from_projection(only(&select.projection)) ); @@ -1682,7 +1683,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Varbinary(Some(50)) + data_type: DataType::Varbinary(Some(50)), }, expr_from_projection(only(&select.projection)) ); @@ -1692,7 +1693,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Blob(None) + data_type: DataType::Blob(None), }, expr_from_projection(only(&select.projection)) ); @@ -1702,7 +1703,7 @@ fn parse_cast() { assert_eq!( &Expr::Cast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::Blob(Some(50)) + data_type: DataType::Blob(Some(50)), }, expr_from_projection(only(&select.projection)) ); @@ -1715,7 +1716,7 @@ fn parse_try_cast() { assert_eq!( &Expr::TryCast { expr: Box::new(Expr::Identifier(Ident::new("id"))), - data_type: DataType::BigInt(None) + data_type: DataType::BigInt(None), }, expr_from_projection(only(&select.projection)) ); @@ -1893,7 +1894,7 @@ fn parse_listagg() { ", ".to_string() )))), on_overflow, - within_group + within_group, }), expr_from_projection(only(&select.projection)) ); @@ -1947,12 +1948,12 @@ fn parse_create_table() { name: "name".into(), data_type: DataType::Varchar(Some(CharacterLength { length: 100, - unit: None + unit: None, })), collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::NotNull + option: ColumnOption::NotNull, }], }, ColumnDef { @@ -1961,7 +1962,7 @@ fn parse_create_table() { collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::Null + option: ColumnOption::Null, }], }, ColumnDef { @@ -1977,15 +1978,15 @@ fn parse_create_table() { options: vec![ ColumnOptionDef { name: None, - option: ColumnOption::Null + option: ColumnOption::Null, }, ColumnOptionDef { name: Some("pkey".into()), - option: ColumnOption::Unique { is_primary: true } + option: ColumnOption::Unique { is_primary: true }, }, ColumnOptionDef { name: None, - option: ColumnOption::NotNull + option: ColumnOption::NotNull, }, ColumnOptionDef { name: None, @@ -1994,7 +1995,7 @@ fn parse_create_table() { ColumnOptionDef { name: None, option: ColumnOption::Check(verified_expr("constrained > 0")), - } + }, ], }, ColumnDef { @@ -2005,11 +2006,11 @@ fn parse_create_table() { name: None, option: ColumnOption::ForeignKey { foreign_table: ObjectName(vec!["othertable".into()]), - referred_columns: vec!["a".into(), "b".into(),], + referred_columns: vec!["a".into(), "b".into()], on_delete: None, on_update: None, - } - }] + }, + }], }, ColumnDef { name: "ref2".into(), @@ -2022,9 +2023,9 @@ fn parse_create_table() { referred_columns: vec![], on_delete: Some(ReferentialAction::Cascade), on_update: Some(ReferentialAction::NoAction), - } - },] - } + }, + },], + }, ] ); assert_eq!( @@ -2036,7 +2037,7 @@ fn parse_create_table() { foreign_table: ObjectName(vec!["othertable3".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Restrict), - on_update: None + on_update: None, }, TableConstraint::ForeignKey { name: Some("fkey2".into()), @@ -2044,7 +2045,7 @@ fn parse_create_table() { foreign_table: ObjectName(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::NoAction), - on_update: Some(ReferentialAction::Restrict) + on_update: Some(ReferentialAction::Restrict), }, TableConstraint::ForeignKey { name: None, @@ -2052,7 +2053,7 @@ fn parse_create_table() { foreign_table: ObjectName(vec!["othertable4".into()]), referred_columns: vec!["lat".into()], on_delete: Some(ReferentialAction::Cascade), - on_update: Some(ReferentialAction::SetDefault) + on_update: Some(ReferentialAction::SetDefault), }, TableConstraint::ForeignKey { name: None, @@ -2060,7 +2061,7 @@ fn parse_create_table() { foreign_table: ObjectName(vec!["othertable4".into()]), referred_columns: vec!["longitude".into()], on_delete: None, - on_update: Some(ReferentialAction::SetNull) + on_update: Some(ReferentialAction::SetNull), }, ] ); @@ -2088,10 +2089,10 @@ fn parse_create_table_hive_array() { let dialects = TestedDialects { dialects: vec![Box::new(PostgreSqlDialect {}), Box::new(HiveDialect {})], }; - let sql = "CREATE TABLE IF NOT EXISTS something (key int, val array)"; + let sql = "CREATE TABLE IF NOT EXISTS something (name int, val array)"; match dialects.one_statement_parses_to( sql, - "CREATE TABLE IF NOT EXISTS something (key INT, val INT[])", + "CREATE TABLE IF NOT EXISTS something (name INT, val INT[])", ) { Statement::CreateTable { if_not_exists, @@ -2105,7 +2106,7 @@ fn parse_create_table_hive_array() { columns, vec![ ColumnDef { - name: Ident::new("key"), + name: Ident::new("name"), data_type: DataType::Int(None), collation: None, options: vec![], @@ -2122,7 +2123,8 @@ fn parse_create_table_hive_array() { _ => unreachable!(), } - let res = parse_sql_statements("CREATE TABLE IF NOT EXISTS something (key int, val array=0".to_string())) + Expr::Value(Value::SingleQuotedString(">=0".to_string())), ], else_result: Some(Box::new(Expr::Value(Value::SingleQuotedString( "<0".to_string() - )))) + )))), }, expr_from_projection(only(&select.projection)), ); @@ -3574,10 +3580,10 @@ fn parse_simple_case_expr() { &Case { operand: Some(Box::new(Identifier(Ident::new("foo")))), conditions: vec![Expr::Value(number("1"))], - results: vec![Expr::Value(Value::SingleQuotedString("Y".to_string())),], + results: vec![Expr::Value(Value::SingleQuotedString("Y".to_string()))], else_result: Some(Box::new(Expr::Value(Value::SingleQuotedString( "N".to_string() - )))) + )))), }, expr_from_projection(only(&select.projection)), ); @@ -3618,7 +3624,7 @@ fn parse_implicit_join() { with_hints: vec![], }, joins: vec![], - } + }, ], select.from, ); @@ -3642,7 +3648,7 @@ fn parse_implicit_join() { with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), - }] + }], }, TableWithJoins { relation: TableFactor::Table { @@ -3659,8 +3665,8 @@ fn parse_implicit_join() { with_hints: vec![], }, join_operator: JoinOperator::Inner(JoinConstraint::Natural), - }] - } + }], + }, ], select.from, ); @@ -3678,7 +3684,7 @@ fn parse_cross_join() { args: None, with_hints: vec![], }, - join_operator: JoinOperator::CrossJoin + join_operator: JoinOperator::CrossJoin, }, only(only(select.from).joins), ); @@ -3711,7 +3717,7 @@ fn parse_joins_on() { vec![join_with_constraint( "t2", table_alias("foo"), - JoinOperator::Inner + JoinOperator::Inner, )] ); one_statement_parses_to( @@ -3760,7 +3766,7 @@ fn parse_joins_using() { vec![join_with_constraint( "t2", table_alias("foo"), - JoinOperator::Inner + JoinOperator::Inner, )] ); one_statement_parses_to( @@ -3850,7 +3856,7 @@ fn parse_join_nesting() { only(&verified_only_select(sql).from).joins, vec![ join(nest!(table("b"), nest!(table("c"), table("d"), table("e")))), - join(nest!(table("f"), nest!(table("g"), table("h")))) + join(nest!(table("f"), nest!(table("g"), table("h")))), ], ); @@ -3885,7 +3891,7 @@ fn parse_join_nesting() { relation: table("a"), joins: vec![join(table("b"))], }), - alias: table_alias("c") + alias: table_alias("c"), } ); assert_eq!(from.joins, vec![]); @@ -4054,7 +4060,7 @@ fn parse_derived_tables() { alias: Some(TableAlias { name: "t1".into(), columns: vec![], - }) + }), }, joins: vec![Join { relation: TableFactor::Table { @@ -4066,7 +4072,7 @@ fn parse_derived_tables() { join_operator: JoinOperator::Inner(JoinConstraint::Natural), }], }), - alias: None + alias: None, } ); } @@ -4192,7 +4198,7 @@ fn parse_overlay() { left: Box::new(Expr::Identifier(Ident::new("id"))), op: BinaryOperator::Plus, right: Box::new(Expr::Value(number("1"))), - })) + })), }, expr_from_projection(only(&select.projection)) ); @@ -4239,7 +4245,7 @@ fn parse_exists_subquery() { assert_eq!( Expr::Exists { negated: false, - subquery: Box::new(expected_inner.clone()) + subquery: Box::new(expected_inner.clone()), }, select.selection.unwrap(), ); @@ -4249,7 +4255,7 @@ fn parse_exists_subquery() { assert_eq!( Expr::Exists { negated: true, - subquery: Box::new(expected_inner) + subquery: Box::new(expected_inner), }, select.selection.unwrap(), ); @@ -4344,11 +4350,11 @@ fn parse_create_view_with_options() { vec![ SqlOption { name: "foo".into(), - value: Value::SingleQuotedString("bar".into()) + value: Value::SingleQuotedString("bar".into()), }, SqlOption { name: "a".into(), - value: number("123") + value: number("123"), }, ], with_options @@ -4380,6 +4386,7 @@ fn parse_create_view_with_columns() { _ => unreachable!(), } } + #[test] fn parse_create_or_replace_view() { let sql = "CREATE OR REPLACE VIEW v AS SELECT 1"; @@ -4463,6 +4470,7 @@ fn parse_drop_table() { names, cascade, purge: _, + .. } => { assert!(!if_exists); assert_eq!(ObjectType::Table, object_type); @@ -4483,6 +4491,7 @@ fn parse_drop_table() { names, cascade, purge: _, + .. } => { assert!(if_exists); assert_eq!(ObjectType::Table, object_type); @@ -4985,6 +4994,7 @@ fn parse_create_index() { _ => unreachable!(), } } + #[test] fn parse_drop_index() { let sql = "DROP INDEX idx_a"; @@ -5080,12 +5090,12 @@ fn parse_grant() { columns: Some(vec![ Ident { value: "shape".into(), - quote_style: None + quote_style: None, }, Ident { value: "size".into(), - quote_style: None - } + quote_style: None, + }, ]) }, Action::Usage, @@ -5259,10 +5269,10 @@ fn parse_merge() { name: ObjectName(vec![Ident::new("s"), Ident::new("bar")]), alias: Some(TableAlias { name: Ident::new("dest"), - columns: vec![] + columns: vec![], }), args: None, - with_hints: vec![] + with_hints: vec![], } ); assert_eq!(table, table_no_into); @@ -5283,9 +5293,9 @@ fn parse_merge() { name: ObjectName(vec![Ident::new("s"), Ident::new("foo")]), alias: None, args: None, - with_hints: vec![] + with_hints: vec![], }, - joins: vec![] + joins: vec![], }], lateral_views: vec![], selection: None, @@ -5300,15 +5310,15 @@ fn parse_merge() { limit: None, offset: None, fetch: None, - lock: None + lock: None, }), alias: Some(TableAlias { name: Ident { value: "stg".to_string(), - quote_style: None + quote_style: None, }, - columns: vec![] - }) + columns: vec![], + }), } ); assert_eq!(source, source_no_into); @@ -5319,26 +5329,26 @@ fn parse_merge() { left: Box::new(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("dest"), - Ident::new("D") + Ident::new("D"), ])), op: BinaryOperator::Eq, right: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("stg"), - Ident::new("D") - ])) + Ident::new("D"), + ])), }), op: BinaryOperator::And, right: Box::new(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("dest"), - Ident::new("E") + Ident::new("E"), ])), op: BinaryOperator::Eq, right: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("stg"), - Ident::new("E") - ])) - }) + Ident::new("E"), + ])), + }), }) ); assert_eq!(on, on_no_into); @@ -5353,37 +5363,37 @@ fn parse_merge() { Expr::CompoundIdentifier(vec![Ident::new("stg"), Ident::new("A")]), Expr::CompoundIdentifier(vec![Ident::new("stg"), Ident::new("B")]), Expr::CompoundIdentifier(vec![Ident::new("stg"), Ident::new("C")]), - ]]) + ]]), }, MergeClause::MatchedUpdate { predicate: Some(Expr::BinaryOp { left: Box::new(Expr::CompoundIdentifier(vec![ Ident::new("dest"), - Ident::new("A") + Ident::new("A"), ])), op: BinaryOperator::Eq, right: Box::new(Expr::Value(Value::SingleQuotedString( "a".to_string() - ))) + ))), }), assignments: vec![ Assignment { id: vec![Ident::new("dest"), Ident::new("F")], value: Expr::CompoundIdentifier(vec![ Ident::new("stg"), - Ident::new("F") - ]) + Ident::new("F"), + ]), }, Assignment { id: vec![Ident::new("dest"), Ident::new("G")], value: Expr::CompoundIdentifier(vec![ Ident::new("stg"), - Ident::new("G") - ]) - } - ] + Ident::new("G"), + ]), + }, + ], }, - MergeClause::MatchedDelete(None) + MergeClause::MatchedDelete(None), ] ); assert_eq!(clauses, clauses_no_into); @@ -5439,7 +5449,7 @@ fn test_placeholder() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("id"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Placeholder("?".into()))) + right: Box::new(Expr::Value(Value::Placeholder("?".into()))), }) ); @@ -5462,7 +5472,7 @@ fn test_placeholder() { Some(Expr::BinaryOp { left: Box::new(Expr::Identifier(Ident::new("id"))), op: BinaryOperator::Eq, - right: Box::new(Expr::Value(Value::Placeholder("$Id1".into()))) + right: Box::new(Expr::Value(Value::Placeholder("$Id1".into()))), }) ); @@ -5810,3 +5820,249 @@ fn parse_show_functions() { } ); } + +#[test] +fn parse_cache_table() { + let sql = "SELECT a, b, c FROM foo"; + let cache_table_name = "cache_table_name"; + let table_flag = "flag"; + let query = all_dialects().verified_query(sql); + + assert_eq!( + verified_stmt( + format!("CACHE TABLE '{table_name}'", table_name = cache_table_name).as_str() + ), + Statement::Cache { + table_flag: None, + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: false, + options: vec![], + query: None, + } + ); + + assert_eq!( + verified_stmt( + format!( + "CACHE {flag} TABLE '{table_name}'", + flag = table_flag, + table_name = cache_table_name + ) + .as_str() + ), + Statement::Cache { + table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: false, + options: vec![], + query: None, + } + ); + + assert_eq!( + verified_stmt( + format!( + "CACHE {flag} TABLE '{table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88)", + flag = table_flag, + table_name = cache_table_name, + ) + .as_str() + ), + Statement::Cache { + table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: false, + options: vec![ + SqlOption { + name: Ident::with_quote('\'', "K1"), + value: Value::SingleQuotedString("V1".into()), + }, + SqlOption { + name: Ident::with_quote('\'', "K2"), + value: number("0.88"), + }, + ], + query: None, + } + ); + + assert_eq!( + verified_stmt( + format!( + "CACHE {flag} TABLE '{table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88) {sql}", + flag = table_flag, + table_name = cache_table_name, + sql = sql, + ) + .as_str() + ), + Statement::Cache { + table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: false, + options: vec![ + SqlOption { + name: Ident::with_quote('\'', "K1"), + value: Value::SingleQuotedString("V1".into()), + }, + SqlOption { + name: Ident::with_quote('\'', "K2"), + value: number("0.88"), + }, + ], + query: Some(query.clone()), + } + ); + + assert_eq!( + verified_stmt( + format!( + "CACHE {flag} TABLE '{table_name}' OPTIONS('K1' = 'V1', 'K2' = 0.88) AS {sql}", + flag = table_flag, + table_name = cache_table_name, + sql = sql, + ) + .as_str() + ), + Statement::Cache { + table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: true, + options: vec![ + SqlOption { + name: Ident::with_quote('\'', "K1"), + value: Value::SingleQuotedString("V1".into()), + }, + SqlOption { + name: Ident::with_quote('\'', "K2"), + value: number("0.88"), + }, + ], + query: Some(query.clone()), + } + ); + + assert_eq!( + verified_stmt( + format!( + "CACHE {flag} TABLE '{table_name}' {sql}", + flag = table_flag, + table_name = cache_table_name, + sql = sql + ) + .as_str() + ), + Statement::Cache { + table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: false, + options: vec![], + query: Some(query.clone()), + } + ); + + assert_eq!( + verified_stmt( + format!( + "CACHE {flag} TABLE '{table_name}' AS {sql}", + flag = table_flag, + table_name = cache_table_name + ) + .as_str() + ), + Statement::Cache { + table_flag: Some(ObjectName(vec![Ident::new(table_flag)])), + table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), + has_as: true, + options: vec![], + query: Some(query), + } + ); + + let res = parse_sql_statements("CACHE TABLE 'table_name' foo"); + assert_eq!( + ParserError::ParserError( + "Expected SELECT, VALUES, or a subquery in the query body, found: foo".to_string() + ), + res.unwrap_err() + ); + + let res = parse_sql_statements("CACHE flag TABLE 'table_name' OPTIONS('K1'='V1') foo"); + assert_eq!( + ParserError::ParserError( + "Expected SELECT, VALUES, or a subquery in the query body, found: foo".to_string() + ), + res.unwrap_err() + ); + + let res = parse_sql_statements("CACHE TABLE 'table_name' AS foo"); + assert_eq!( + ParserError::ParserError( + "Expected SELECT, VALUES, or a subquery in the query body, found: foo".to_string() + ), + res.unwrap_err() + ); + + let res = parse_sql_statements("CACHE flag TABLE 'table_name' OPTIONS('K1'='V1') AS foo"); + assert_eq!( + ParserError::ParserError( + "Expected SELECT, VALUES, or a subquery in the query body, found: foo".to_string() + ), + res.unwrap_err() + ); + + let res = parse_sql_statements("CACHE 'table_name'"); + assert_eq!( + ParserError::ParserError("Expected a `TABLE` keyword, found: 'table_name'".to_string()), + res.unwrap_err() + ); + + let res = parse_sql_statements("CACHE 'table_name' OPTIONS('K1'='V1')"); + assert_eq!( + ParserError::ParserError("Expected a `TABLE` keyword, found: OPTIONS".to_string()), + res.unwrap_err() + ); + + let res = parse_sql_statements("CACHE flag 'table_name' OPTIONS('K1'='V1')"); + assert_eq!( + ParserError::ParserError("Expected a `TABLE` keyword, found: 'table_name'".to_string()), + res.unwrap_err() + ); +} + +#[test] +fn parse_uncache_table() { + assert_eq!( + verified_stmt("UNCACHE TABLE 'table_name'"), + Statement::UNCache { + table_name: ObjectName(vec![Ident::with_quote('\'', "table_name")]), + if_exists: false, + } + ); + + assert_eq!( + verified_stmt("UNCACHE TABLE IF EXISTS 'table_name'"), + Statement::UNCache { + table_name: ObjectName(vec![Ident::with_quote('\'', "table_name")]), + if_exists: true, + } + ); + + let res = parse_sql_statements("UNCACHE TABLE 'table_name' foo"); + assert_eq!( + ParserError::ParserError("Expected an `EOF`, found: foo".to_string()), + res.unwrap_err() + ); + + let res = parse_sql_statements("UNCACHE 'table_name' foo"); + assert_eq!( + ParserError::ParserError("Expected a `TABLE` keyword, found: 'table_name'".to_string()), + res.unwrap_err() + ); + + let res = parse_sql_statements("UNCACHE IF EXISTS 'table_name' foo"); + assert_eq!( + ParserError::ParserError("Expected a `TABLE` keyword, found: IF".to_string()), + res.unwrap_err() + ); +} diff --git a/tests/sqlparser_mysql.rs b/tests/sqlparser_mysql.rs index 8b8754db4..8c42c715b 100644 --- a/tests/sqlparser_mysql.rs +++ b/tests/sqlparser_mysql.rs @@ -1073,6 +1073,49 @@ fn parse_limit_my_sql_syntax() { ); } +#[test] +fn parse_create_table_with_index_definition() { + mysql_and_generic().one_statement_parses_to( + "CREATE TABLE tb (id INT, INDEX (id))", + "CREATE TABLE tb (id INT, INDEX (id))", + ); + + mysql_and_generic().one_statement_parses_to( + "CREATE TABLE tb (id INT, index USING BTREE (id))", + "CREATE TABLE tb (id INT, INDEX USING BTREE (id))", + ); + + mysql_and_generic().one_statement_parses_to( + "CREATE TABLE tb (id INT, KEY USING HASH (id))", + "CREATE TABLE tb (id INT, KEY USING HASH (id))", + ); + + mysql_and_generic().one_statement_parses_to( + "CREATE TABLE tb (id INT, key index (id))", + "CREATE TABLE tb (id INT, KEY index (id))", + ); + + mysql_and_generic().one_statement_parses_to( + "CREATE TABLE tb (id INT, INDEX 'index' (id))", + "CREATE TABLE tb (id INT, INDEX 'index' (id))", + ); + + mysql_and_generic().one_statement_parses_to( + "CREATE TABLE tb (id INT, INDEX index USING BTREE (id))", + "CREATE TABLE tb (id INT, INDEX index USING BTREE (id))", + ); + + mysql_and_generic().one_statement_parses_to( + "CREATE TABLE tb (id INT, INDEX index USING HASH (id))", + "CREATE TABLE tb (id INT, INDEX index USING HASH (id))", + ); + + mysql_and_generic().one_statement_parses_to( + "CREATE TABLE tb (id INT, INDEX (c1, c2, c3, c4,c5))", + "CREATE TABLE tb (id INT, INDEX (c1, c2, c3, c4, c5))", + ); +} + fn mysql() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MySqlDialect {})], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 93be8e4ad..c5d4bd0fc 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -22,6 +22,42 @@ use sqlparser::ast::*; use sqlparser::dialect::{GenericDialect, PostgreSqlDialect}; use sqlparser::parser::ParserError; +#[test] +fn parse_create_sequence() { + // SimpleLogger::new().init().unwrap(); + + let sql1 = "CREATE SEQUENCE name0"; + pg().one_statement_parses_to(sql1, "CREATE SEQUENCE name0"); + + let sql2 = "CREATE SEQUENCE IF NOT EXISTS name0"; + pg().one_statement_parses_to(sql2, "CREATE SEQUENCE IF NOT EXISTS name0"); + + let sql3 = "CREATE TEMPORARY SEQUENCE IF NOT EXISTS name0"; + pg().one_statement_parses_to(sql3, "CREATE TEMPORARY SEQUENCE IF NOT EXISTS name0"); + + let sql4 = "CREATE TEMPORARY SEQUENCE name0"; + pg().one_statement_parses_to(sql4, "CREATE TEMPORARY SEQUENCE name0"); +} + +#[test] +fn parse_drop_sequence() { + // SimpleLogger::new().init().unwrap(); + let sql1 = "DROP SEQUENCE IF EXISTS name0 CASCADE"; + pg().one_statement_parses_to(sql1, "DROP SEQUENCE IF EXISTS name0 CASCADE"); + let sql2 = "DROP SEQUENCE IF EXISTS name1 RESTRICT"; + pg().one_statement_parses_to(sql2, "DROP SEQUENCE IF EXISTS name1 RESTRICT"); + let sql3 = "DROP SEQUENCE name2 CASCADE"; + pg().one_statement_parses_to(sql3, "DROP SEQUENCE name2 CASCADE"); + let sql4 = "DROP SEQUENCE name2"; + pg().one_statement_parses_to(sql4, "DROP SEQUENCE name2"); + let sql5 = "DROP SEQUENCE name0 CASCADE"; + pg().one_statement_parses_to(sql5, "DROP SEQUENCE name0 CASCADE"); + let sql6 = "DROP SEQUENCE name1 RESTRICT"; + pg().one_statement_parses_to(sql6, "DROP SEQUENCE name1 RESTRICT"); + let sql7 = "DROP SEQUENCE name1, name2, name3"; + pg().one_statement_parses_to(sql7, "DROP SEQUENCE name1, name2, name3"); +} + #[test] fn parse_create_table_with_defaults() { let sql = "CREATE TABLE public.customer (