From 6ad7811eb03c0e3dc8e967d7908dd14173de9897 Mon Sep 17 00:00:00 2001 From: "aleksei.p" Date: Tue, 4 Mar 2025 15:19:44 +0100 Subject: [PATCH 1/6] Snowflake: Support dollar quoted comment of table, view and field --- src/dialect/snowflake.rs | 11 ++++++++++- src/parser/mod.rs | 27 +++++++++++++++++---------- tests/sqlparser_snowflake.rs | 21 +++++++++++++++++++++ 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 72252b277..635aa73f7 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -245,6 +245,14 @@ impl Dialect for SnowflakeDialect { .map(|p| Some(ColumnOption::Policy(ColumnPolicy::ProjectionPolicy(p))))) } else if parser.parse_keywords(&[Keyword::TAG]) { Ok(parse_column_tags(parser, with).map(|p| Some(ColumnOption::Tags(p)))) + } else if parser.parse_keywords(&[Keyword::COMMENT]) { + let next_token = parser.next_token(); + match next_token.token { + Token::DollarQuotedString(value, ..) => { + Ok(Ok(Some(ColumnOption::Comment(value.value)))) + } + _ => Err(ParserError::ParserError("not found match".to_string())), + } } else { Err(ParserError::ParserError("not found match".to_string())) } @@ -422,7 +430,7 @@ pub fn parse_create_table( Keyword::COMMENT => { // Rewind the COMMENT keyword parser.prev_token(); - builder = builder.comment(parser.parse_optional_inline_comment()?); + builder = builder.comment(parser.parse_optional_inline_comment(true)?); } Keyword::AS => { let query = parser.parse_query()?; @@ -646,6 +654,7 @@ pub fn parse_create_stage( parser.expect_token(&Token::Eq)?; comment = Some(match parser.next_token().token { Token::SingleQuotedString(word) => Ok(word), + Token::DollarQuotedString(word) => Ok(word.value), _ => parser.expected("a comment statement", parser.peek_token()), }?) } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index f234fcc07..ed00fe14d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5323,6 +5323,7 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match next_token.token { Token::SingleQuotedString(str) => Some(str), + Token::DollarQuotedString(str) => Some(str.value), _ => self.expected("string literal", next_token)?, } } else { @@ -5764,7 +5765,7 @@ impl<'a> Parser<'a> { None }; - let comment = self.parse_optional_inline_comment()?; + let comment = self.parse_optional_inline_comment(false)?; let with_dcproperties = match self.parse_options_with_keywords(&[Keyword::WITH, Keyword::DCPROPERTIES])? { @@ -6821,7 +6822,8 @@ impl<'a> Parser<'a> { if !dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::COMMENT) { // rewind the COMMENT keyword self.prev_token(); - comment = self.parse_optional_inline_comment()? + let support_dollar_quoted_comment = dialect_of!(self is SnowflakeDialect); + comment = self.parse_optional_inline_comment(support_dollar_quoted_comment)? }; // Parse optional `AS ( query )` @@ -6922,18 +6924,23 @@ impl<'a> Parser<'a> { }) } - pub fn parse_optional_inline_comment(&mut self) -> Result, ParserError> { + pub fn parse_optional_inline_comment( + &mut self, + support_dollar_quoted_comment: bool, + ) -> Result, ParserError> { let comment = if self.parse_keyword(Keyword::COMMENT) { let has_eq = self.consume_token(&Token::Eq); let next_token = self.next_token(); - match next_token.token { - Token::SingleQuotedString(str) => Some(if has_eq { - CommentDef::WithEq(str) - } else { - CommentDef::WithoutEq(str) - }), + let comment = match next_token.token { + Token::SingleQuotedString(str) => str, + Token::DollarQuotedString(str) if support_dollar_quoted_comment => str.value, _ => self.expected("comment", next_token)?, - } + }; + Some(if has_eq { + CommentDef::WithEq(comment) + } else { + CommentDef::WithoutEq(comment) + }) } else { None }; diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index b1d31e6dd..8185aba30 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -976,6 +976,27 @@ fn parse_sf_create_or_replace_with_comment_for_snowflake() { } } +#[test] +fn parse_sf_create_table_or_view_with_dollars_comment() { + assert!(snowflake() + .parse_sql_statements( + r#"CREATE OR REPLACE TEMPORARY VIEW foo.bar.baz ( + "COL_1" COMMENT $$comment 1$$ + ) COMMENT = $$view comment$$ AS ( + SELECT 1 + )"# + ) + .is_ok()); + + assert!(snowflake() + .parse_sql_statements( + r#"CREATE TABLE my_table ( + a STRING COMMENT $$comment 1$$ + ) COMMENT = $$table comment$$"# + ) + .is_ok()); +} + #[test] fn test_sf_derived_table_in_parenthesis() { // Nesting a subquery in an extra set of parentheses is non-standard, From 24a2968b33787626fe63fc626c19dbfb50ec48f5 Mon Sep 17 00:00:00 2001 From: "aleksei.p" Date: Tue, 4 Mar 2025 15:30:36 +0100 Subject: [PATCH 2/6] update a test name --- tests/sqlparser_snowflake.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 8185aba30..50c36b4a6 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -977,7 +977,7 @@ fn parse_sf_create_or_replace_with_comment_for_snowflake() { } #[test] -fn parse_sf_create_table_or_view_with_dollars_comment() { +fn parse_sf_create_table_or_view_with_dollar_quoted_comment() { assert!(snowflake() .parse_sql_statements( r#"CREATE OR REPLACE TEMPORARY VIEW foo.bar.baz ( From eaf66bc63e70865c0b40129d5bfe8687916b27d3 Mon Sep 17 00:00:00 2001 From: "aleksei.p" Date: Thu, 13 Mar 2025 18:41:42 +0100 Subject: [PATCH 3/6] update --- src/dialect/snowflake.rs | 10 +--------- src/parser/mod.rs | 15 +++++++-------- tests/sqlparser_snowflake.rs | 26 ++++++++++---------------- 3 files changed, 18 insertions(+), 33 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index 635aa73f7..eb7a24413 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -245,14 +245,6 @@ impl Dialect for SnowflakeDialect { .map(|p| Some(ColumnOption::Policy(ColumnPolicy::ProjectionPolicy(p))))) } else if parser.parse_keywords(&[Keyword::TAG]) { Ok(parse_column_tags(parser, with).map(|p| Some(ColumnOption::Tags(p)))) - } else if parser.parse_keywords(&[Keyword::COMMENT]) { - let next_token = parser.next_token(); - match next_token.token { - Token::DollarQuotedString(value, ..) => { - Ok(Ok(Some(ColumnOption::Comment(value.value)))) - } - _ => Err(ParserError::ParserError("not found match".to_string())), - } } else { Err(ParserError::ParserError("not found match".to_string())) } @@ -430,7 +422,7 @@ pub fn parse_create_table( Keyword::COMMENT => { // Rewind the COMMENT keyword parser.prev_token(); - builder = builder.comment(parser.parse_optional_inline_comment(true)?); + builder = builder.comment(parser.parse_optional_inline_comment()?); } Keyword::AS => { let query = parser.parse_query()?; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ed00fe14d..7d01fec3a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5765,7 +5765,7 @@ impl<'a> Parser<'a> { None }; - let comment = self.parse_optional_inline_comment(false)?; + let comment = self.parse_optional_inline_comment()?; let with_dcproperties = match self.parse_options_with_keywords(&[Keyword::WITH, Keyword::DCPROPERTIES])? { @@ -6822,8 +6822,7 @@ impl<'a> Parser<'a> { if !dialect_of!(self is HiveDialect) && self.parse_keyword(Keyword::COMMENT) { // rewind the COMMENT keyword self.prev_token(); - let support_dollar_quoted_comment = dialect_of!(self is SnowflakeDialect); - comment = self.parse_optional_inline_comment(support_dollar_quoted_comment)? + comment = self.parse_optional_inline_comment()? }; // Parse optional `AS ( query )` @@ -6924,16 +6923,13 @@ impl<'a> Parser<'a> { }) } - pub fn parse_optional_inline_comment( - &mut self, - support_dollar_quoted_comment: bool, - ) -> Result, ParserError> { + pub fn parse_optional_inline_comment(&mut self) -> Result, ParserError> { let comment = if self.parse_keyword(Keyword::COMMENT) { let has_eq = self.consume_token(&Token::Eq); let next_token = self.next_token(); let comment = match next_token.token { Token::SingleQuotedString(str) => str, - Token::DollarQuotedString(str) if support_dollar_quoted_comment => str.value, + Token::DollarQuotedString(str) => str.value, _ => self.expected("comment", next_token)?, }; Some(if has_eq { @@ -7085,6 +7081,9 @@ impl<'a> Parser<'a> { let next_token = self.next_token(); match next_token.token { Token::SingleQuotedString(value, ..) => Ok(Some(ColumnOption::Comment(value))), + Token::DollarQuotedString(value, ..) => { + Ok(Some(ColumnOption::Comment(value.value))) + } _ => self.expected("string", next_token), } } else if self.parse_keyword(Keyword::NULL) { diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index 50c36b4a6..f37b657e5 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -978,23 +978,17 @@ fn parse_sf_create_or_replace_with_comment_for_snowflake() { #[test] fn parse_sf_create_table_or_view_with_dollar_quoted_comment() { - assert!(snowflake() - .parse_sql_statements( - r#"CREATE OR REPLACE TEMPORARY VIEW foo.bar.baz ( - "COL_1" COMMENT $$comment 1$$ - ) COMMENT = $$view comment$$ AS ( - SELECT 1 - )"# - ) - .is_ok()); + // Snowflake transforms dollar quoted comments into a common comment in DDL representation of creation + snowflake() + .one_statement_parses_to( + r#"CREATE OR REPLACE TEMPORARY VIEW foo.bar.baz ("COL_1" COMMENT $$comment 1$$) COMMENT = $$view comment$$ AS (SELECT 1)"#, + r#"CREATE OR REPLACE TEMPORARY VIEW foo.bar.baz ("COL_1" COMMENT 'comment 1') COMMENT = 'view comment' AS (SELECT 1)"# + ); - assert!(snowflake() - .parse_sql_statements( - r#"CREATE TABLE my_table ( - a STRING COMMENT $$comment 1$$ - ) COMMENT = $$table comment$$"# - ) - .is_ok()); + snowflake().one_statement_parses_to( + r#"CREATE TABLE my_table (a STRING COMMENT $$comment 1$$) COMMENT = $$table comment$$"#, + r#"CREATE TABLE my_table (a STRING COMMENT 'comment 1') COMMENT = 'table comment'"#, + ); } #[test] From 32549538f4823b1cb42fcc517001e326d4d78e25 Mon Sep 17 00:00:00 2001 From: "aleksei.p" Date: Fri, 14 Mar 2025 17:11:55 +0100 Subject: [PATCH 4/6] update --- src/parser/mod.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7d01fec3a..73865fe1b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5320,12 +5320,7 @@ impl<'a> Parser<'a> { && self.parse_keyword(Keyword::COMMENT) { self.expect_token(&Token::Eq)?; - let next_token = self.next_token(); - match next_token.token { - Token::SingleQuotedString(str) => Some(str), - Token::DollarQuotedString(str) => Some(str.value), - _ => self.expected("string literal", next_token)?, - } + Some(self.parse_comment_value()?) } else { None }; @@ -6926,12 +6921,7 @@ impl<'a> Parser<'a> { pub fn parse_optional_inline_comment(&mut self) -> Result, ParserError> { let comment = if self.parse_keyword(Keyword::COMMENT) { let has_eq = self.consume_token(&Token::Eq); - let next_token = self.next_token(); - let comment = match next_token.token { - Token::SingleQuotedString(str) => str, - Token::DollarQuotedString(str) => str.value, - _ => self.expected("comment", next_token)?, - }; + let comment = self.parse_comment_value()?; Some(if has_eq { CommentDef::WithEq(comment) } else { @@ -6943,6 +6933,16 @@ impl<'a> Parser<'a> { Ok(comment) } + fn parse_comment_value(&mut self) -> Result { + let next_token = self.next_token(); + let value = match next_token.token { + Token::SingleQuotedString(str) => str, + Token::DollarQuotedString(str) => str.value, + _ => self.expected("string literal", next_token)?, + }; + Ok(value) + } + pub fn parse_optional_procedure_parameters( &mut self, ) -> Result>, ParserError> { From 190b44ad4ca4f10c72283d270380766a5b0dae79 Mon Sep 17 00:00:00 2001 From: "aleksei.p" Date: Fri, 14 Mar 2025 17:16:13 +0100 Subject: [PATCH 5/6] update --- src/parser/mod.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 73865fe1b..5a7753828 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -7078,14 +7078,7 @@ impl<'a> Parser<'a> { } else if self.parse_keywords(&[Keyword::NOT, Keyword::NULL]) { Ok(Some(ColumnOption::NotNull)) } else if self.parse_keywords(&[Keyword::COMMENT]) { - let next_token = self.next_token(); - match next_token.token { - Token::SingleQuotedString(value, ..) => Ok(Some(ColumnOption::Comment(value))), - Token::DollarQuotedString(value, ..) => { - Ok(Some(ColumnOption::Comment(value.value))) - } - _ => self.expected("string", next_token), - } + Ok(Some(ColumnOption::Comment(self.parse_comment_value()?))) } else if self.parse_keyword(Keyword::NULL) { Ok(Some(ColumnOption::Null)) } else if self.parse_keyword(Keyword::DEFAULT) { From 455e164610d368fdb5c0eb3a5c1fff1b2e0c28ea Mon Sep 17 00:00:00 2001 From: "aleksei.p" Date: Fri, 14 Mar 2025 17:18:15 +0100 Subject: [PATCH 6/6] update --- src/dialect/snowflake.rs | 6 +----- src/parser/mod.rs | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/dialect/snowflake.rs b/src/dialect/snowflake.rs index eb7a24413..09a0e57ce 100644 --- a/src/dialect/snowflake.rs +++ b/src/dialect/snowflake.rs @@ -644,11 +644,7 @@ pub fn parse_create_stage( // [ comment ] if parser.parse_keyword(Keyword::COMMENT) { parser.expect_token(&Token::Eq)?; - comment = Some(match parser.next_token().token { - Token::SingleQuotedString(word) => Ok(word), - Token::DollarQuotedString(word) => Ok(word.value), - _ => parser.expected("a comment statement", parser.peek_token()), - }?) + comment = Some(parser.parse_comment_value()?); } Ok(Statement::CreateStage { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 5a7753828..cf127b9dd 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6933,7 +6933,7 @@ impl<'a> Parser<'a> { Ok(comment) } - fn parse_comment_value(&mut self) -> Result { + pub fn parse_comment_value(&mut self) -> Result { let next_token = self.next_token(); let value = match next_token.token { Token::SingleQuotedString(str) => str,