diff --git a/src/sql/src/parsers/create_parser.rs b/src/sql/src/parsers/create_parser.rs index d1a76ffacf..0ddb8e6ff5 100644 --- a/src/sql/src/parsers/create_parser.rs +++ b/src/sql/src/parsers/create_parser.rs @@ -18,7 +18,8 @@ use itertools::Itertools; use mito::engine; use once_cell::sync::Lazy; use snafu::{ensure, OptionExt, ResultExt}; -use sqlparser::ast::Value; +use sqlparser::ast::ColumnOption::NotNull; +use sqlparser::ast::{ColumnOptionDef, DataType, Value}; use sqlparser::dialect::keywords::Keyword; use sqlparser::parser::IsOptional::Mandatory; use sqlparser::tokenizer::{Token, Word}; @@ -220,11 +221,7 @@ impl<'a> ParserContext<'a> { if let Some(constraint) = self.parse_optional_table_constraint()? { constraints.push(constraint); } else if let Token::Word(_) = self.parser.peek_token() { - columns.push( - self.parser - .parse_column_def() - .context(SyntaxSnafu { sql: self.sql })?, - ); + self.parse_column(&mut columns, &mut constraints)?; } else { return self.expected( "column name or constraint definition", @@ -246,6 +243,75 @@ impl<'a> ParserContext<'a> { Ok((columns, constraints)) } + fn parse_column( + &mut self, + columns: &mut Vec, + constraints: &mut Vec, + ) -> Result<()> { + let column = self + .parser + .parse_column_def() + .context(SyntaxSnafu { sql: self.sql })?; + + if !matches!(column.data_type, DataType::Timestamp) + || matches!(self.parser.peek_token(), Token::Comma) + { + columns.push(column); + return Ok(()); + } + + // for supporting `ts TIMESTAMP TIME INDEX,` syntax. + self.parse_time_index(column, columns, constraints) + } + + fn parse_time_index( + &mut self, + mut column: ColumnDef, + columns: &mut Vec, + constraints: &mut Vec, + ) -> Result<()> { + self.parser + .expect_keywords(&[Keyword::TIME, Keyword::INDEX]) + .context(error::UnexpectedSnafu { + sql: self.sql, + expected: "TIME INDEX", + actual: self.peek_token_as_string(), + })?; + + let constraint = TableConstraint::Unique { + name: Some(Ident { + value: TIME_INDEX.to_owned(), + quote_style: None, + }), + columns: vec![Ident { + value: column.name.value.clone(), + quote_style: None, + }], + is_primary: false, + }; + + column.options = vec![ColumnOptionDef { + name: None, + option: NotNull, + }]; + columns.push(column); + constraints.push(constraint); + + if let Token::Comma = self.parser.peek_token() { + return Ok(()); + } + + self.parser + .expect_keywords(&[Keyword::NOT, Keyword::NULL]) + .context(error::UnexpectedSnafu { + sql: self.sql, + expected: "NOT NULL", + actual: self.peek_token_as_string(), + })?; + + Ok(()) + } + // Copy from sqlparser by boyan fn parse_optional_table_constraint(&mut self) -> Result> { let name = if self.parser.parse_keyword(Keyword::CONSTRAINT) { @@ -705,6 +771,160 @@ ENGINE=mito"; } } + #[test] + fn test_parse_create_table_with_timestamp_index() { + let sql1 = r" +CREATE TABLE monitor ( + host_id INT, + idc STRING, + ts TIMESTAMP TIME INDEX, + cpu DOUBLE DEFAULT 0, + memory DOUBLE, + PRIMARY KEY (host), +) +ENGINE=mito"; + let result1 = ParserContext::create_with_dialect(sql1, &GenericDialect {}).unwrap(); + + if let Statement::CreateTable(c) = &result1[0] { + assert_eq!(c.constraints.len(), 2); + let tc = c.constraints[0].clone(); + match tc { + TableConstraint::Unique { + name, + columns, + is_primary, + } => { + assert_eq!(name.unwrap().to_string(), "__time_index"); + assert_eq!(columns.len(), 1); + assert_eq!(&columns[0].value, "ts"); + assert!(!is_primary); + } + _ => panic!("should be time index constraint"), + }; + } else { + panic!("should be create_table statement"); + } + + // `TIME INDEX` should be in front of `PRIMARY KEY` + // in order to equal the `TIMESTAMP TIME INDEX` constraint options vector + let sql2 = r" +CREATE TABLE monitor ( + host_id INT, + idc STRING, + ts TIMESTAMP NOT NULL, + cpu DOUBLE DEFAULT 0, + memory DOUBLE, + TIME INDEX (ts), + PRIMARY KEY (host), +) +ENGINE=mito"; + let result2 = ParserContext::create_with_dialect(sql2, &GenericDialect {}).unwrap(); + + assert_eq!(result1, result2); + + // TIMESTAMP can be NULL which is not equal to above + let sql3 = r" +CREATE TABLE monitor ( + host_id INT, + idc STRING, + ts TIMESTAMP, + cpu DOUBLE DEFAULT 0, + memory DOUBLE, + TIME INDEX (ts), + PRIMARY KEY (host), +) +ENGINE=mito"; + + let result3 = ParserContext::create_with_dialect(sql3, &GenericDialect {}).unwrap(); + + assert_ne!(result1, result3); + } + + #[test] + fn test_parse_create_table_with_timestamp_index_not_null() { + let sql = r" +CREATE TABLE monitor ( + host_id INT, + idc STRING, + ts TIMESTAMP TIME INDEX, + cpu DOUBLE DEFAULT 0, + memory DOUBLE, + TIME INDEX (ts), + PRIMARY KEY (host), +) +ENGINE=mito"; + let result = ParserContext::create_with_dialect(sql, &GenericDialect {}).unwrap(); + + assert_eq!(result.len(), 1); + if let Statement::CreateTable(c) = &result[0] { + let ts = c.columns[2].clone(); + assert_eq!(ts.name.to_string(), "ts"); + assert_eq!(ts.options[0].option, NotNull); + } else { + panic!("should be create table statement"); + } + + let sql1 = r" +CREATE TABLE monitor ( + host_id INT, + idc STRING, + ts TIMESTAMP NOT NULL TIME INDEX, + cpu DOUBLE DEFAULT 0, + memory DOUBLE, + TIME INDEX (ts), + PRIMARY KEY (host), +) +ENGINE=mito"; + + let result1 = ParserContext::create_with_dialect(sql1, &GenericDialect {}).unwrap(); + assert_eq!(result, result1); + + let sql2 = r" +CREATE TABLE monitor ( + host_id INT, + idc STRING, + ts TIMESTAMP TIME INDEX NOT NULL, + cpu DOUBLE DEFAULT 0, + memory DOUBLE, + TIME INDEX (ts), + PRIMARY KEY (host), +) +ENGINE=mito"; + + let result2 = ParserContext::create_with_dialect(sql2, &GenericDialect {}).unwrap(); + assert_eq!(result, result2); + + let sql3 = r" +CREATE TABLE monitor ( + host_id INT, + idc STRING, + ts TIMESTAMP TIME INDEX NULL NOT, + cpu DOUBLE DEFAULT 0, + memory DOUBLE, + TIME INDEX (ts), + PRIMARY KEY (host), +) +ENGINE=mito"; + + let result3 = ParserContext::create_with_dialect(sql3, &GenericDialect {}); + assert!(result3.is_err()); + + let sql4 = r" +CREATE TABLE monitor ( + host_id INT, + idc STRING, + ts TIMESTAMP TIME INDEX NOT NULL NULL, + cpu DOUBLE DEFAULT 0, + memory DOUBLE, + TIME INDEX (ts), + PRIMARY KEY (host), +) +ENGINE=mito"; + + let result4 = ParserContext::create_with_dialect(sql4, &GenericDialect {}); + assert!(result4.is_err()); + } + #[test] fn test_parse_partitions_with_error_syntax() { let sql = r"