From f8b32e659a1c1b9ad1f1c003f68638915231086b Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Tue, 31 Dec 2024 23:14:07 +0800 Subject: [PATCH 1/4] add time unit `SECONDS/MINUTES/HOURS/DAYS/WEEKS/MONTHS/YEARS` for `INTERVAL` type --- src/ast/value.rs | 20 +++++++ src/keywords.rs | 6 +++ src/parser/mod.rs | 25 +++++++++ tests/sqlparser_common.rs | 106 +++++++++++++++++++++++++++++++------- 4 files changed, 139 insertions(+), 18 deletions(-) diff --git a/src/ast/value.rs b/src/ast/value.rs index 28bf89ba8..fa291b9bd 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -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), + Weeks(Option), 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,25 @@ impl fmt::Display for DateTimeField { } 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"), diff --git a/src/keywords.rs b/src/keywords.rs index 43abc2b03..0ea0c9f7b 100644 --- a/src/keywords.rs +++ b/src/keywords.rs @@ -234,6 +234,7 @@ define_keywords!( DAY, DAYOFWEEK, DAYOFYEAR, + DAYS, DEALLOCATE, DEC, DECADE, @@ -497,6 +498,7 @@ define_keywords!( MILLISECONDS, MIN, MINUTE, + MINUTES, MINVALUE, MOD, MODE, @@ -504,6 +506,7 @@ define_keywords!( MODIFY, MODULE, MONTH, + MONTHS, MSCK, MULTISET, MUTATION, @@ -693,6 +696,7 @@ define_keywords!( SEARCH, SECOND, SECONDARY, + SECONDS, SECRET, SECURITY, SEED, @@ -861,6 +865,7 @@ define_keywords!( VOLATILE, WAREHOUSE, WEEK, + WEEKS, WHEN, WHENEVER, WHERE, @@ -875,6 +880,7 @@ define_keywords!( XML, XOR, YEAR, + YEARS, ZONE, ZORDER ); diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 012314b42..c1e05fedb 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -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) @@ -2353,14 +2355,30 @@ impl<'a> Parser<'a> { }; Ok(DateTimeField::Week(week_day)) } + Keyword::WEEKS => { + let week_day = if dialect_of!(self is BigQueryDialect | 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), @@ -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 diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 3b21160b9..adc0681e2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -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; @@ -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!( @@ -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"); @@ -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] @@ -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, @@ -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] @@ -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)"#; From 072cf8ae0d91b522266cfdcea3640daa36ffb724 Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Mon, 6 Jan 2025 21:56:02 +0800 Subject: [PATCH 2/4] correct BigQuery not support weeks --- src/parser/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c1e05fedb..4e9e8cd80 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2356,7 +2356,7 @@ impl<'a> Parser<'a> { Ok(DateTimeField::Week(week_day)) } Keyword::WEEKS => { - let week_day = if dialect_of!(self is BigQueryDialect | GenericDialect) + let week_day = if dialect_of!(self is BigQueryDialect) && self.consume_token(&Token::LParen) { let week_day = self.parse_identifier()?; From ba3acb243007555bfc90885fcc9aad52642c06ca Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Tue, 7 Jan 2025 23:09:47 +0800 Subject: [PATCH 3/4] add test for extract week with weekday syntax --- src/parser/mod.rs | 2 +- tests/sqlparser_common.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4e9e8cd80..009d6f9cf 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2356,7 +2356,7 @@ impl<'a> Parser<'a> { Ok(DateTimeField::Week(week_day)) } Keyword::WEEKS => { - let week_day = if dialect_of!(self is BigQueryDialect) + let week_day = if dialect_of!(self is GenericDialect) && self.consume_token(&Token::LParen) { let week_day = self.parse_identifier()?; diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index adc0681e2..49161ae00 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5411,6 +5411,34 @@ fn parse_interval_all() { 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); + + 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() + ); } #[test] From 539c971cf1c785e3abdc789c04aa9c66f10629ec Mon Sep 17 00:00:00 2001 From: wugeer <1284057728@qq.com> Date: Wed, 8 Jan 2025 21:34:17 +0800 Subject: [PATCH 4/4] simply `weeks` --- src/ast/value.rs | 10 ++-------- src/parser/mod.rs | 13 +------------ tests/sqlparser_common.rs | 28 ---------------------------- 3 files changed, 3 insertions(+), 48 deletions(-) diff --git a/src/ast/value.rs b/src/ast/value.rs index fa291b9bd..45cc06a07 100644 --- a/src/ast/value.rs +++ b/src/ast/value.rs @@ -166,7 +166,7 @@ pub enum DateTimeField { /// /// [BigQuery](https://cloud.google.com/bigquery/docs/reference/standard-sql/date_functions#extract) Week(Option), - Weeks(Option), + Weeks, Day, DayOfWeek, DayOfYear, @@ -227,13 +227,7 @@ impl fmt::Display for DateTimeField { } Ok(()) } - DateTimeField::Weeks(week_day) => { - write!(f, "WEEKS")?; - if let Some(week_day) = week_day { - write!(f, "({week_day})")? - } - Ok(()) - } + DateTimeField::Weeks => write!(f, "WEEKS"), DateTimeField::Day => write!(f, "DAY"), DateTimeField::DayOfWeek => write!(f, "DAYOFWEEK"), DateTimeField::DayOfYear => write!(f, "DAYOFYEAR"), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 009d6f9cf..7b8e7d41d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -2355,18 +2355,7 @@ 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::WEEKS => Ok(DateTimeField::Weeks), Keyword::DAY => Ok(DateTimeField::Day), Keyword::DAYOFWEEK => Ok(DateTimeField::DayOfWeek), Keyword::DAYOFYEAR => Ok(DateTimeField::DayOfYear), diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 49161ae00..adc0681e2 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -5411,34 +5411,6 @@ fn parse_interval_all() { 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); - - 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() - ); } #[test]