Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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 BigQueryDialect)
&& 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
106 changes: 88 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,21 @@ 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",
);
}

#[test]
Expand Down Expand Up @@ -11282,16 +11315,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 +11331,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 +11412,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