From 9a28c7dbd0a1e51d0943c8b30c5f10010585e8e5 Mon Sep 17 00:00:00 2001 From: Zdenko Nevrala Date: Fri, 22 Sep 2023 13:24:41 +0200 Subject: [PATCH 1/4] Support Snowflake TRIM. --- src/ast/mod.rs | 6 ++++++ src/parser/mod.rs | 12 ++++++++++++ tests/sqlparser_common.rs | 24 ++++++++++++++++++++++++ tests/sqlparser_snowflake.rs | 25 +++++++++++++++++++++++++ 4 files changed, 67 insertions(+) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index eb8830bb1..fa363aa48 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -496,12 +496,14 @@ pub enum Expr { /// ```sql /// TRIM([BOTH | LEADING | TRAILING] [ FROM] ) /// TRIM() + /// TRIM(, [, characters]) -- only Snowflake /// ``` Trim { expr: Box, // ([BOTH | LEADING | TRAILING] trim_where: Option, trim_what: Option>, + trim_characters: Option>, }, /// ```sql /// OVERLAY( PLACING FROM [ FOR ] @@ -895,6 +897,7 @@ impl fmt::Display for Expr { expr, trim_where, trim_what, + trim_characters } => { write!(f, "TRIM(")?; if let Some(ident) = trim_where { @@ -905,6 +908,9 @@ impl fmt::Display for Expr { } else { write!(f, "{expr}")?; } + if let Some(characters) = trim_characters { + write!(f, ", {}", display_comma_separated(characters))?; + } write!(f, ")") } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ba8f5784f..1bc28ac22 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1302,6 +1302,7 @@ impl<'a> Parser<'a> { /// ```sql /// TRIM ([WHERE] ['text' FROM] 'text') /// TRIM ('text') + /// TRIM(, [, characters]) -- only Snowflake /// ``` pub fn parse_trim_expr(&mut self) -> Result { self.expect_token(&Token::LParen)?; @@ -1323,6 +1324,16 @@ impl<'a> Parser<'a> { expr: Box::new(expr), trim_where, trim_what: Some(trim_what), + trim_characters: None, + }) + } else if self.consume_token(&Token::Comma) && dialect_of!(self is SnowflakeDialect) { + let characters = self.parse_comma_separated(Parser::parse_expr)?; + self.expect_token(&Token::RParen)?; + Ok(Expr::Trim { + expr: Box::new(expr), + trim_where: None, + trim_what: None, + trim_characters: Some(characters), }) } else { self.expect_token(&Token::RParen)?; @@ -1330,6 +1341,7 @@ impl<'a> Parser<'a> { expr: Box::new(expr), trim_where, trim_what: None, + trim_characters: None, }) } } diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 6f780de9e..016012dbb 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5215,6 +5215,30 @@ fn parse_trim() { ParserError::ParserError("Expected ), found: 'xyz'".to_owned()), parse_sql_statements("SELECT TRIM(FOO 'xyz' FROM 'xyzfooxyz')").unwrap_err() ); + + //keep Snowflake TRIM syntax failing + let all_expected_snowflake = TestedDialects { + dialects: vec![ + Box::new(GenericDialect {}), + Box::new(PostgreSqlDialect {}), + Box::new(MsSqlDialect {}), + Box::new(AnsiDialect {}), + //Box::new(SnowflakeDialect {}), + Box::new(HiveDialect {}), + Box::new(RedshiftSqlDialect {}), + Box::new(MySqlDialect {}), + Box::new(BigQueryDialect {}), + Box::new(SQLiteDialect {}), + Box::new(DuckDbDialect {}), + ], + options: None, + }; + assert_eq!( + ParserError::ParserError("Expected ), found: 'a'\nNear `SELECT TRIM('xyz',`".to_owned()), + all_expected_snowflake + .parse_sql_statements("SELECT TRIM('xyz', 'a')") + .unwrap_err() + ); } #[test] diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index e1db7ec61..a639b5861 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1039,3 +1039,28 @@ fn test_snowflake_stage_object_names() { } } } + +#[test] +fn test_snowflake_trim() { + let real_sql = r#"SELECT customer_id, TRIM(sub_items.value:item_price_id, '"', "a") AS item_price_id FROM models_staging.subscriptions"#; + assert_eq!(snowflake().verified_stmt(real_sql).to_string(), real_sql); + + let sql_only_select = "SELECT TRIM('xyz', 'a')"; + let select = snowflake().verified_only_select(sql_only_select); + assert_eq!( + &Expr::Trim { + expr: Box::new(Expr::Value(Value::SingleQuotedString("xyz".to_owned()))), + trim_where: None, + trim_what: None, + trim_characters: Some(vec![Expr::Value(Value::SingleQuotedString("a".to_owned()))]), + }, + expr_from_projection(only(&select.projection)) + ); + + // missing comma separation + let error_sql = "SELECT TRIM('xyz' 'a')"; + assert_eq!( + ParserError::ParserError("Expected ), found: 'a'\nNear `SELECT TRIM('xyz'`".to_owned()), + snowflake().parse_sql_statements(error_sql).unwrap_err() + ); +} From de2ddb53910e3132a57005a1b0d81ae27ca86491 Mon Sep 17 00:00:00 2001 From: Zdenko Nevrala Date: Wed, 4 Oct 2023 08:30:28 +0200 Subject: [PATCH 2/4] Support syntax also for BigQuery. --- src/ast/mod.rs | 2 +- src/parser/mod.rs | 6 ++++-- tests/sqlparser_bigquery.rs | 26 ++++++++++++++++++++++++++ tests/sqlparser_common.rs | 6 +++--- tests/sqlparser_snowflake.rs | 2 +- 5 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index fa363aa48..182f14b3b 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -897,7 +897,7 @@ impl fmt::Display for Expr { expr, trim_where, trim_what, - trim_characters + trim_characters, } => { write!(f, "TRIM(")?; if let Some(ident) = trim_where { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1bc28ac22..21460b962 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1302,7 +1302,7 @@ impl<'a> Parser<'a> { /// ```sql /// TRIM ([WHERE] ['text' FROM] 'text') /// TRIM ('text') - /// TRIM(, [, characters]) -- only Snowflake + /// TRIM(, [, characters]) -- only Snowflake or BigQuery /// ``` pub fn parse_trim_expr(&mut self) -> Result { self.expect_token(&Token::LParen)?; @@ -1326,7 +1326,9 @@ impl<'a> Parser<'a> { trim_what: Some(trim_what), trim_characters: None, }) - } else if self.consume_token(&Token::Comma) && dialect_of!(self is SnowflakeDialect) { + } else if self.consume_token(&Token::Comma) + && dialect_of!(self is SnowflakeDialect | BigQueryDialect) + { let characters = self.parse_comma_separated(Parser::parse_expr)?; self.expect_token(&Token::RParen)?; Ok(Expr::Trim { diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index 3502d7dfa..90f721a25 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -15,6 +15,7 @@ mod test_utils; use sqlparser::ast::*; use sqlparser::dialect::{BigQueryDialect, GenericDialect}; +use sqlparser::parser::ParserError; use test_utils::*; #[test] @@ -464,3 +465,28 @@ fn parse_map_access_offset() { bigquery().verified_only_select(sql); } } + +#[test] +fn test_bigquery_trim() { + let real_sql = r#"SELECT customer_id, TRIM(item_price_id, '"', "a") AS item_price_id FROM models_staging.subscriptions"#; + assert_eq!(bigquery().verified_stmt(real_sql).to_string(), real_sql); + + let sql_only_select = "SELECT TRIM('xyz', 'a')"; + let select = bigquery().verified_only_select(sql_only_select); + assert_eq!( + &Expr::Trim { + expr: Box::new(Expr::Value(Value::SingleQuotedString("xyz".to_owned()))), + trim_where: None, + trim_what: None, + trim_characters: Some(vec![Expr::Value(Value::SingleQuotedString("a".to_owned()))]), + }, + expr_from_projection(only(&select.projection)) + ); + + // missing comma separation + let error_sql = "SELECT TRIM('xyz' 'a')"; + assert_eq!( + ParserError::ParserError("Expected ), found: 'a'".to_owned()), + bigquery().parse_sql_statements(error_sql).unwrap_err() + ); +} diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 016012dbb..9c85cfb1c 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5216,7 +5216,7 @@ fn parse_trim() { parse_sql_statements("SELECT TRIM(FOO 'xyz' FROM 'xyzfooxyz')").unwrap_err() ); - //keep Snowflake TRIM syntax failing + //keep Snowflake/BigQuery TRIM syntax failing let all_expected_snowflake = TestedDialects { dialects: vec![ Box::new(GenericDialect {}), @@ -5227,14 +5227,14 @@ fn parse_trim() { Box::new(HiveDialect {}), Box::new(RedshiftSqlDialect {}), Box::new(MySqlDialect {}), - Box::new(BigQueryDialect {}), + //Box::new(BigQueryDialect {}), Box::new(SQLiteDialect {}), Box::new(DuckDbDialect {}), ], options: None, }; assert_eq!( - ParserError::ParserError("Expected ), found: 'a'\nNear `SELECT TRIM('xyz',`".to_owned()), + ParserError::ParserError("Expected ), found: 'a'".to_owned()), all_expected_snowflake .parse_sql_statements("SELECT TRIM('xyz', 'a')") .unwrap_err() diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index a639b5861..e92656d0b 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -1060,7 +1060,7 @@ fn test_snowflake_trim() { // missing comma separation let error_sql = "SELECT TRIM('xyz' 'a')"; assert_eq!( - ParserError::ParserError("Expected ), found: 'a'\nNear `SELECT TRIM('xyz'`".to_owned()), + ParserError::ParserError("Expected ), found: 'a'".to_owned()), snowflake().parse_sql_statements(error_sql).unwrap_err() ); } From 9b86032bd0acb6b3000d8a3ae12921660a50610a Mon Sep 17 00:00:00 2001 From: Zdenko Nevrala Date: Wed, 4 Oct 2023 08:32:50 +0200 Subject: [PATCH 3/4] Update comment. --- src/ast/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 182f14b3b..b9d73f82a 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -496,7 +496,7 @@ pub enum Expr { /// ```sql /// TRIM([BOTH | LEADING | TRAILING] [ FROM] ) /// TRIM() - /// TRIM(, [, characters]) -- only Snowflake + /// TRIM(, [, characters]) -- only Snowflake or Bigquery /// ``` Trim { expr: Box, From 09a3009a0b8e73898d195f1374c8c1645f37eda4 Mon Sep 17 00:00:00 2001 From: Zdenko Nevrala Date: Thu, 5 Oct 2023 21:29:47 +0200 Subject: [PATCH 4/4] CR. --- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 21460b962..f18c7e1a1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1327,7 +1327,7 @@ impl<'a> Parser<'a> { trim_characters: None, }) } else if self.consume_token(&Token::Comma) - && dialect_of!(self is SnowflakeDialect | BigQueryDialect) + && dialect_of!(self is SnowflakeDialect | BigQueryDialect | GenericDialect) { let characters = self.parse_comma_separated(Parser::parse_expr)?; self.expect_token(&Token::RParen)?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 9c85cfb1c..aaf0df007 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5219,7 +5219,7 @@ fn parse_trim() { //keep Snowflake/BigQuery TRIM syntax failing let all_expected_snowflake = TestedDialects { dialects: vec![ - Box::new(GenericDialect {}), + //Box::new(GenericDialect {}), Box::new(PostgreSqlDialect {}), Box::new(MsSqlDialect {}), Box::new(AnsiDialect {}),