Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support pluralized time units #1630

Merged
merged 4 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/ast/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ impl fmt::Display for DollarQuotedString {
#[cfg_attr(feature = "visitor", derive(Visit, VisitMut))]
pub enum DateTimeField {
Year,
Years,
Month,
Months,
/// Week optionally followed by a WEEKDAY.
///
/// ```sql
Expand All @@ -164,14 +166,19 @@ pub enum DateTimeField {
///
/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/date_functions#extract)
Week(Option<Ident>),
Weeks(Option<Ident>),
Day,
DayOfWeek,
DayOfYear,
Days,
Date,
Datetime,
Hour,
Hours,
Minute,
Minutes,
Second,
Seconds,
Century,
Decade,
Dow,
Expand Down Expand Up @@ -210,22 +217,35 @@ impl fmt::Display for DateTimeField {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
DateTimeField::Year => write!(f, "YEAR"),
DateTimeField::Years => write!(f, "YEARS"),
DateTimeField::Month => write!(f, "MONTH"),
DateTimeField::Months => write!(f, "MONTHS"),
DateTimeField::Week(week_day) => {
write!(f, "WEEK")?;
if let Some(week_day) = week_day {
write!(f, "({week_day})")?
}
Ok(())
}
DateTimeField::Weeks(week_day) => {
write!(f, "WEEKS")?;
if let Some(week_day) = week_day {
write!(f, "({week_day})")?
}
Ok(())
}
DateTimeField::Day => write!(f, "DAY"),
DateTimeField::DayOfWeek => write!(f, "DAYOFWEEK"),
DateTimeField::DayOfYear => write!(f, "DAYOFYEAR"),
DateTimeField::Days => write!(f, "DAYS"),
DateTimeField::Date => write!(f, "DATE"),
DateTimeField::Datetime => write!(f, "DATETIME"),
DateTimeField::Hour => write!(f, "HOUR"),
DateTimeField::Hours => write!(f, "HOURS"),
DateTimeField::Minute => write!(f, "MINUTE"),
DateTimeField::Minutes => write!(f, "MINUTES"),
DateTimeField::Second => write!(f, "SECOND"),
DateTimeField::Seconds => write!(f, "SECONDS"),
DateTimeField::Century => write!(f, "CENTURY"),
DateTimeField::Decade => write!(f, "DECADE"),
DateTimeField::Dow => write!(f, "DOW"),
Expand Down
6 changes: 6 additions & 0 deletions src/keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ define_keywords!(
DAY,
DAYOFWEEK,
DAYOFYEAR,
DAYS,
DEALLOCATE,
DEC,
DECADE,
Expand Down Expand Up @@ -497,13 +498,15 @@ define_keywords!(
MILLISECONDS,
MIN,
MINUTE,
MINUTES,
MINVALUE,
MOD,
MODE,
MODIFIES,
MODIFY,
MODULE,
MONTH,
MONTHS,
MSCK,
MULTISET,
MUTATION,
Expand Down Expand Up @@ -693,6 +696,7 @@ define_keywords!(
SEARCH,
SECOND,
SECONDARY,
SECONDS,
SECRET,
SECURITY,
SEED,
Expand Down Expand Up @@ -861,6 +865,7 @@ define_keywords!(
VOLATILE,
WAREHOUSE,
WEEK,
WEEKS,
WHEN,
WHENEVER,
WHERE,
Expand All @@ -875,6 +880,7 @@ define_keywords!(
XML,
XOR,
YEAR,
YEARS,
ZONE,
ZORDER
);
Expand Down
25 changes: 25 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2340,7 +2340,9 @@ impl<'a> Parser<'a> {
match &next_token.token {
Token::Word(w) => match w.keyword {
Keyword::YEAR => Ok(DateTimeField::Year),
Keyword::YEARS => Ok(DateTimeField::Years),
Keyword::MONTH => Ok(DateTimeField::Month),
Keyword::MONTHS => Ok(DateTimeField::Months),
Keyword::WEEK => {
let week_day = if dialect_of!(self is BigQueryDialect | GenericDialect)
&& self.consume_token(&Token::LParen)
Expand All @@ -2353,14 +2355,30 @@ impl<'a> Parser<'a> {
};
Ok(DateTimeField::Week(week_day))
}
Keyword::WEEKS => {
let week_day = if dialect_of!(self is GenericDialect)
&& self.consume_token(&Token::LParen)
{
let week_day = self.parse_identifier()?;
self.expect_token(&Token::RParen)?;
Some(week_day)
} else {
None
};
Ok(DateTimeField::Weeks(week_day))
}
Keyword::DAY => Ok(DateTimeField::Day),
Keyword::DAYOFWEEK => Ok(DateTimeField::DayOfWeek),
Keyword::DAYOFYEAR => Ok(DateTimeField::DayOfYear),
Keyword::DAYS => Ok(DateTimeField::Days),
Keyword::DATE => Ok(DateTimeField::Date),
Keyword::DATETIME => Ok(DateTimeField::Datetime),
Keyword::HOUR => Ok(DateTimeField::Hour),
Keyword::HOURS => Ok(DateTimeField::Hours),
Keyword::MINUTE => Ok(DateTimeField::Minute),
Keyword::MINUTES => Ok(DateTimeField::Minutes),
Keyword::SECOND => Ok(DateTimeField::Second),
Keyword::SECONDS => Ok(DateTimeField::Seconds),
Keyword::CENTURY => Ok(DateTimeField::Century),
Keyword::DECADE => Ok(DateTimeField::Decade),
Keyword::DOY => Ok(DateTimeField::Doy),
Expand Down Expand Up @@ -2587,12 +2605,19 @@ impl<'a> Parser<'a> {
matches!(
word.keyword,
Keyword::YEAR
| Keyword::YEARS
| Keyword::MONTH
| Keyword::MONTHS
| Keyword::WEEK
| Keyword::WEEKS
| Keyword::DAY
| Keyword::DAYS
| Keyword::HOUR
| Keyword::HOURS
| Keyword::MINUTE
| Keyword::MINUTES
| Keyword::SECOND
| Keyword::SECONDS
| Keyword::CENTURY
| Keyword::DECADE
| Keyword::DOW
Expand Down
134 changes: 116 additions & 18 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ mod test_utils;
#[cfg(test)]
use pretty_assertions::assert_eq;
use sqlparser::ast::ColumnOption::Comment;
use sqlparser::ast::DateTimeField::Seconds;
use sqlparser::ast::Expr::{Identifier, UnaryOp};
use sqlparser::ast::Value::Number;
use sqlparser::test_utils::all_dialects_except;
Expand Down Expand Up @@ -5334,6 +5335,19 @@ fn parse_interval_all() {
expr_from_projection(only(&select.projection)),
);

let sql = "SELECT INTERVAL 5 DAYS";
let select = verified_only_select(sql);
assert_eq!(
&Expr::Interval(Interval {
value: Box::new(Expr::Value(number("5"))),
leading_field: Some(DateTimeField::Days),
leading_precision: None,
last_field: None,
fractional_seconds_precision: None,
}),
expr_from_projection(only(&select.projection)),
);

let sql = "SELECT INTERVAL '10' HOUR (1)";
let select = verified_only_select(sql);
assert_eq!(
Expand Down Expand Up @@ -5361,10 +5375,18 @@ fn parse_interval_all() {

verified_only_select("SELECT INTERVAL '1' YEAR");
verified_only_select("SELECT INTERVAL '1' MONTH");
verified_only_select("SELECT INTERVAL '1' WEEK");
verified_only_select("SELECT INTERVAL '1' DAY");
verified_only_select("SELECT INTERVAL '1' HOUR");
verified_only_select("SELECT INTERVAL '1' MINUTE");
verified_only_select("SELECT INTERVAL '1' SECOND");
verified_only_select("SELECT INTERVAL '1' YEARS");
verified_only_select("SELECT INTERVAL '1' MONTHS");
verified_only_select("SELECT INTERVAL '1' WEEKS");
verified_only_select("SELECT INTERVAL '1' DAYS");
verified_only_select("SELECT INTERVAL '1' HOURS");
verified_only_select("SELECT INTERVAL '1' MINUTES");
verified_only_select("SELECT INTERVAL '1' SECONDS");
verified_only_select("SELECT INTERVAL '1' YEAR TO MONTH");
verified_only_select("SELECT INTERVAL '1' DAY TO HOUR");
verified_only_select("SELECT INTERVAL '1' DAY TO MINUTE");
Expand All @@ -5374,10 +5396,49 @@ fn parse_interval_all() {
verified_only_select("SELECT INTERVAL '1' MINUTE TO SECOND");
verified_only_select("SELECT INTERVAL 1 YEAR");
verified_only_select("SELECT INTERVAL 1 MONTH");
verified_only_select("SELECT INTERVAL 1 WEEK");
verified_only_select("SELECT INTERVAL 1 DAY");
verified_only_select("SELECT INTERVAL 1 HOUR");
verified_only_select("SELECT INTERVAL 1 MINUTE");
verified_only_select("SELECT INTERVAL 1 SECOND");
verified_only_select("SELECT INTERVAL 1 YEARS");
verified_only_select("SELECT INTERVAL 1 MONTHS");
verified_only_select("SELECT INTERVAL 1 WEEKS");
verified_only_select("SELECT INTERVAL 1 DAYS");
verified_only_select("SELECT INTERVAL 1 HOURS");
verified_only_select("SELECT INTERVAL 1 MINUTES");
verified_only_select("SELECT INTERVAL 1 SECONDS");
verified_only_select(
"SELECT '2 years 15 months 100 weeks 99 hours 123456789 milliseconds'::INTERVAL",
);

// keep Generic/BigQuery extract week with weekday syntax success
let supported_dialects = TestedDialects::new(vec![
Box::new(GenericDialect {}),
Box::new(BigQueryDialect {}),
]);

let sql = "SELECT EXTRACT(WEEK(a) FROM date)";
supported_dialects.verified_stmt(sql);
wugeer marked this conversation as resolved.
Show resolved Hide resolved

let all_other_dialects = TestedDialects::new(vec![
Box::new(PostgreSqlDialect {}),
Box::new(MsSqlDialect {}),
Box::new(AnsiDialect {}),
Box::new(SnowflakeDialect {}),
Box::new(HiveDialect {}),
Box::new(RedshiftSqlDialect {}),
Box::new(MySqlDialect {}),
Box::new(SQLiteDialect {}),
Box::new(DuckDbDialect {}),
]);

assert_eq!(
ParserError::ParserError("Expected 'FROM' or ','".to_owned()),
all_other_dialects
.parse_sql_statements("SELECT EXTRACT(WEEK(a) FROM date)")
.unwrap_err()
);
wugeer marked this conversation as resolved.
Show resolved Hide resolved
}

#[test]
Expand Down Expand Up @@ -11282,16 +11343,12 @@ fn test_group_by_nothing() {
#[test]
fn test_extract_seconds_ok() {
let dialects = all_dialects_where(|d| d.allow_extract_custom());
let stmt = dialects.verified_expr("EXTRACT(seconds FROM '2 seconds'::INTERVAL)");
let stmt = dialects.verified_expr("EXTRACT(SECONDS FROM '2 seconds'::INTERVAL)");

assert_eq!(
stmt,
Expr::Extract {
field: DateTimeField::Custom(Ident {
value: "seconds".to_string(),
quote_style: None,
span: Span::empty(),
}),
field: Seconds,
syntax: ExtractSyntax::From,
expr: Box::new(Expr::Cast {
kind: CastKind::DoubleColon,
Expand All @@ -11302,7 +11359,59 @@ fn test_extract_seconds_ok() {
format: None,
}),
}
)
);

let actual_ast = dialects
.parse_sql_statements("SELECT EXTRACT(seconds FROM '2 seconds'::INTERVAL)")
.unwrap();

let expected_ast = vec![Statement::Query(Box::new(Query {
with: None,
body: Box::new(SetExpr::Select(Box::new(Select {
select_token: AttachedToken::empty(),
distinct: None,
top: None,
top_before_distinct: false,
projection: vec![UnnamedExpr(Expr::Extract {
field: Seconds,
syntax: ExtractSyntax::From,
expr: Box::new(Expr::Cast {
kind: CastKind::DoubleColon,
expr: Box::new(Expr::Value(Value::SingleQuotedString(
"2 seconds".to_string(),
))),
data_type: DataType::Interval,
format: None,
}),
})],
into: None,
from: vec![],
lateral_views: vec![],
prewhere: None,
selection: None,
group_by: GroupByExpr::Expressions(vec![], vec![]),
cluster_by: vec![],
distribute_by: vec![],
sort_by: vec![],
having: None,
named_window: vec![],
qualify: None,
window_before_qualify: false,
value_table_mode: None,
connect_by: None,
}))),
order_by: None,
limit: None,
limit_by: vec![],
offset: None,
fetch: None,
locks: vec![],
for_clause: None,
settings: None,
format_clause: None,
}))];

assert_eq!(actual_ast, expected_ast);
}

#[test]
Expand Down Expand Up @@ -11331,17 +11440,6 @@ fn test_extract_seconds_single_quote_ok() {
)
}

#[test]
fn test_extract_seconds_err() {
let sql = "SELECT EXTRACT(seconds FROM '2 seconds'::INTERVAL)";
let dialects = all_dialects_except(|d| d.allow_extract_custom());
let err = dialects.parse_sql_statements(sql).unwrap_err();
assert_eq!(
err.to_string(),
"sql parser error: Expected: date/time field, found: seconds"
);
}

#[test]
fn test_extract_seconds_single_quote_err() {
let sql = r#"SELECT EXTRACT('seconds' FROM '2 seconds'::INTERVAL)"#;
Expand Down
Loading