Skip to content

Commit f5b818e

Browse files
seve-martinezalamb
andauthored
supporting snowflake extract syntax (#1374)
Co-authored-by: Andrew Lamb <[email protected]>
1 parent ca5262c commit f5b818e

File tree

5 files changed

+78
-3
lines changed

5 files changed

+78
-3
lines changed

src/ast/mod.rs

+27-2
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,22 @@ pub enum CastKind {
477477
DoubleColon,
478478
}
479479

480+
/// `EXTRACT` syntax variants.
481+
///
482+
/// In Snowflake dialect, the `EXTRACT` expression can support either the `from` syntax
483+
/// or the comma syntax.
484+
///
485+
/// See <https://docs.snowflake.com/en/sql-reference/functions/extract>
486+
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
487+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
488+
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
489+
pub enum ExtractSyntax {
490+
/// `EXTRACT( <date_or_time_part> FROM <date_or_time_expr> )`
491+
From,
492+
/// `EXTRACT( <date_or_time_part> , <date_or_timestamp_expr> )`
493+
Comma,
494+
}
495+
480496
/// An SQL expression of any type.
481497
///
482498
/// The parser does not distinguish between expressions of different types
@@ -637,13 +653,15 @@ pub enum Expr {
637653
time_zone: Box<Expr>,
638654
},
639655
/// Extract a field from a timestamp e.g. `EXTRACT(MONTH FROM foo)`
656+
/// Or `EXTRACT(MONTH, foo)`
640657
///
641658
/// Syntax:
642659
/// ```sql
643-
/// EXTRACT(DateTimeField FROM <expr>)
660+
/// EXTRACT(DateTimeField FROM <expr>) | EXTRACT(DateTimeField, <expr>)
644661
/// ```
645662
Extract {
646663
field: DateTimeField,
664+
syntax: ExtractSyntax,
647665
expr: Box<Expr>,
648666
},
649667
/// ```sql
@@ -1197,7 +1215,14 @@ impl fmt::Display for Expr {
11971215
write!(f, "{expr}::{data_type}")
11981216
}
11991217
},
1200-
Expr::Extract { field, expr } => write!(f, "EXTRACT({field} FROM {expr})"),
1218+
Expr::Extract {
1219+
field,
1220+
syntax,
1221+
expr,
1222+
} => match syntax {
1223+
ExtractSyntax::From => write!(f, "EXTRACT({field} FROM {expr})"),
1224+
ExtractSyntax::Comma => write!(f, "EXTRACT({field}, {expr})"),
1225+
},
12011226
Expr::Ceil { expr, field } => {
12021227
if field == &DateTimeField::NoDateTime {
12031228
write!(f, "CEIL({expr})")

src/parser/mod.rs

+20-1
Original file line numberDiff line numberDiff line change
@@ -1682,12 +1682,25 @@ impl<'a> Parser<'a> {
16821682
pub fn parse_extract_expr(&mut self) -> Result<Expr, ParserError> {
16831683
self.expect_token(&Token::LParen)?;
16841684
let field = self.parse_date_time_field()?;
1685-
self.expect_keyword(Keyword::FROM)?;
1685+
1686+
let syntax = if self.parse_keyword(Keyword::FROM) {
1687+
ExtractSyntax::From
1688+
} else if self.consume_token(&Token::Comma)
1689+
&& dialect_of!(self is SnowflakeDialect | GenericDialect)
1690+
{
1691+
ExtractSyntax::Comma
1692+
} else {
1693+
return Err(ParserError::ParserError(
1694+
"Expected 'FROM' or ','".to_string(),
1695+
));
1696+
};
1697+
16861698
let expr = self.parse_expr()?;
16871699
self.expect_token(&Token::RParen)?;
16881700
Ok(Expr::Extract {
16891701
field,
16901702
expr: Box::new(expr),
1703+
syntax,
16911704
})
16921705
}
16931706

@@ -1950,6 +1963,12 @@ impl<'a> Parser<'a> {
19501963
}
19511964
_ => self.expected("date/time field", next_token),
19521965
},
1966+
Token::SingleQuotedString(_) if dialect_of!(self is SnowflakeDialect | GenericDialect) =>
1967+
{
1968+
self.prev_token();
1969+
let custom = self.parse_identifier(false)?;
1970+
Ok(DateTimeField::Custom(custom))
1971+
}
19531972
_ => self.expected("date/time field", next_token),
19541973
}
19551974
}

tests/sqlparser_bigquery.rs

+1
Original file line numberDiff line numberDiff line change
@@ -2136,6 +2136,7 @@ fn parse_extract_weekday() {
21362136
assert_eq!(
21372137
&Expr::Extract {
21382138
field: DateTimeField::Week(Some(Ident::new("MONDAY"))),
2139+
syntax: ExtractSyntax::From,
21392140
expr: Box::new(Expr::Identifier(Ident::new("d"))),
21402141
},
21412142
expr_from_projection(only(&select.projection)),

tests/sqlparser_common.rs

+1
Original file line numberDiff line numberDiff line change
@@ -2430,6 +2430,7 @@ fn parse_extract() {
24302430
assert_eq!(
24312431
&Expr::Extract {
24322432
field: DateTimeField::Year,
2433+
syntax: ExtractSyntax::From,
24332434
expr: Box::new(Expr::Identifier(Ident::new("d"))),
24342435
},
24352436
expr_from_projection(only(&select.projection)),

tests/sqlparser_snowflake.rs

+29
Original file line numberDiff line numberDiff line change
@@ -2019,6 +2019,35 @@ fn parse_extract_custom_part() {
20192019
assert_eq!(
20202020
&Expr::Extract {
20212021
field: DateTimeField::Custom(Ident::new("eod")),
2022+
syntax: ExtractSyntax::From,
2023+
expr: Box::new(Expr::Identifier(Ident::new("d"))),
2024+
},
2025+
expr_from_projection(only(&select.projection)),
2026+
);
2027+
}
2028+
2029+
#[test]
2030+
fn parse_extract_comma() {
2031+
let sql = "SELECT EXTRACT(HOUR, d)";
2032+
let select = snowflake_and_generic().verified_only_select(sql);
2033+
assert_eq!(
2034+
&Expr::Extract {
2035+
field: DateTimeField::Hour,
2036+
syntax: ExtractSyntax::Comma,
2037+
expr: Box::new(Expr::Identifier(Ident::new("d"))),
2038+
},
2039+
expr_from_projection(only(&select.projection)),
2040+
);
2041+
}
2042+
2043+
#[test]
2044+
fn parse_extract_comma_quoted() {
2045+
let sql = "SELECT EXTRACT('hour', d)";
2046+
let select = snowflake_and_generic().verified_only_select(sql);
2047+
assert_eq!(
2048+
&Expr::Extract {
2049+
field: DateTimeField::Custom(Ident::with_quote('\'', "hour")),
2050+
syntax: ExtractSyntax::Comma,
20222051
expr: Box::new(Expr::Identifier(Ident::new("d"))),
20232052
},
20242053
expr_from_projection(only(&select.projection)),

0 commit comments

Comments
 (0)