From 619d7c936b3a64227783a9b3d422ca9e7e980644 Mon Sep 17 00:00:00 2001 From: "aleksei.p" Date: Tue, 24 Sep 2024 15:14:34 +0200 Subject: [PATCH 1/6] Snowflake: support of views column descriptions --- src/ast/ddl.rs | 9 +++------ src/ast/mod.rs | 12 ++++++------ src/parser/mod.rs | 23 +++++++++++++++++++---- tests/sqlparser_bigquery.rs | 4 ++-- tests/sqlparser_snowflake.rs | 26 ++++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 18 deletions(-) diff --git a/src/ast/ddl.rs b/src/ast/ddl.rs index b5444b8da..ca8d3a22a 100644 --- a/src/ast/ddl.rs +++ b/src/ast/ddl.rs @@ -992,6 +992,7 @@ impl fmt::Display for ColumnDef { /// ```sql /// name /// age OPTIONS(description = "age column", tag = "prod") +/// amount COMMENT 'The total amount for the order line' /// created_at DateTime64 /// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -1000,7 +1001,7 @@ impl fmt::Display for ColumnDef { pub struct ViewColumnDef { pub name: Ident, pub data_type: Option, - pub options: Option>, + pub options: Option>, } impl fmt::Display for ViewColumnDef { @@ -1010,11 +1011,7 @@ impl fmt::Display for ViewColumnDef { write!(f, " {}", data_type)?; } if let Some(options) = self.options.as_ref() { - write!( - f, - " OPTIONS({})", - display_comma_separated(options.as_slice()) - )?; + write!(f, " {}", display_comma_separated(options.as_slice()))?; } Ok(()) } diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 6c851906c..0d60ab350 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -3831,6 +3831,12 @@ impl fmt::Display for Statement { .map(|to| format!(" TO {to}")) .unwrap_or_default() )?; + if !columns.is_empty() { + write!(f, " ({})", display_comma_separated(columns))?; + } + if matches!(options, CreateTableOptions::With(_)) { + write!(f, " {options}")?; + } if let Some(comment) = comment { write!( f, @@ -3838,12 +3844,6 @@ impl fmt::Display for Statement { value::escape_single_quote_string(comment) )?; } - if matches!(options, CreateTableOptions::With(_)) { - write!(f, " {options}")?; - } - if !columns.is_empty() { - write!(f, " ({})", display_comma_separated(columns))?; - } if !cluster_by.is_empty() { write!(f, " CLUSTER BY ({})", display_comma_separated(cluster_by))?; } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e2f4dd508..104fc2e73 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8192,19 +8192,34 @@ impl<'a> Parser<'a> { /// Parses a column definition within a view. fn parse_view_column(&mut self) -> Result { let name = self.parse_identifier(false)?; - let options = if dialect_of!(self is BigQueryDialect | GenericDialect) + let mut options = vec![]; + if dialect_of!(self is BigQueryDialect | GenericDialect) && self.parse_keyword(Keyword::OPTIONS) { self.prev_token(); - Some(self.parse_options(Keyword::OPTIONS)?) - } else { - None + let option = ColumnOption::Options(self.parse_options(Keyword::OPTIONS)?); + options.push(option); + }; + if dialect_of!(self is SnowflakeDialect | GenericDialect) + && self.parse_keyword(Keyword::COMMENT) + { + let next_token = self.next_token(); + let option = match next_token.token { + Token::SingleQuotedString(str) => ColumnOption::Comment(str), + _ => self.expected("string literal", next_token)?, + }; + options.push(option); }; let data_type = if dialect_of!(self is ClickHouseDialect) { Some(self.parse_data_type()?) } else { None }; + let options = if !options.is_empty() { + Some(options) + } else { + None + }; Ok(ViewColumnDef { name, data_type, diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index e051baa8b..0dc6a529c 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -267,10 +267,10 @@ fn parse_create_view_with_options() { ViewColumnDef { name: Ident::new("age"), data_type: None, - options: Some(vec![SqlOption::KeyValue { + options: Some(vec![ColumnOption::Options(vec![SqlOption::KeyValue { key: Ident::new("description"), value: Expr::Value(Value::DoubleQuotedString("field age".to_string())), - }]) + }])]), }, ], columns diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index d0876fc50..81a5cc7f6 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2400,3 +2400,29 @@ fn parse_use() { ); } } + +#[test] +fn parse_view_column_descriptions() { + let sql = "CREATE OR REPLACE VIEW v (a COMMENT 'Comment of field') COMMENT = 'Comment of view' AS SELECT a FROM table1 AS o"; + + match snowflake_and_generic().verified_stmt(sql) { + Statement::CreateView { + name, + columns, + comment, + .. + } => { + assert_eq!(name.to_string(), "v"); + assert_eq!(comment, Some("Comment of view".to_string())); + assert_eq!( + columns, + vec![ViewColumnDef { + name: Ident::new("a"), + data_type: None, + options: Some(vec![ColumnOption::Comment("Comment of field".to_string())]), + },] + ); + } + _ => unreachable!(), + }; +} From 4cb10c0a1b7436c5bdfd6a70888e6b7bda31751b Mon Sep 17 00:00:00 2001 From: "aleksei.p" Date: Wed, 25 Sep 2024 07:57:06 +0200 Subject: [PATCH 2/6] update --- src/parser/mod.rs | 23 +++++++++++------------ tests/sqlparser_snowflake.rs | 34 +++++++++++++++++++++------------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 104fc2e73..d623e882d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8197,26 +8197,25 @@ impl<'a> Parser<'a> { && self.parse_keyword(Keyword::OPTIONS) { self.prev_token(); - let option = ColumnOption::Options(self.parse_options(Keyword::OPTIONS)?); - options.push(option); + if let Some(option) = self.parse_optional_column_option()? { + options.push(option); + } }; if dialect_of!(self is SnowflakeDialect | GenericDialect) && self.parse_keyword(Keyword::COMMENT) { - let next_token = self.next_token(); - let option = match next_token.token { - Token::SingleQuotedString(str) => ColumnOption::Comment(str), - _ => self.expected("string literal", next_token)?, - }; - options.push(option); + self.prev_token(); + if let Some(option) = self.parse_optional_column_option()? { + options.push(option); + } }; - let data_type = if dialect_of!(self is ClickHouseDialect) { - Some(self.parse_data_type()?) + let options = if !options.is_empty() { + Some(options) } else { None }; - let options = if !options.is_empty() { - Some(options) + let data_type = if dialect_of!(self is ClickHouseDialect) { + Some(self.parse_data_type()?) } else { None }; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 81a5cc7f6..15ce08b2a 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2401,26 +2401,34 @@ fn parse_use() { } } +#[test] +fn view_comment_option_should_be_after_column_list() { + snowflake_and_generic() + .verified_stmt("CREATE OR REPLACE VIEW v (a) COMMENT = 'Comment' AS SELECT a FROM t"); +} + #[test] fn parse_view_column_descriptions() { - let sql = "CREATE OR REPLACE VIEW v (a COMMENT 'Comment of field') COMMENT = 'Comment of view' AS SELECT a FROM table1 AS o"; + let sql = + "CREATE OR REPLACE VIEW v (a COMMENT 'Comment', b) AS SELECT a, b FROM table1"; match snowflake_and_generic().verified_stmt(sql) { - Statement::CreateView { - name, - columns, - comment, - .. - } => { + Statement::CreateView { name, columns, .. } => { assert_eq!(name.to_string(), "v"); - assert_eq!(comment, Some("Comment of view".to_string())); assert_eq!( columns, - vec![ViewColumnDef { - name: Ident::new("a"), - data_type: None, - options: Some(vec![ColumnOption::Comment("Comment of field".to_string())]), - },] + vec![ + ViewColumnDef { + name: Ident::new("a"), + data_type: None, + options: Some(vec![ColumnOption::Comment("Comment".to_string())]), + }, + ViewColumnDef { + name: Ident::new("b"), + data_type: None, + options: None, + } + ] ); } _ => unreachable!(), From c705e407ce1b84aa46050a66b6b302ff78598c73 Mon Sep 17 00:00:00 2001 From: "aleksei.p" Date: Wed, 25 Sep 2024 07:59:39 +0200 Subject: [PATCH 3/6] update --- tests/sqlparser_snowflake.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 15ce08b2a..5b33fd80b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2409,8 +2409,7 @@ fn view_comment_option_should_be_after_column_list() { #[test] fn parse_view_column_descriptions() { - let sql = - "CREATE OR REPLACE VIEW v (a COMMENT 'Comment', b) AS SELECT a, b FROM table1"; + let sql = "CREATE OR REPLACE VIEW v (a COMMENT 'Comment', b) AS SELECT a, b FROM table1"; match snowflake_and_generic().verified_stmt(sql) { Statement::CreateView { name, columns, .. } => { From 85d09b5284e2da2df4fd23edf4a732add4fb1612 Mon Sep 17 00:00:00 2001 From: "aleksei.p" Date: Mon, 30 Sep 2024 09:16:37 +0200 Subject: [PATCH 4/6] update --- src/parser/mod.rs | 14 ++++---------- tests/sqlparser_snowflake.rs | 9 +++++++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d623e882d..01e516593 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8193,16 +8193,10 @@ impl<'a> Parser<'a> { fn parse_view_column(&mut self) -> Result { let name = self.parse_identifier(false)?; let mut options = vec![]; - if dialect_of!(self is BigQueryDialect | GenericDialect) - && self.parse_keyword(Keyword::OPTIONS) - { - self.prev_token(); - if let Some(option) = self.parse_optional_column_option()? { - options.push(option); - } - }; - if dialect_of!(self is SnowflakeDialect | GenericDialect) - && self.parse_keyword(Keyword::COMMENT) + if (dialect_of!(self is BigQueryDialect | GenericDialect) + && self.parse_keyword(Keyword::OPTIONS)) + || (dialect_of!(self is SnowflakeDialect | GenericDialect) + && self.parse_keyword(Keyword::COMMENT)) { self.prev_token(); if let Some(option) = self.parse_optional_column_option()? { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 5b33fd80b..2cf0aee69 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2403,8 +2403,13 @@ fn parse_use() { #[test] fn view_comment_option_should_be_after_column_list() { - snowflake_and_generic() - .verified_stmt("CREATE OR REPLACE VIEW v (a) COMMENT = 'Comment' AS SELECT a FROM t"); + for sql in [ + "CREATE OR REPLACE VIEW v (a) COMMENT = 'Comment' AS SELECT a FROM t", + "CREATE OR REPLACE VIEW v (a COMMENT 'a comment', b, c COMMENT 'c comment') COMMENT = 'Comment' AS SELECT a FROM t", + ] { + snowflake_and_generic() + .verified_stmt(sql); + } } #[test] From 80f7aea8a880ab314d6fe63e6c0674677752ab2e Mon Sep 17 00:00:00 2001 From: "aleksei.p" Date: Fri, 4 Oct 2024 14:13:59 +0200 Subject: [PATCH 5/6] update --- src/parser/mod.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 01e516593..d925832df 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8192,19 +8192,14 @@ impl<'a> Parser<'a> { /// Parses a column definition within a view. fn parse_view_column(&mut self) -> Result { let name = self.parse_identifier(false)?; - let mut options = vec![]; - if (dialect_of!(self is BigQueryDialect | GenericDialect) + let options = if (dialect_of!(self is BigQueryDialect | GenericDialect) && self.parse_keyword(Keyword::OPTIONS)) || (dialect_of!(self is SnowflakeDialect | GenericDialect) && self.parse_keyword(Keyword::COMMENT)) { self.prev_token(); - if let Some(option) = self.parse_optional_column_option()? { - options.push(option); - } - }; - let options = if !options.is_empty() { - Some(options) + self.parse_optional_column_option()? + .map(|option| vec![option]) } else { None }; From 8b2c1218ebb232b4b3cb72d1bafe1056b51f54c3 Mon Sep 17 00:00:00 2001 From: "aleksei.p" Date: Fri, 4 Oct 2024 16:24:05 +0200 Subject: [PATCH 6/6] update test --- tests/sqlparser_snowflake.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 2cf0aee69..903a21854 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2406,6 +2406,7 @@ fn view_comment_option_should_be_after_column_list() { for sql in [ "CREATE OR REPLACE VIEW v (a) COMMENT = 'Comment' AS SELECT a FROM t", "CREATE OR REPLACE VIEW v (a COMMENT 'a comment', b, c COMMENT 'c comment') COMMENT = 'Comment' AS SELECT a FROM t", + "CREATE OR REPLACE VIEW v (a COMMENT 'a comment', b, c COMMENT 'c comment') WITH (foo = bar) COMMENT = 'Comment' AS SELECT a FROM t", ] { snowflake_and_generic() .verified_stmt(sql);