Skip to content

Commit cd640b9

Browse files
committed
Support more DateTimeField variants
- Adds `DATETIME` variant to `DatetimeField` - Adds support for snowflake abbreviations via `Custom` variant. https://docs.snowflake.com/en/sql-reference/functions-date-time#supported-date-and-time-parts - Adds support for BigQuery weekday `WEEK(MONDAY)` syntax https://cloud.google.com/bigquery/docs/reference/standard-sql/date_functions#extract
1 parent 6b03a25 commit cd640b9

File tree

6 files changed

+129
-46
lines changed

6 files changed

+129
-46
lines changed

src/ast/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ impl fmt::Display for Interval {
229229
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
230230
let value = self.value.as_ref();
231231
match (
232-
self.leading_field,
232+
&self.leading_field,
233233
self.leading_precision,
234234
self.fractional_seconds_precision,
235235
) {
@@ -248,13 +248,13 @@ impl fmt::Display for Interval {
248248
}
249249
_ => {
250250
write!(f, "INTERVAL {value}")?;
251-
if let Some(leading_field) = self.leading_field {
251+
if let Some(leading_field) = &self.leading_field {
252252
write!(f, " {leading_field}")?;
253253
}
254254
if let Some(leading_precision) = self.leading_precision {
255255
write!(f, " ({leading_precision})")?;
256256
}
257-
if let Some(last_field) = self.last_field {
257+
if let Some(last_field) = &self.last_field {
258258
write!(f, " TO {last_field}")?;
259259
}
260260
if let Some(fractional_seconds_precision) = self.fractional_seconds_precision {

src/ast/value.rs

Lines changed: 75 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@
1212

1313
#[cfg(not(feature = "std"))]
1414
use alloc::string::String;
15+
16+
#[cfg(not(feature = "std"))]
17+
use alloc::format;
18+
19+
#[cfg(not(feature = "std"))]
20+
use alloc::string::ToString;
21+
1522
use core::fmt;
1623

1724
#[cfg(feature = "bigdecimal")]
@@ -20,6 +27,7 @@ use bigdecimal::BigDecimal;
2027
#[cfg(feature = "serde")]
2128
use serde::{Deserialize, Serialize};
2229

30+
use crate::ast::Ident;
2331
#[cfg(feature = "visitor")]
2432
use sqlparser_derive::{Visit, VisitMut};
2533

@@ -109,17 +117,25 @@ impl fmt::Display for DollarQuotedString {
109117
}
110118
}
111119

112-
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
120+
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
113121
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
114122
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
115123
pub enum DateTimeField {
116124
Year,
117125
Month,
118-
Week,
126+
/// Week optionally followed by a WEEKDAY.
127+
///
128+
/// ```sql
129+
/// WEEK(MONDAY)
130+
/// ```
131+
///
132+
/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/date_functions#extract)
133+
Week(Option<Ident>),
119134
Day,
120135
DayOfWeek,
121136
DayOfYear,
122137
Date,
138+
Datetime,
123139
Hour,
124140
Minute,
125141
Second,
@@ -148,47 +164,67 @@ pub enum DateTimeField {
148164
TimezoneMinute,
149165
TimezoneRegion,
150166
NoDateTime,
167+
/// Arbitrary abbreviation or custom date-time part.
168+
///
169+
/// ```sql
170+
/// EXTRACT(q FROM CURRENT_TIMESTAMP)
171+
/// ```
172+
/// [Snowflake](https://docs.snowflake.com/en/sql-reference/functions-date-time#supported-date-and-time-parts)
173+
Custom(Ident),
151174
}
152175

153176
impl fmt::Display for DateTimeField {
154177
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
155-
f.write_str(match self {
156-
DateTimeField::Year => "YEAR",
157-
DateTimeField::Month => "MONTH",
158-
DateTimeField::Week => "WEEK",
159-
DateTimeField::Day => "DAY",
160-
DateTimeField::DayOfWeek => "DAYOFWEEK",
161-
DateTimeField::DayOfYear => "DAYOFYEAR",
162-
DateTimeField::Date => "DATE",
163-
DateTimeField::Hour => "HOUR",
164-
DateTimeField::Minute => "MINUTE",
165-
DateTimeField::Second => "SECOND",
166-
DateTimeField::Century => "CENTURY",
167-
DateTimeField::Decade => "DECADE",
168-
DateTimeField::Dow => "DOW",
169-
DateTimeField::Doy => "DOY",
170-
DateTimeField::Epoch => "EPOCH",
171-
DateTimeField::Isodow => "ISODOW",
172-
DateTimeField::Isoyear => "ISOYEAR",
173-
DateTimeField::IsoWeek => "ISOWEEK",
174-
DateTimeField::Julian => "JULIAN",
175-
DateTimeField::Microsecond => "MICROSECOND",
176-
DateTimeField::Microseconds => "MICROSECONDS",
177-
DateTimeField::Millenium => "MILLENIUM",
178-
DateTimeField::Millennium => "MILLENNIUM",
179-
DateTimeField::Millisecond => "MILLISECOND",
180-
DateTimeField::Milliseconds => "MILLISECONDS",
181-
DateTimeField::Nanosecond => "NANOSECOND",
182-
DateTimeField::Nanoseconds => "NANOSECONDS",
183-
DateTimeField::Quarter => "QUARTER",
184-
DateTimeField::Time => "TIME",
185-
DateTimeField::Timezone => "TIMEZONE",
186-
DateTimeField::TimezoneAbbr => "TIMEZONE_ABBR",
187-
DateTimeField::TimezoneHour => "TIMEZONE_HOUR",
188-
DateTimeField::TimezoneMinute => "TIMEZONE_MINUTE",
189-
DateTimeField::TimezoneRegion => "TIMEZONE_REGION",
190-
DateTimeField::NoDateTime => "NODATETIME",
191-
})
178+
f.write_str(
179+
match self {
180+
DateTimeField::Year => "YEAR".to_string(),
181+
DateTimeField::Month => "MONTH".to_string(),
182+
DateTimeField::Week(week_day) => {
183+
format!(
184+
"WEEK{}",
185+
week_day
186+
.as_ref()
187+
.map(|w| format!("({w})"))
188+
.unwrap_or_default()
189+
)
190+
}
191+
DateTimeField::Day => "DAY".to_string(),
192+
DateTimeField::DayOfWeek => "DAYOFWEEK".to_string(),
193+
DateTimeField::DayOfYear => "DAYOFYEAR".to_string(),
194+
DateTimeField::Date => "DATE".to_string(),
195+
DateTimeField::Datetime => "DATETIME".to_string(),
196+
DateTimeField::Hour => "HOUR".to_string(),
197+
DateTimeField::Minute => "MINUTE".to_string(),
198+
DateTimeField::Second => "SECOND".to_string(),
199+
DateTimeField::Century => "CENTURY".to_string(),
200+
DateTimeField::Decade => "DECADE".to_string(),
201+
DateTimeField::Dow => "DOW".to_string(),
202+
DateTimeField::Doy => "DOY".to_string(),
203+
DateTimeField::Epoch => "EPOCH".to_string(),
204+
DateTimeField::Isodow => "ISODOW".to_string(),
205+
DateTimeField::Isoyear => "ISOYEAR".to_string(),
206+
DateTimeField::IsoWeek => "ISOWEEK".to_string(),
207+
DateTimeField::Julian => "JULIAN".to_string(),
208+
DateTimeField::Microsecond => "MICROSECOND".to_string(),
209+
DateTimeField::Microseconds => "MICROSECONDS".to_string(),
210+
DateTimeField::Millenium => "MILLENIUM".to_string(),
211+
DateTimeField::Millennium => "MILLENNIUM".to_string(),
212+
DateTimeField::Millisecond => "MILLISECOND".to_string(),
213+
DateTimeField::Milliseconds => "MILLISECONDS".to_string(),
214+
DateTimeField::Nanosecond => "NANOSECOND".to_string(),
215+
DateTimeField::Nanoseconds => "NANOSECONDS".to_string(),
216+
DateTimeField::Quarter => "QUARTER".to_string(),
217+
DateTimeField::Time => "TIME".to_string(),
218+
DateTimeField::Timezone => "TIMEZONE".to_string(),
219+
DateTimeField::TimezoneAbbr => "TIMEZONE_ABBR".to_string(),
220+
DateTimeField::TimezoneHour => "TIMEZONE_HOUR".to_string(),
221+
DateTimeField::TimezoneMinute => "TIMEZONE_MINUTE".to_string(),
222+
DateTimeField::TimezoneRegion => "TIMEZONE_REGION".to_string(),
223+
DateTimeField::NoDateTime => "NODATETIME".to_string(),
224+
DateTimeField::Custom(custom) => format!("{custom}"),
225+
}
226+
.as_str(),
227+
)
192228
}
193229
}
194230

src/parser/mod.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1778,11 +1778,23 @@ impl<'a> Parser<'a> {
17781778
Token::Word(w) => match w.keyword {
17791779
Keyword::YEAR => Ok(DateTimeField::Year),
17801780
Keyword::MONTH => Ok(DateTimeField::Month),
1781-
Keyword::WEEK => Ok(DateTimeField::Week),
1781+
Keyword::WEEK => {
1782+
let week_day = if dialect_of!(self is BigQueryDialect | GenericDialect)
1783+
&& self.consume_token(&Token::LParen)
1784+
{
1785+
let week_day = self.parse_identifier(false)?;
1786+
self.expect_token(&Token::RParen)?;
1787+
Some(week_day)
1788+
} else {
1789+
None
1790+
};
1791+
Ok(DateTimeField::Week(week_day))
1792+
}
17821793
Keyword::DAY => Ok(DateTimeField::Day),
17831794
Keyword::DAYOFWEEK => Ok(DateTimeField::DayOfWeek),
17841795
Keyword::DAYOFYEAR => Ok(DateTimeField::DayOfYear),
17851796
Keyword::DATE => Ok(DateTimeField::Date),
1797+
Keyword::DATETIME => Ok(DateTimeField::Datetime),
17861798
Keyword::HOUR => Ok(DateTimeField::Hour),
17871799
Keyword::MINUTE => Ok(DateTimeField::Minute),
17881800
Keyword::SECOND => Ok(DateTimeField::Second),
@@ -1810,6 +1822,11 @@ impl<'a> Parser<'a> {
18101822
Keyword::TIMEZONE_HOUR => Ok(DateTimeField::TimezoneHour),
18111823
Keyword::TIMEZONE_MINUTE => Ok(DateTimeField::TimezoneMinute),
18121824
Keyword::TIMEZONE_REGION => Ok(DateTimeField::TimezoneRegion),
1825+
_ if dialect_of!(self is SnowflakeDialect | GenericDialect) => {
1826+
self.prev_token();
1827+
let custom = self.parse_identifier(false)?;
1828+
Ok(DateTimeField::Custom(custom))
1829+
}
18131830
_ => self.expected("date/time field", next_token),
18141831
},
18151832
_ => self.expected("date/time field", next_token),

tests/sqlparser_bigquery.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1417,6 +1417,19 @@ fn test_bigquery_trim() {
14171417
);
14181418
}
14191419

1420+
#[test]
1421+
fn parse_extract_weekday() {
1422+
let sql = "SELECT EXTRACT(WEEK(MONDAY) FROM d)";
1423+
let select = bigquery_and_generic().verified_only_select(sql);
1424+
assert_eq!(
1425+
&Expr::Extract {
1426+
field: DateTimeField::Week(Some(Ident::new("MONDAY"))),
1427+
expr: Box::new(Expr::Identifier(Ident::new("d"))),
1428+
},
1429+
expr_from_projection(only(&select.projection)),
1430+
);
1431+
}
1432+
14201433
#[test]
14211434
fn test_select_as_struct() {
14221435
bigquery().verified_only_select("SELECT * FROM (SELECT AS VALUE STRUCT(123 AS a, false AS b))");

tests/sqlparser_common.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2270,6 +2270,7 @@ fn parse_extract() {
22702270
verified_stmt("SELECT EXTRACT(DAYOFWEEK FROM d)");
22712271
verified_stmt("SELECT EXTRACT(DAYOFYEAR FROM d)");
22722272
verified_stmt("SELECT EXTRACT(DATE FROM d)");
2273+
verified_stmt("SELECT EXTRACT(DATETIME FROM d)");
22732274
verified_stmt("SELECT EXTRACT(HOUR FROM d)");
22742275
verified_stmt("SELECT EXTRACT(MINUTE FROM d)");
22752276
verified_stmt("SELECT EXTRACT(SECOND FROM d)");
@@ -2299,7 +2300,8 @@ fn parse_extract() {
22992300
verified_stmt("SELECT EXTRACT(TIMEZONE_REGION FROM d)");
23002301
verified_stmt("SELECT EXTRACT(TIME FROM d)");
23012302

2302-
let res = parse_sql_statements("SELECT EXTRACT(JIFFY FROM d)");
2303+
let dialects = all_dialects_except(|d| d.is::<SnowflakeDialect>() || d.is::<GenericDialect>());
2304+
let res = dialects.parse_sql_statements("SELECT EXTRACT(JIFFY FROM d)");
23032305
assert_eq!(
23042306
ParserError::ParserError("Expected date/time field, found: JIFFY".to_string()),
23052307
res.unwrap_err()
@@ -2337,7 +2339,8 @@ fn parse_ceil_datetime() {
23372339
verified_stmt("SELECT CEIL(d TO SECOND) FROM df");
23382340
verified_stmt("SELECT CEIL(d TO MILLISECOND) FROM df");
23392341

2340-
let res = parse_sql_statements("SELECT CEIL(d TO JIFFY) FROM df");
2342+
let dialects = all_dialects_except(|d| d.is::<SnowflakeDialect>() || d.is::<GenericDialect>());
2343+
let res = dialects.parse_sql_statements("SELECT CEIL(d TO JIFFY) FROM df");
23412344
assert_eq!(
23422345
ParserError::ParserError("Expected date/time field, found: JIFFY".to_string()),
23432346
res.unwrap_err()
@@ -2363,7 +2366,8 @@ fn parse_floor_datetime() {
23632366
verified_stmt("SELECT FLOOR(d TO SECOND) FROM df");
23642367
verified_stmt("SELECT FLOOR(d TO MILLISECOND) FROM df");
23652368

2366-
let res = parse_sql_statements("SELECT FLOOR(d TO JIFFY) FROM df");
2369+
let dialects = all_dialects_except(|d| d.is::<SnowflakeDialect>() || d.is::<GenericDialect>());
2370+
let res = dialects.parse_sql_statements("SELECT FLOOR(d TO JIFFY) FROM df");
23672371
assert_eq!(
23682372
ParserError::ParserError("Expected date/time field, found: JIFFY".to_string()),
23692373
res.unwrap_err()

tests/sqlparser_snowflake.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,6 +1430,19 @@ fn parse_top() {
14301430
);
14311431
}
14321432

1433+
#[test]
1434+
fn parse_extract_custom_part() {
1435+
let sql = "SELECT EXTRACT(eod FROM d)";
1436+
let select = snowflake_and_generic().verified_only_select(sql);
1437+
assert_eq!(
1438+
&Expr::Extract {
1439+
field: DateTimeField::Custom(Ident::new("eod")),
1440+
expr: Box::new(Expr::Identifier(Ident::new("d"))),
1441+
},
1442+
expr_from_projection(only(&select.projection)),
1443+
);
1444+
}
1445+
14331446
#[test]
14341447
fn parse_comma_outer_join() {
14351448
// compound identifiers

0 commit comments

Comments
 (0)