From 7612a1e52b32f33936eb07001767d4d853f24bc1 Mon Sep 17 00:00:00 2001 From: Simon Sawert Date: Thu, 5 Sep 2024 17:09:20 +0200 Subject: [PATCH 1/4] feat: Add support for T-SQL table options --- src/ast/mod.rs | 113 +++++++++++++++++++++- src/keywords.rs | 2 + src/parser/mod.rs | 108 ++++++++++++++++++++- tests/sqlparser_bigquery.rs | 14 +-- tests/sqlparser_common.rs | 28 +++--- tests/sqlparser_mssql.rs | 183 ++++++++++++++++++++++++++++++++++++ tests/sqlparser_postgres.rs | 10 +- 7 files changed, 425 insertions(+), 33 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 2bb7a161a..397c8b5e2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1943,6 +1943,15 @@ pub enum CreateTableOptions { /// e.g. `WITH (description = "123")` /// /// + /// + /// T-sql supports more specific options that's not only key-value pairs. + /// + /// WITH ( + /// DISTRIBUTION = ROUND_ROBIN, + /// CLUSTERED INDEX (column_a DESC, column_b) + /// ) + /// + /// With(Vec), /// Options specified using the `OPTIONS` keyword. /// e.g. `OPTIONS(description = "123")` @@ -5589,14 +5598,112 @@ pub struct HiveFormat { #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] -pub struct SqlOption { +pub struct ClusteredIndex { pub name: Ident, - pub value: Expr, + pub asc: Option, +} + +impl fmt::Display for ClusteredIndex { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name)?; + match self.asc { + Some(true) => write!(f, " ASC"), + Some(false) => write!(f, " DESC"), + _ => Ok(()), + } + } +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum TableOptionsClustered { + ColumnstoreIndex, + ColumnstoreIndexOrder(Vec), + Index(Vec), +} + +impl fmt::Display for TableOptionsClustered { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + TableOptionsClustered::ColumnstoreIndex => { + write!(f, "CLUSTERED COLUMNSTORE INDEX") + } + TableOptionsClustered::ColumnstoreIndexOrder(values) => { + write!( + f, + "CLUSTERED COLUMNSTORE INDEX ORDER ({})", + display_comma_separated(values) + ) + } + TableOptionsClustered::Index(values) => { + write!(f, "CLUSTERED INDEX ({})", display_comma_separated(values)) + } + } + } +} + +/// Specifies which partition the boundary values on table partitioning belongs to. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum PartitionRangeDirection { + Left, + Right, +} + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum SqlOption { + /// Clustered represents the clustered version of table storage for T-sql. + /// + /// + Clustered(TableOptionsClustered), + /// Single identifier options, e.g. `HEAP`. + Ident(Ident), + /// Any option that consists of a key value pair where the value is an expression. + KeyValue { name: Ident, value: Expr }, + /// One or more table partitions and represents which partition the boundary values belong to. + /// + /// + Partition { + column_name: Ident, + range_direction: Option, + for_values: Vec, + }, } impl fmt::Display for SqlOption { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{} = {}", self.name, self.value) + match self { + SqlOption::Clustered(c) => write!(f, "{}", c), + SqlOption::Ident(ident) => { + write!(f, "{}", ident) + } + SqlOption::KeyValue { name, value } => { + write!(f, "{} = {}", name, value) + } + SqlOption::Partition { + column_name, + range_direction, + for_values, + } => { + let direction = match range_direction { + Some(PartitionRangeDirection::Left) => " LEFT", + Some(PartitionRangeDirection::Right) => " RIGHT", + None => "", + }; + + write!( + f, + "PARTITION ({} RANGE{} FOR VALUES ({}))", + column_name, + direction, + display_comma_separated(for_values) + ) + } + } } } diff --git a/src/keywords.rs b/src/keywords.rs index ae0f14f18..057dd5e33 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -165,6 +165,7 @@ define_keywords!( COLLECTION, COLUMN, COLUMNS, + COLUMNSTORE, COMMENT, COMMIT, COMMITTED, @@ -354,6 +355,7 @@ define_keywords!( HASH, HAVING, HEADER, + HEAP, HIGH_PRIORITY, HISTORY, HIVEVAR, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 30e776787..cdc405c3f 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4458,7 +4458,7 @@ impl<'a> Parser<'a> { let name = self.parse_object_name(allow_unquoted_hyphen)?; let columns = self.parse_view_columns()?; let mut options = CreateTableOptions::None; - let with_options = self.parse_options(Keyword::WITH)?; + let with_options = self.parse_table_options(Keyword::WITH)?; if !with_options.is_empty() { options = CreateTableOptions::With(with_options); } @@ -5621,7 +5621,8 @@ impl<'a> Parser<'a> { let clustered_by = self.parse_optional_clustered_by()?; let hive_formats = self.parse_hive_formats()?; // PostgreSQL supports `WITH ( options )`, before `AS` - let with_options = self.parse_options(Keyword::WITH)?; + // T-sql supports `WITH` options for clustering and distribution + let with_options = self.parse_table_options(Keyword::WITH)?; let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?; let engine = if self.parse_keyword(Keyword::ENGINE) { @@ -6399,6 +6400,17 @@ impl<'a> Parser<'a> { Ok(None) } + pub fn parse_table_options(&mut self, keyword: Keyword) -> Result, ParserError> { + if self.parse_keyword(keyword) { + self.expect_token(&Token::LParen)?; + let options = self.parse_comma_separated(Parser::parse_sql_option)?; + self.expect_token(&Token::RParen)?; + Ok(options) + } else { + Ok(vec![]) + } + } + pub fn parse_options(&mut self, keyword: Keyword) -> Result, ParserError> { if self.parse_keyword(keyword) { self.expect_token(&Token::LParen)?; @@ -6484,11 +6496,99 @@ impl<'a> Parser<'a> { } } - pub fn parse_sql_option(&mut self) -> Result { + pub fn parse_key_value(&mut self) -> Result<(Ident, Expr), ParserError> { let name = self.parse_identifier(false)?; self.expect_token(&Token::Eq)?; let value = self.parse_expr()?; - Ok(SqlOption { name, value }) + + Ok((name, value)) + } + + pub fn parse_sql_option(&mut self) -> Result { + let next_token = self.peek_token(); + let is_mssql = dialect_of!(self is MsSqlDialect|GenericDialect); + + let Token::Word(w) = next_token.token else { + let (name, value) = self.parse_key_value()?; + return Ok(SqlOption::KeyValue { name, value }); + }; + + match w.keyword { + Keyword::HEAP if is_mssql => Ok(SqlOption::Ident(self.parse_identifier(false)?)), + Keyword::PARTITION if is_mssql => self.parse_table_option_partition(), + Keyword::CLUSTERED if is_mssql => self.parse_table_option_clustered(), + _ => { + let (name, value) = self.parse_key_value()?; + Ok(SqlOption::KeyValue { name, value }) + } + } + } + + pub fn parse_table_option_clustered(&mut self) -> Result { + self.expect_keyword(Keyword::CLUSTERED)?; + + if self.parse_keywords(&[Keyword::COLUMNSTORE, Keyword::INDEX]) { + if self.parse_keyword(Keyword::ORDER) { + Ok(SqlOption::Clustered( + TableOptionsClustered::ColumnstoreIndexOrder( + self.parse_parenthesized_column_list(IsOptional::Mandatory, false)?, + ), + )) + } else { + Ok(SqlOption::Clustered( + TableOptionsClustered::ColumnstoreIndex, + )) + } + } else { + self.expect_keyword(Keyword::INDEX)?; + self.expect_token(&Token::LParen)?; + + let columns = self.parse_comma_separated(|p| { + let name = p.parse_identifier(false)?; + let asc = if p.parse_keyword(Keyword::ASC) { + Some(true) + } else if p.parse_keyword(Keyword::DESC) { + Some(false) + } else { + None + }; + + Ok(ClusteredIndex { name, asc }) + })?; + + self.expect_token(&Token::RParen)?; + + Ok(SqlOption::Clustered(TableOptionsClustered::Index(columns))) + } + } + + pub fn parse_table_option_partition(&mut self) -> Result { + self.expect_keyword(Keyword::PARTITION)?; + self.expect_token(&Token::LParen)?; + let column_name = self.parse_identifier(false)?; + + self.expect_keyword(Keyword::RANGE)?; + let range_direction = if self.parse_keyword(Keyword::LEFT) { + Some(PartitionRangeDirection::Left) + } else if self.parse_keyword(Keyword::RIGHT) { + Some(PartitionRangeDirection::Right) + } else { + None + }; + + self.expect_keywords(&[Keyword::FOR, Keyword::VALUES])?; + self.expect_token(&Token::LParen)?; + + let for_values = self.parse_comma_separated(Parser::parse_expr)?; + + self.expect_token(&Token::RParen)?; + self.expect_token(&Token::RParen)?; + + Ok(SqlOption::Partition { + column_name, + range_direction, + for_values, + }) } pub fn parse_partition(&mut self) -> Result { diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 57cf9d7fd..6363433f8 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -268,7 +268,7 @@ fn parse_create_view_with_options() { ViewColumnDef { name: Ident::new("age"), data_type: None, - options: Some(vec![SqlOption { + options: Some(vec![SqlOption::KeyValue { name: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString("field age".to_string())), }]) @@ -288,7 +288,7 @@ fn parse_create_view_with_options() { unreachable!() }; assert_eq!( - &SqlOption { + &SqlOption::KeyValue { name: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "a view that expires in 2 days".to_string() @@ -415,7 +415,7 @@ fn parse_create_table_with_options() { }, ColumnOptionDef { name: None, - option: ColumnOption::Options(vec![SqlOption { + option: ColumnOption::Options(vec![SqlOption::KeyValue { name: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "field x".to_string() @@ -430,7 +430,7 @@ fn parse_create_table_with_options() { collation: None, options: vec![ColumnOptionDef { name: None, - option: ColumnOption::Options(vec![SqlOption { + option: ColumnOption::Options(vec![SqlOption::KeyValue { name: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "field y".to_string() @@ -449,11 +449,11 @@ fn parse_create_table_with_options() { Ident::new("age"), ])), Some(vec![ - SqlOption { + SqlOption::KeyValue { name: Ident::new("partition_expiration_days"), value: Expr::Value(number("1")), }, - SqlOption { + SqlOption::KeyValue { name: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "table option description".to_string() @@ -2010,7 +2010,7 @@ fn test_bigquery_create_function() { function_body: Some(CreateFunctionBody::AsAfterOptions(Expr::Value(number( "42" )))), - options: Some(vec![SqlOption { + options: Some(vec![SqlOption::KeyValue { name: Ident::new("x"), value: Expr::Value(Value::SingleQuotedString("y".into())), }]), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index fbe97171b..f010ef3fc 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3637,11 +3637,11 @@ fn parse_create_table_with_options() { Statement::CreateTable(CreateTable { with_options, .. }) => { assert_eq!( vec![ - SqlOption { + SqlOption::KeyValue { name: "foo".into(), value: Expr::Value(Value::SingleQuotedString("bar".into())), }, - SqlOption { + SqlOption::KeyValue { name: "a".into(), value: Expr::Value(number("123")), }, @@ -3870,7 +3870,7 @@ fn parse_alter_table() { AlterTableOperation::SetTblProperties { table_properties } => { assert_eq!( table_properties, - [SqlOption { + [SqlOption::KeyValue { name: Ident { value: "classification".to_string(), quote_style: Some('\'') @@ -3958,11 +3958,11 @@ fn parse_alter_view_with_options() { Statement::AlterView { with_options, .. } => { assert_eq!( vec![ - SqlOption { + SqlOption::KeyValue { name: "foo".into(), value: Expr::Value(Value::SingleQuotedString("bar".into())), }, - SqlOption { + SqlOption::KeyValue { name: "a".into(), value: Expr::Value(number("123")), }, @@ -6530,11 +6530,11 @@ fn parse_create_view_with_options() { Statement::CreateView { options, .. } => { assert_eq!( CreateTableOptions::With(vec![ - SqlOption { + SqlOption::KeyValue { name: "foo".into(), value: Expr::Value(Value::SingleQuotedString("bar".into())), }, - SqlOption { + SqlOption::KeyValue { name: "a".into(), value: Expr::Value(number("123")), }, @@ -8628,11 +8628,11 @@ fn parse_cache_table() { table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![ - SqlOption { + SqlOption::KeyValue { name: Ident::with_quote('\'', "K1"), value: Expr::Value(Value::SingleQuotedString("V1".into())), }, - SqlOption { + SqlOption::KeyValue { name: Ident::with_quote('\'', "K2"), value: Expr::Value(number("0.88")), }, @@ -8653,11 +8653,11 @@ fn parse_cache_table() { table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), has_as: false, options: vec![ - SqlOption { + SqlOption::KeyValue { name: Ident::with_quote('\'', "K1"), value: Expr::Value(Value::SingleQuotedString("V1".into())), }, - SqlOption { + SqlOption::KeyValue { name: Ident::with_quote('\'', "K2"), value: Expr::Value(number("0.88")), }, @@ -8678,11 +8678,11 @@ fn parse_cache_table() { table_name: ObjectName(vec![Ident::with_quote('\'', cache_table_name)]), has_as: true, options: vec![ - SqlOption { + SqlOption::KeyValue { name: Ident::with_quote('\'', "K1"), value: Expr::Value(Value::SingleQuotedString("V1".into())), }, - SqlOption { + SqlOption::KeyValue { name: Ident::with_quote('\'', "K2"), value: Expr::Value(number("0.88")), }, @@ -9489,7 +9489,7 @@ fn parse_unload() { value: "s3://...".to_string(), quote_style: Some('\'') }, - with: vec![SqlOption { + with: vec![SqlOption::KeyValue { name: Ident { value: "format".to_string(), quote_style: None diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 5c2ec8763..575a45c22 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -653,6 +653,189 @@ fn parse_use() { } } +#[test] +fn parse_create_table_with_options() { + let options = [ + ( + "PARTITION (column_a RANGE LEFT FOR VALUES (10, 11))", + SqlOption::Partition { + column_name: "column_a".into(), + range_direction: Some(PartitionRangeDirection::Left), + for_values: vec![Expr::Value(test_utils::number("10")), Expr::Value(test_utils::number("11"))] }, + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = ROUND_ROBIN, PARTITION (column_a RANGE LEFT FOR VALUES (10, 11)))", + ), + ( + "HEAP", + SqlOption::Ident("HEAP".into()), + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = ROUND_ROBIN, HEAP)", + ), + ( + "CLUSTERED COLUMNSTORE INDEX", + SqlOption::Clustered(TableOptionsClustered::ColumnstoreIndex), + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = ROUND_ROBIN, CLUSTERED COLUMNSTORE INDEX)", + ), + ( + "CLUSTERED COLUMNSTORE INDEX ORDER (column_a, column_b)", + SqlOption::Clustered(TableOptionsClustered::ColumnstoreIndexOrder(vec![ + "column_a".into(), + "column_b".into(), + ])), + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = ROUND_ROBIN, CLUSTERED COLUMNSTORE INDEX ORDER (column_a, column_b))", + ), + ( + "CLUSTERED INDEX (column_a ASC, column_b DESC, column_c)", + SqlOption::Clustered(TableOptionsClustered::Index(vec![ + ClusteredIndex { + name: Ident { + value: "column_a".to_string(), + quote_style: None, + }, + asc: Some(true), + }, + ClusteredIndex { + name: Ident { + value: "column_b".to_string(), + quote_style: None, + }, + asc: Some(false), + }, + ClusteredIndex { + name: Ident { + value: "column_c".to_string(), + quote_style: None, + }, + asc: None, + }, + ])), + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = ROUND_ROBIN, CLUSTERED INDEX (column_a ASC, column_b DESC, column_c))", + ), + ]; + + for (query_placeholder, query_with_option, full_query) in options { + let sql = format!( + " + CREATE TABLE mytable ( + column_a INT, + column_b INT, + column_c INT + ) + WITH ( + DISTRIBUTION = ROUND_ROBIN, + {query_placeholder} + ) + " + ); + + let stmt = ms() + .parse_sql_statements(sql.as_str()) + .unwrap() + .first() + .unwrap() + .to_owned(); + + let with_options = vec![ + SqlOption::KeyValue { + name: Ident { + value: "DISTRIBUTION".to_string(), + quote_style: None, + }, + value: Expr::Identifier(Ident { + value: "ROUND_ROBIN".to_string(), + quote_style: None, + }), + }, + query_with_option, + ]; + + assert_eq!( + Statement::CreateTable(CreateTable { + or_replace: false, + temporary: false, + external: false, + global: None, + if_not_exists: false, + transient: false, + volatile: false, + name: ObjectName(vec![Ident { + value: "mytable".to_string(), + quote_style: None, + },],), + columns: vec![ + ColumnDef { + name: Ident { + value: "column_a".to_string(), + quote_style: None, + }, + data_type: Int(None,), + collation: None, + options: vec![], + }, + ColumnDef { + name: Ident { + value: "column_b".to_string(), + quote_style: None, + }, + data_type: Int(None,), + collation: None, + options: vec![], + }, + ColumnDef { + name: Ident { + value: "column_c".to_string(), + quote_style: None, + }, + data_type: Int(None,), + collation: None, + options: vec![], + }, + ], + constraints: vec![], + hive_distribution: HiveDistributionStyle::NONE, + hive_formats: Some(HiveFormat { + row_format: None, + serde_properties: None, + storage: None, + location: None, + },), + table_properties: vec![], + with_options, + file_format: None, + location: None, + query: None, + without_rowid: false, + like: None, + clone: None, + engine: None, + comment: None, + auto_increment_offset: None, + default_charset: None, + collation: None, + on_commit: None, + on_cluster: None, + primary_key: None, + order_by: None, + partition_by: None, + cluster_by: None, + clustered_by: None, + options: None, + strict: false, + copy_grants: false, + enable_schema_evolution: None, + change_tracking: None, + data_retention_time_in_days: None, + max_data_extension_time_in_days: None, + default_ddl_collation: None, + with_aggregation_policy: None, + with_row_access_policy: None, + with_tags: None, + }), + stmt + ); + + assert_eq!(full_query, stmt.to_string()); + } +} + fn ms() -> TestedDialects { TestedDialects { dialects: vec![Box::new(MsSqlDialect {})], diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 1ebb5d54c..bd39ddf58 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -461,15 +461,15 @@ fn parse_create_table_with_defaults() { assert_eq!( with_options, vec![ - SqlOption { + SqlOption::KeyValue { name: "fillfactor".into(), value: Expr::Value(number("20")) }, - SqlOption { + SqlOption::KeyValue { name: "user_catalog_table".into(), value: Expr::Value(Value::Boolean(true)) }, - SqlOption { + SqlOption::KeyValue { name: "autovacuum_vacuum_threshold".into(), value: Expr::Value(number("100")) }, @@ -4482,11 +4482,11 @@ fn parse_create_table_with_options() { Statement::CreateTable(CreateTable { with_options, .. }) => { assert_eq!( vec![ - SqlOption { + SqlOption::KeyValue { name: "foo".into(), value: Expr::Value(Value::SingleQuotedString("bar".into())), }, - SqlOption { + SqlOption::KeyValue { name: "a".into(), value: Expr::Value(number("123")), }, From 3665d8cb26ce5ff483cff7c6ac24b461c10bd3a9 Mon Sep 17 00:00:00 2001 From: Simon Sawert Date: Mon, 9 Sep 2024 09:20:23 +0200 Subject: [PATCH 2/4] Address PR feedback - Rename T-SQL to MSSQL in docs - Add examples and links to all `SqlOption` - Remove duplicate `parse_options` function - Avoid nested if/else chains - Rename field of `SqlOption::KeyValue` to `key` - Extend testing with function calls, negative tests and using `verified_stmt` - Move parsing of `ASC`|`DESC` to function --- src/ast/mod.rs | 21 ++-- src/parser/mod.rs | 104 ++++++++-------- tests/sqlparser_bigquery.rs | 14 +-- tests/sqlparser_common.rs | 28 ++--- tests/sqlparser_mssql.rs | 236 +++++++++++++++++++++++------------- tests/sqlparser_postgres.rs | 10 +- 6 files changed, 242 insertions(+), 171 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 397c8b5e2..5c98eeb43 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1944,7 +1944,7 @@ pub enum CreateTableOptions { /// /// /// - /// T-sql supports more specific options that's not only key-value pairs. + /// MSSQL supports more specific options that's not only key-value pairs. /// /// WITH ( /// DISTRIBUTION = ROUND_ROBIN, @@ -5656,15 +5656,22 @@ pub enum PartitionRangeDirection { #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum SqlOption { - /// Clustered represents the clustered version of table storage for T-sql. + /// Clustered represents the clustered version of table storage for MSSQL. /// /// Clustered(TableOptionsClustered), - /// Single identifier options, e.g. `HEAP`. + /// Single identifier options, e.g. `HEAP` for MSSQL. + /// + /// Ident(Ident), - /// Any option that consists of a key value pair where the value is an expression. - KeyValue { name: Ident, value: Expr }, - /// One or more table partitions and represents which partition the boundary values belong to. + /// Any option that consists of a key value pair where the value is an expression. e.g. + /// + /// WITH(DISTRIBUTION = ROUND_ROBIN) + KeyValue { key: Ident, value: Expr }, + /// One or more table partitions and represents which partition the boundary values belong to, + /// e.g. + /// + /// PARTITION (id RANGE LEFT FOR VALUES (10, 20, 30, 40)) /// /// Partition { @@ -5681,7 +5688,7 @@ impl fmt::Display for SqlOption { SqlOption::Ident(ident) => { write!(f, "{}", ident) } - SqlOption::KeyValue { name, value } => { + SqlOption::KeyValue { key: name, value } => { write!(f, "{} = {}", name, value) } SqlOption::Partition { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index cdc405c3f..84a96b0de 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -4458,7 +4458,7 @@ impl<'a> Parser<'a> { let name = self.parse_object_name(allow_unquoted_hyphen)?; let columns = self.parse_view_columns()?; let mut options = CreateTableOptions::None; - let with_options = self.parse_table_options(Keyword::WITH)?; + let with_options = self.parse_options(Keyword::WITH)?; if !with_options.is_empty() { options = CreateTableOptions::With(with_options); } @@ -5621,8 +5621,7 @@ impl<'a> Parser<'a> { let clustered_by = self.parse_optional_clustered_by()?; let hive_formats = self.parse_hive_formats()?; // PostgreSQL supports `WITH ( options )`, before `AS` - // T-sql supports `WITH` options for clustering and distribution - let with_options = self.parse_table_options(Keyword::WITH)?; + let with_options = self.parse_options(Keyword::WITH)?; let table_properties = self.parse_options(Keyword::TBLPROPERTIES)?; let engine = if self.parse_keyword(Keyword::ENGINE) { @@ -6400,17 +6399,6 @@ impl<'a> Parser<'a> { Ok(None) } - pub fn parse_table_options(&mut self, keyword: Keyword) -> Result, ParserError> { - if self.parse_keyword(keyword) { - self.expect_token(&Token::LParen)?; - let options = self.parse_comma_separated(Parser::parse_sql_option)?; - self.expect_token(&Token::RParen)?; - Ok(options) - } else { - Ok(vec![]) - } - } - pub fn parse_options(&mut self, keyword: Keyword) -> Result, ParserError> { if self.parse_keyword(keyword) { self.expect_token(&Token::LParen)?; @@ -6505,53 +6493,47 @@ impl<'a> Parser<'a> { } pub fn parse_sql_option(&mut self) -> Result { - let next_token = self.peek_token(); let is_mssql = dialect_of!(self is MsSqlDialect|GenericDialect); - let Token::Word(w) = next_token.token else { - let (name, value) = self.parse_key_value()?; - return Ok(SqlOption::KeyValue { name, value }); - }; - - match w.keyword { - Keyword::HEAP if is_mssql => Ok(SqlOption::Ident(self.parse_identifier(false)?)), - Keyword::PARTITION if is_mssql => self.parse_table_option_partition(), - Keyword::CLUSTERED if is_mssql => self.parse_table_option_clustered(), + match self.peek_token().token { + Token::Word(w) if w.keyword == Keyword::HEAP && is_mssql => { + Ok(SqlOption::Ident(self.parse_identifier(false)?)) + } + Token::Word(w) if w.keyword == Keyword::PARTITION && is_mssql => { + self.parse_option_partition() + } + Token::Word(w) if w.keyword == Keyword::CLUSTERED && is_mssql => { + self.parse_option_clustered() + } _ => { let (name, value) = self.parse_key_value()?; - Ok(SqlOption::KeyValue { name, value }) + Ok(SqlOption::KeyValue { key: name, value }) } } } - pub fn parse_table_option_clustered(&mut self) -> Result { - self.expect_keyword(Keyword::CLUSTERED)?; - - if self.parse_keywords(&[Keyword::COLUMNSTORE, Keyword::INDEX]) { - if self.parse_keyword(Keyword::ORDER) { - Ok(SqlOption::Clustered( - TableOptionsClustered::ColumnstoreIndexOrder( - self.parse_parenthesized_column_list(IsOptional::Mandatory, false)?, - ), - )) - } else { - Ok(SqlOption::Clustered( - TableOptionsClustered::ColumnstoreIndex, - )) - } - } else { - self.expect_keyword(Keyword::INDEX)?; + pub fn parse_option_clustered(&mut self) -> Result { + if self.parse_keywords(&[ + Keyword::CLUSTERED, + Keyword::COLUMNSTORE, + Keyword::INDEX, + Keyword::ORDER, + ]) { + Ok(SqlOption::Clustered( + TableOptionsClustered::ColumnstoreIndexOrder( + self.parse_parenthesized_column_list(IsOptional::Mandatory, false)?, + ), + )) + } else if self.parse_keywords(&[Keyword::CLUSTERED, Keyword::COLUMNSTORE, Keyword::INDEX]) { + Ok(SqlOption::Clustered( + TableOptionsClustered::ColumnstoreIndex, + )) + } else if self.parse_keywords(&[Keyword::CLUSTERED, Keyword::INDEX]) { self.expect_token(&Token::LParen)?; let columns = self.parse_comma_separated(|p| { let name = p.parse_identifier(false)?; - let asc = if p.parse_keyword(Keyword::ASC) { - Some(true) - } else if p.parse_keyword(Keyword::DESC) { - Some(false) - } else { - None - }; + let asc = p.parse_asc(); Ok(ClusteredIndex { name, asc }) })?; @@ -6559,10 +6541,14 @@ impl<'a> Parser<'a> { self.expect_token(&Token::RParen)?; Ok(SqlOption::Clustered(TableOptionsClustered::Index(columns))) + } else { + Err(ParserError::ParserError( + "invalid CLUSTERED sequence".to_string(), + )) } } - pub fn parse_table_option_partition(&mut self) -> Result { + pub fn parse_option_partition(&mut self) -> Result { self.expect_keyword(Keyword::PARTITION)?; self.expect_token(&Token::LParen)?; let column_name = self.parse_identifier(false)?; @@ -11083,17 +11069,23 @@ impl<'a> Parser<'a> { }) } - /// Parse an expression, optionally followed by ASC or DESC (used in ORDER BY) - pub fn parse_order_by_expr(&mut self) -> Result { - let expr = self.parse_expr()?; - - let asc = if self.parse_keyword(Keyword::ASC) { + /// Parsae ASC or DESC, returns Option if ASC or Option if DESC. If token is not + /// one of ASC or DESC, `None` is returned. + pub fn parse_asc(&mut self) -> Option { + if self.parse_keyword(Keyword::ASC) { Some(true) } else if self.parse_keyword(Keyword::DESC) { Some(false) } else { None - }; + } + } + + /// Parse an expression, optionally followed by ASC or DESC (used in ORDER BY) + pub fn parse_order_by_expr(&mut self) -> Result { + let expr = self.parse_expr()?; + + let asc = self.parse_asc(); let nulls_first = if self.parse_keywords(&[Keyword::NULLS, Keyword::FIRST]) { Some(true) diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 6363433f8..6380e632e 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -269,7 +269,7 @@ fn parse_create_view_with_options() { name: Ident::new("age"), data_type: None, options: Some(vec![SqlOption::KeyValue { - name: Ident::new("description"), + key: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString("field age".to_string())), }]) }, @@ -289,7 +289,7 @@ fn parse_create_view_with_options() { }; assert_eq!( &SqlOption::KeyValue { - name: Ident::new("description"), + key: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "a view that expires in 2 days".to_string() )), @@ -416,7 +416,7 @@ fn parse_create_table_with_options() { ColumnOptionDef { name: None, option: ColumnOption::Options(vec![SqlOption::KeyValue { - name: Ident::new("description"), + key: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "field x".to_string() )), @@ -431,7 +431,7 @@ fn parse_create_table_with_options() { options: vec![ColumnOptionDef { name: None, option: ColumnOption::Options(vec![SqlOption::KeyValue { - name: Ident::new("description"), + key: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "field y".to_string() )), @@ -450,11 +450,11 @@ fn parse_create_table_with_options() { ])), Some(vec![ SqlOption::KeyValue { - name: Ident::new("partition_expiration_days"), + key: Ident::new("partition_expiration_days"), value: Expr::Value(number("1")), }, SqlOption::KeyValue { - name: Ident::new("description"), + key: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString( "table option description".to_string() )), @@ -2011,7 +2011,7 @@ fn test_bigquery_create_function() { "42" )))), options: Some(vec![SqlOption::KeyValue { - name: Ident::new("x"), + key: Ident::new("x"), value: Expr::Value(Value::SingleQuotedString("y".into())), }]), behavior: None, diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index f010ef3fc..0a24ea0e4 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -3638,11 +3638,11 @@ fn parse_create_table_with_options() { assert_eq!( vec![ SqlOption::KeyValue { - name: "foo".into(), + key: "foo".into(), value: Expr::Value(Value::SingleQuotedString("bar".into())), }, SqlOption::KeyValue { - name: "a".into(), + key: "a".into(), value: Expr::Value(number("123")), }, ], @@ -3871,7 +3871,7 @@ fn parse_alter_table() { assert_eq!( table_properties, [SqlOption::KeyValue { - name: Ident { + key: Ident { value: "classification".to_string(), quote_style: Some('\'') }, @@ -3959,11 +3959,11 @@ fn parse_alter_view_with_options() { assert_eq!( vec![ SqlOption::KeyValue { - name: "foo".into(), + key: "foo".into(), value: Expr::Value(Value::SingleQuotedString("bar".into())), }, SqlOption::KeyValue { - name: "a".into(), + key: "a".into(), value: Expr::Value(number("123")), }, ], @@ -6531,11 +6531,11 @@ fn parse_create_view_with_options() { assert_eq!( CreateTableOptions::With(vec![ SqlOption::KeyValue { - name: "foo".into(), + key: "foo".into(), value: Expr::Value(Value::SingleQuotedString("bar".into())), }, SqlOption::KeyValue { - name: "a".into(), + key: "a".into(), value: Expr::Value(number("123")), }, ]), @@ -8629,11 +8629,11 @@ fn parse_cache_table() { has_as: false, options: vec![ SqlOption::KeyValue { - name: Ident::with_quote('\'', "K1"), + key: Ident::with_quote('\'', "K1"), value: Expr::Value(Value::SingleQuotedString("V1".into())), }, SqlOption::KeyValue { - name: Ident::with_quote('\'', "K2"), + key: Ident::with_quote('\'', "K2"), value: Expr::Value(number("0.88")), }, ], @@ -8654,11 +8654,11 @@ fn parse_cache_table() { has_as: false, options: vec![ SqlOption::KeyValue { - name: Ident::with_quote('\'', "K1"), + key: Ident::with_quote('\'', "K1"), value: Expr::Value(Value::SingleQuotedString("V1".into())), }, SqlOption::KeyValue { - name: Ident::with_quote('\'', "K2"), + key: Ident::with_quote('\'', "K2"), value: Expr::Value(number("0.88")), }, ], @@ -8679,11 +8679,11 @@ fn parse_cache_table() { has_as: true, options: vec![ SqlOption::KeyValue { - name: Ident::with_quote('\'', "K1"), + key: Ident::with_quote('\'', "K1"), value: Expr::Value(Value::SingleQuotedString("V1".into())), }, SqlOption::KeyValue { - name: Ident::with_quote('\'', "K2"), + key: Ident::with_quote('\'', "K2"), value: Expr::Value(number("0.88")), }, ], @@ -9490,7 +9490,7 @@ fn parse_unload() { quote_style: Some('\'') }, with: vec![SqlOption::KeyValue { - name: Ident { + key: Ident { value: "format".to_string(), quote_style: None }, diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 575a45c22..6890a00c0 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -654,100 +654,144 @@ fn parse_use() { } #[test] -fn parse_create_table_with_options() { +fn parse_create_table_with_valid_options() { let options = [ ( - "PARTITION (column_a RANGE LEFT FOR VALUES (10, 11))", - SqlOption::Partition { - column_name: "column_a".into(), - range_direction: Some(PartitionRangeDirection::Left), - for_values: vec![Expr::Value(test_utils::number("10")), Expr::Value(test_utils::number("11"))] }, - "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = ROUND_ROBIN, PARTITION (column_a RANGE LEFT FOR VALUES (10, 11)))", + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = ROUND_ROBIN, PARTITION (column_a RANGE FOR VALUES (10, 11)))", + vec![ + SqlOption::KeyValue { + key: Ident { + value: "DISTRIBUTION".to_string(), + quote_style: None, + }, + value: Expr::Identifier(Ident { + value: "ROUND_ROBIN".to_string(), + quote_style: None, + }) + }, + SqlOption::Partition { + column_name: "column_a".into(), + range_direction: None, + for_values: vec![Expr::Value(test_utils::number("10")), Expr::Value(test_utils::number("11"))] , + }, + ], ), ( - "HEAP", - SqlOption::Ident("HEAP".into()), - "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = ROUND_ROBIN, HEAP)", - ), + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (PARTITION (column_a RANGE LEFT FOR VALUES (10, 11)))", + vec![ + SqlOption::Partition { + column_name: "column_a".into(), + range_direction: Some(PartitionRangeDirection::Left), + for_values: vec![ + Expr::Value(test_utils::number("10")), + Expr::Value(test_utils::number("11")), + ], + } + ], + ), ( - "CLUSTERED COLUMNSTORE INDEX", - SqlOption::Clustered(TableOptionsClustered::ColumnstoreIndex), - "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = ROUND_ROBIN, CLUSTERED COLUMNSTORE INDEX)", + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED COLUMNSTORE INDEX)", + vec![SqlOption::Clustered(TableOptionsClustered::ColumnstoreIndex)], ), ( - "CLUSTERED COLUMNSTORE INDEX ORDER (column_a, column_b)", - SqlOption::Clustered(TableOptionsClustered::ColumnstoreIndexOrder(vec![ - "column_a".into(), - "column_b".into(), - ])), - "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = ROUND_ROBIN, CLUSTERED COLUMNSTORE INDEX ORDER (column_a, column_b))", + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED COLUMNSTORE INDEX ORDER (column_a, column_b))", + vec![ + SqlOption::Clustered(TableOptionsClustered::ColumnstoreIndexOrder(vec![ + "column_a".into(), + "column_b".into(), + ])) + ], ), ( - "CLUSTERED INDEX (column_a ASC, column_b DESC, column_c)", - SqlOption::Clustered(TableOptionsClustered::Index(vec![ - ClusteredIndex { - name: Ident { - value: "column_a".to_string(), - quote_style: None, - }, - asc: Some(true), - }, - ClusteredIndex { - name: Ident { - value: "column_b".to_string(), - quote_style: None, - }, - asc: Some(false), - }, - ClusteredIndex { - name: Ident { - value: "column_c".to_string(), + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED INDEX (column_a ASC, column_b DESC, column_c))", + vec![ + SqlOption::Clustered(TableOptionsClustered::Index(vec![ + ClusteredIndex { + name: Ident { + value: "column_a".to_string(), + quote_style: None, + }, + asc: Some(true), + }, + ClusteredIndex { + name: Ident { + value: "column_b".to_string(), + quote_style: None, + }, + asc: Some(false), + }, + ClusteredIndex { + name: Ident { + value: "column_c".to_string(), + quote_style: None, + }, + asc: None, + }, + ])) + ], + ), + ( + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = HASH(column_a, column_b), HEAP)", + vec![ + SqlOption::KeyValue { + key: Ident { + value: "DISTRIBUTION".to_string(), quote_style: None, }, - asc: None, + value: Expr::Function( + Function { + name: ObjectName( + vec![ + Ident { + value: "HASH".to_string(), + quote_style: None, + }, + ], + ), + parameters: FunctionArguments::None, + args: FunctionArguments::List( + FunctionArgumentList { + duplicate_treatment: None, + args: vec![ + FunctionArg::Unnamed( + FunctionArgExpr::Expr( + Expr::Identifier( + Ident { + value: "column_a".to_string(), + quote_style: None, + }, + ), + ), + ), + FunctionArg::Unnamed( + FunctionArgExpr::Expr( + Expr::Identifier( + Ident { + value: "column_b".to_string(), + quote_style: None, + }, + ), + ), + ), + ], + clauses: vec![], + }, + ), + filter: None, + null_treatment: None, + over: None, + within_group: vec![], + }, + ), }, - ])), - "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (DISTRIBUTION = ROUND_ROBIN, CLUSTERED INDEX (column_a ASC, column_b DESC, column_c))", - ), + SqlOption::Ident("HEAP".into()), + ], + ), ]; - for (query_placeholder, query_with_option, full_query) in options { - let sql = format!( - " - CREATE TABLE mytable ( - column_a INT, - column_b INT, - column_c INT - ) - WITH ( - DISTRIBUTION = ROUND_ROBIN, - {query_placeholder} - ) - " - ); - - let stmt = ms() - .parse_sql_statements(sql.as_str()) - .unwrap() - .first() - .unwrap() - .to_owned(); - - let with_options = vec![ - SqlOption::KeyValue { - name: Ident { - value: "DISTRIBUTION".to_string(), - quote_style: None, - }, - value: Expr::Identifier(Ident { - value: "ROUND_ROBIN".to_string(), - quote_style: None, - }), - }, - query_with_option, - ]; - + for (sql, with_options) in options { assert_eq!( + ms().verified_stmt(sql), Statement::CreateTable(CreateTable { or_replace: false, temporary: false, @@ -828,11 +872,39 @@ fn parse_create_table_with_options() { with_aggregation_policy: None, with_row_access_policy: None, with_tags: None, - }), - stmt + }) ); + } +} - assert_eq!(full_query, stmt.to_string()); +#[test] +fn parse_create_table_with_invalid_options() { + let invalid_cases = vec![ + ( + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED COLUMNSTORE INDEX ORDER ())", + "Expected: identifier, found: )", + ), + ( + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (CLUSTERED COLUMNSTORE)", + "invalid CLUSTERED sequence", + ), + ( + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (HEAP INDEX)", + "Expected: ), found: INDEX", + ), + ( + + "CREATE TABLE mytable (column_a INT, column_b INT, column_c INT) WITH (PARTITION (RANGE LEFT FOR VALUES (10, 11)))", + "Expected: RANGE, found: LEFT", + ), + ]; + + for (sql, expected_error) in invalid_cases { + let res = ms().parse_sql_statements(sql); + assert_eq!( + format!("sql parser error: {expected_error}"), + res.unwrap_err().to_string() + ); } } diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index bd39ddf58..ec1311f2c 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -462,15 +462,15 @@ fn parse_create_table_with_defaults() { with_options, vec![ SqlOption::KeyValue { - name: "fillfactor".into(), + key: "fillfactor".into(), value: Expr::Value(number("20")) }, SqlOption::KeyValue { - name: "user_catalog_table".into(), + key: "user_catalog_table".into(), value: Expr::Value(Value::Boolean(true)) }, SqlOption::KeyValue { - name: "autovacuum_vacuum_threshold".into(), + key: "autovacuum_vacuum_threshold".into(), value: Expr::Value(number("100")) }, ] @@ -4483,11 +4483,11 @@ fn parse_create_table_with_options() { assert_eq!( vec![ SqlOption::KeyValue { - name: "foo".into(), + key: "foo".into(), value: Expr::Value(Value::SingleQuotedString("bar".into())), }, SqlOption::KeyValue { - name: "a".into(), + key: "a".into(), value: Expr::Value(number("123")), }, ], From 2ad1621c44846ddfa3136354b29d8acdd0374bc9 Mon Sep 17 00:00:00 2001 From: Simon Sawert Date: Mon, 9 Sep 2024 09:35:44 +0200 Subject: [PATCH 3/4] Change docstring to make HTML render work --- src/parser/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 84a96b0de..45041f903 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -11069,8 +11069,8 @@ impl<'a> Parser<'a> { }) } - /// Parsae ASC or DESC, returns Option if ASC or Option if DESC. If token is not - /// one of ASC or DESC, `None` is returned. + /// Parsae ASC or DESC, returns an Option with true if ASC, false of DESC or `None` if none of + /// them. pub fn parse_asc(&mut self) -> Option { if self.parse_keyword(Keyword::ASC) { Some(true) From ed632e872d21ede9da77482b438fad86ee9b52fd Mon Sep 17 00:00:00 2001 From: Simon Sawert Date: Tue, 10 Sep 2024 23:21:15 +0200 Subject: [PATCH 4/4] Update names and fix typos --- src/parser/mod.rs | 21 ++++++++------------- tests/sqlparser_mssql.rs | 4 ++-- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 45041f903..b61a06a60 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6484,14 +6484,6 @@ impl<'a> Parser<'a> { } } - pub fn parse_key_value(&mut self) -> Result<(Ident, Expr), ParserError> { - let name = self.parse_identifier(false)?; - self.expect_token(&Token::Eq)?; - let value = self.parse_expr()?; - - Ok((name, value)) - } - pub fn parse_sql_option(&mut self) -> Result { let is_mssql = dialect_of!(self is MsSqlDialect|GenericDialect); @@ -6506,7 +6498,10 @@ impl<'a> Parser<'a> { self.parse_option_clustered() } _ => { - let (name, value) = self.parse_key_value()?; + let name = self.parse_identifier(false)?; + self.expect_token(&Token::Eq)?; + let value = self.parse_expr()?; + Ok(SqlOption::KeyValue { key: name, value }) } } @@ -6533,7 +6528,7 @@ impl<'a> Parser<'a> { let columns = self.parse_comma_separated(|p| { let name = p.parse_identifier(false)?; - let asc = p.parse_asc(); + let asc = p.parse_asc_desc(); Ok(ClusteredIndex { name, asc }) })?; @@ -11069,9 +11064,9 @@ impl<'a> Parser<'a> { }) } - /// Parsae ASC or DESC, returns an Option with true if ASC, false of DESC or `None` if none of + /// Parse ASC or DESC, returns an Option with true if ASC, false of DESC or `None` if none of /// them. - pub fn parse_asc(&mut self) -> Option { + pub fn parse_asc_desc(&mut self) -> Option { if self.parse_keyword(Keyword::ASC) { Some(true) } else if self.parse_keyword(Keyword::DESC) { @@ -11085,7 +11080,7 @@ impl<'a> Parser<'a> { pub fn parse_order_by_expr(&mut self) -> Result { let expr = self.parse_expr()?; - let asc = self.parse_asc(); + let asc = self.parse_asc_desc(); let nulls_first = if self.parse_keywords(&[Keyword::NULLS, Keyword::FIRST]) { Some(true) diff --git a/tests/sqlparser_mssql.rs b/tests/sqlparser_mssql.rs index 6890a00c0..0ab160f56 100644 --- a/tests/sqlparser_mssql.rs +++ b/tests/sqlparser_mssql.rs @@ -791,7 +791,7 @@ fn parse_create_table_with_valid_options() { for (sql, with_options) in options { assert_eq!( - ms().verified_stmt(sql), + ms_and_generic().verified_stmt(sql), Statement::CreateTable(CreateTable { or_replace: false, temporary: false, @@ -900,7 +900,7 @@ fn parse_create_table_with_invalid_options() { ]; for (sql, expected_error) in invalid_cases { - let res = ms().parse_sql_statements(sql); + let res = ms_and_generic().parse_sql_statements(sql); assert_eq!( format!("sql parser error: {expected_error}"), res.unwrap_err().to_string()