mirror of
https://github.com/apache/datafusion-sqlparser-rs.git
synced 2025-07-07 17:04:59 +00:00
Support pluralized time units (#1630)
This commit is contained in:
parent
0cd49fb699
commit
62bcaa1c98
4 changed files with 122 additions and 18 deletions
|
@ -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
|
||||
|
@ -164,14 +166,19 @@ pub enum DateTimeField {
|
|||
///
|
||||
/// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/date_functions#extract)
|
||||
Week(Option<Ident>),
|
||||
Weeks,
|
||||
Day,
|
||||
DayOfWeek,
|
||||
DayOfYear,
|
||||
Days,
|
||||
Date,
|
||||
Datetime,
|
||||
Hour,
|
||||
Hours,
|
||||
Minute,
|
||||
Minutes,
|
||||
Second,
|
||||
Seconds,
|
||||
Century,
|
||||
Decade,
|
||||
Dow,
|
||||
|
@ -210,7 +217,9 @@ 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 {
|
||||
|
@ -218,14 +227,19 @@ impl fmt::Display for DateTimeField {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
DateTimeField::Weeks => write!(f, "WEEKS"),
|
||||
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"),
|
||||
|
|
|
@ -234,6 +234,7 @@ define_keywords!(
|
|||
DAY,
|
||||
DAYOFWEEK,
|
||||
DAYOFYEAR,
|
||||
DAYS,
|
||||
DEALLOCATE,
|
||||
DEC,
|
||||
DECADE,
|
||||
|
@ -499,6 +500,7 @@ define_keywords!(
|
|||
MILLISECONDS,
|
||||
MIN,
|
||||
MINUTE,
|
||||
MINUTES,
|
||||
MINVALUE,
|
||||
MOD,
|
||||
MODE,
|
||||
|
@ -506,6 +508,7 @@ define_keywords!(
|
|||
MODIFY,
|
||||
MODULE,
|
||||
MONTH,
|
||||
MONTHS,
|
||||
MSCK,
|
||||
MULTISET,
|
||||
MUTATION,
|
||||
|
@ -698,6 +701,7 @@ define_keywords!(
|
|||
SEARCH,
|
||||
SECOND,
|
||||
SECONDARY,
|
||||
SECONDS,
|
||||
SECRET,
|
||||
SECURITY,
|
||||
SEED,
|
||||
|
@ -866,6 +870,7 @@ define_keywords!(
|
|||
VOLATILE,
|
||||
WAREHOUSE,
|
||||
WEEK,
|
||||
WEEKS,
|
||||
WHEN,
|
||||
WHENEVER,
|
||||
WHERE,
|
||||
|
@ -880,6 +885,7 @@ define_keywords!(
|
|||
XML,
|
||||
XOR,
|
||||
YEAR,
|
||||
YEARS,
|
||||
ZONE,
|
||||
ZORDER
|
||||
);
|
||||
|
|
|
@ -2358,7 +2358,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)
|
||||
|
@ -2371,14 +2373,19 @@ impl<'a> Parser<'a> {
|
|||
};
|
||||
Ok(DateTimeField::Week(week_day))
|
||||
}
|
||||
Keyword::WEEKS => Ok(DateTimeField::Weeks),
|
||||
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),
|
||||
|
@ -2605,12 +2612,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
|
||||
|
|
|
@ -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;
|
||||
|
@ -5399,6 +5400,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!(
|
||||
|
@ -5426,10 +5440,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");
|
||||
|
@ -5439,10 +5461,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]
|
||||
|
@ -11356,16 +11389,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,
|
||||
|
@ -11376,7 +11405,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]
|
||||
|
@ -11405,17 +11486,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)"#;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue