From d4a78f2b87866b1f7e1da552c0628f5a9fd104f2 Mon Sep 17 00:00:00 2001 From: Seve Martinez Date: Sat, 10 Aug 2024 12:07:43 -0700 Subject: [PATCH 1/4] supporting snowflake extract syntax --- src/ast/mod.rs | 20 ++++++++++++++++++-- src/parser/mod.rs | 11 ++++++++++- tests/sqlparser_bigquery.rs | 1 + tests/sqlparser_common.rs | 1 + tests/sqlparser_snowflake.rs | 15 +++++++++++++++ 5 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index e0c929a9d..0c3082945 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -477,6 +477,15 @@ pub enum CastKind { DoubleColon, } +/// `EXTRACT` syntax types. +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] +pub enum ExtractSyntax { + From, + Comma +} + /// An SQL expression of any type. /// /// The parser does not distinguish between expressions of different types @@ -637,13 +646,15 @@ pub enum Expr { time_zone: Box, }, /// Extract a field from a timestamp e.g. `EXTRACT(MONTH FROM foo)` + /// Or `EXTRACT(MONTH, foo)` /// /// Syntax: /// ```sql - /// EXTRACT(DateTimeField FROM ) + /// EXTRACT(DateTimeField FROM ) | EXTRACT(DateTimeField, ) /// ``` Extract { field: DateTimeField, + syntax: ExtractSyntax, expr: Box, }, /// ```sql @@ -1197,7 +1208,12 @@ impl fmt::Display for Expr { write!(f, "{expr}::{data_type}") } }, - Expr::Extract { field, expr } => write!(f, "EXTRACT({field} FROM {expr})"), + Expr::Extract { field, syntax, expr } => { + match syntax { + ExtractSyntax::From => write!(f, "EXTRACT({field} FROM {expr})"), + ExtractSyntax::Comma => write!(f, "EXTRACT({field}, {expr})") + } + } Expr::Ceil { expr, field } => { if field == &DateTimeField::NoDateTime { write!(f, "CEIL({expr})") diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9b252ce29..592588f30 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1682,12 +1682,21 @@ impl<'a> Parser<'a> { pub fn parse_extract_expr(&mut self) -> Result { self.expect_token(&Token::LParen)?; let field = self.parse_date_time_field()?; - self.expect_keyword(Keyword::FROM)?; + + let syntax = if self.parse_keyword(Keyword::FROM) { + ExtractSyntax::From + } else if self.consume_token(&Token::Comma) { + ExtractSyntax::Comma + } else { + return Err(ParserError::ParserError("Expected 'FROM' or ','".to_string())); + }; + let expr = self.parse_expr()?; self.expect_token(&Token::RParen)?; Ok(Expr::Extract { field, expr: Box::new(expr), + syntax, }) } diff --git a/tests/sqlparser_bigquery.rs b/tests/sqlparser_bigquery.rs index a0dd5a662..134c8ddad 100644 --- a/tests/sqlparser_bigquery.rs +++ b/tests/sqlparser_bigquery.rs @@ -2136,6 +2136,7 @@ fn parse_extract_weekday() { assert_eq!( &Expr::Extract { field: DateTimeField::Week(Some(Ident::new("MONDAY"))), + syntax: ExtractSyntax::From, expr: Box::new(Expr::Identifier(Ident::new("d"))), }, expr_from_projection(only(&select.projection)), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 7ec017269..293269cdd 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -2430,6 +2430,7 @@ fn parse_extract() { assert_eq!( &Expr::Extract { field: DateTimeField::Year, + syntax: ExtractSyntax::From, expr: Box::new(Expr::Identifier(Ident::new("d"))), }, expr_from_projection(only(&select.projection)), diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index eaf8c1d14..baaed26c6 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2019,6 +2019,21 @@ fn parse_extract_custom_part() { assert_eq!( &Expr::Extract { field: DateTimeField::Custom(Ident::new("eod")), + syntax: ExtractSyntax::From, + expr: Box::new(Expr::Identifier(Ident::new("d"))), + }, + expr_from_projection(only(&select.projection)), + ); +} + +#[test] +fn parse_extract_comma() { + let sql = "SELECT EXTRACT(HOUR, d)"; + let select = snowflake_and_generic().verified_only_select(sql); + assert_eq!( + &Expr::Extract { + field: DateTimeField::Hour, + syntax: ExtractSyntax::Comma, expr: Box::new(Expr::Identifier(Ident::new("d"))), }, expr_from_projection(only(&select.projection)), From 35b76563187ea6a9acc1c7c3f404bba8b17955a8 Mon Sep 17 00:00:00 2001 From: Seve Martinez Date: Sun, 11 Aug 2024 13:18:03 -0700 Subject: [PATCH 2/4] adding example usage and limiting check to Snowflake dialect --- src/ast/mod.rs | 9 ++++++++- src/parser/mod.rs | 10 ++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0c3082945..0e3a24804 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -477,12 +477,19 @@ pub enum CastKind { DoubleColon, } -/// `EXTRACT` syntax types. +/// `EXTRACT` syntax variants. +/// +/// In Snowflake dialect, the `EXTRACT` expression can support either the `from` syntax +/// or the comma syntax. +/// +/// See #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "visitor", derive(Visit, VisitMut))] pub enum ExtractSyntax { + /// `EXTRACT( FROM )` From, + /// `EXTRACT( , )` Comma } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 592588f30..dd452e91a 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1682,15 +1682,17 @@ impl<'a> Parser<'a> { pub fn parse_extract_expr(&mut self) -> Result { self.expect_token(&Token::LParen)?; let field = self.parse_date_time_field()?; - + let syntax = if self.parse_keyword(Keyword::FROM) { ExtractSyntax::From - } else if self.consume_token(&Token::Comma) { + } else if self.consume_token(&Token::Comma) && dialect_of!(self is SnowflakeDialect | GenericDialect) { ExtractSyntax::Comma } else { - return Err(ParserError::ParserError("Expected 'FROM' or ','".to_string())); + return Err(ParserError::ParserError( + "Expected 'FROM' or ','".to_string(), + )); }; - + let expr = self.parse_expr()?; self.expect_token(&Token::RParen)?; Ok(Expr::Extract { From 529e0659571dbcad612b92297759e586ed1f4e9d Mon Sep 17 00:00:00 2001 From: Seve Martinez Date: Sun, 11 Aug 2024 13:58:32 -0700 Subject: [PATCH 3/4] including single quoted identifiers in extract expression --- src/parser/mod.rs | 5 +++++ tests/sqlparser_snowflake.rs | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index dd452e91a..1bb1e546b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1961,6 +1961,11 @@ impl<'a> Parser<'a> { } _ => self.expected("date/time field", next_token), }, + Token::SingleQuotedString(_) if dialect_of!(self is SnowflakeDialect | GenericDialect) => { + self.prev_token(); + let custom = self.parse_identifier(false)?; + Ok(DateTimeField::Custom(custom)) + }, _ => self.expected("date/time field", next_token), } } diff --git a/tests/sqlparser_snowflake.rs b/tests/sqlparser_snowflake.rs index baaed26c6..a331c7df9 100644 --- a/tests/sqlparser_snowflake.rs +++ b/tests/sqlparser_snowflake.rs @@ -2040,6 +2040,20 @@ fn parse_extract_comma() { ); } +#[test] +fn parse_extract_comma_quoted() { + let sql = "SELECT EXTRACT('hour', d)"; + let select = snowflake_and_generic().verified_only_select(sql); + assert_eq!( + &Expr::Extract { + field: DateTimeField::Custom(Ident::with_quote('\'', "hour")), + syntax: ExtractSyntax::Comma, + expr: Box::new(Expr::Identifier(Ident::new("d"))), + }, + expr_from_projection(only(&select.projection)), + ); +} + #[test] fn parse_comma_outer_join() { // compound identifiers From dbd986b3d6622340073f7ae0433ad28a86f6a370 Mon Sep 17 00:00:00 2001 From: Andrew Lamb Date: Tue, 13 Aug 2024 07:46:19 -0400 Subject: [PATCH 4/4] cargo fmt --- src/ast/mod.rs | 18 ++++++++++-------- src/parser/mod.rs | 15 +++++++++------ 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index 0e3a24804..86e2592a3 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -480,7 +480,7 @@ pub enum CastKind { /// `EXTRACT` syntax variants. /// /// In Snowflake dialect, the `EXTRACT` expression can support either the `from` syntax -/// or the comma syntax. +/// or the comma syntax. /// /// See #[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)] @@ -490,7 +490,7 @@ pub enum ExtractSyntax { /// `EXTRACT( FROM )` From, /// `EXTRACT( , )` - Comma + Comma, } /// An SQL expression of any type. @@ -1215,12 +1215,14 @@ impl fmt::Display for Expr { write!(f, "{expr}::{data_type}") } }, - Expr::Extract { field, syntax, expr } => { - match syntax { - ExtractSyntax::From => write!(f, "EXTRACT({field} FROM {expr})"), - ExtractSyntax::Comma => write!(f, "EXTRACT({field}, {expr})") - } - } + Expr::Extract { + field, + syntax, + expr, + } => match syntax { + ExtractSyntax::From => write!(f, "EXTRACT({field} FROM {expr})"), + ExtractSyntax::Comma => write!(f, "EXTRACT({field}, {expr})"), + }, Expr::Ceil { expr, field } => { if field == &DateTimeField::NoDateTime { write!(f, "CEIL({expr})") diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 1bb1e546b..60a7b4d0b 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1685,7 +1685,9 @@ impl<'a> Parser<'a> { let syntax = if self.parse_keyword(Keyword::FROM) { ExtractSyntax::From - } else if self.consume_token(&Token::Comma) && dialect_of!(self is SnowflakeDialect | GenericDialect) { + } else if self.consume_token(&Token::Comma) + && dialect_of!(self is SnowflakeDialect | GenericDialect) + { ExtractSyntax::Comma } else { return Err(ParserError::ParserError( @@ -1961,11 +1963,12 @@ impl<'a> Parser<'a> { } _ => self.expected("date/time field", next_token), }, - Token::SingleQuotedString(_) if dialect_of!(self is SnowflakeDialect | GenericDialect) => { - self.prev_token(); - let custom = self.parse_identifier(false)?; - Ok(DateTimeField::Custom(custom)) - }, + Token::SingleQuotedString(_) if dialect_of!(self is SnowflakeDialect | GenericDialect) => + { + self.prev_token(); + let custom = self.parse_identifier(false)?; + Ok(DateTimeField::Custom(custom)) + } _ => self.expected("date/time field", next_token), } }