diff --git a/fuzz/fuzz_seeds/postgres_interval_alias_after_value.sql b/fuzz/fuzz_seeds/postgres_interval_alias_after_value.sql new file mode 100644 index 0000000000..34747071da --- /dev/null +++ b/fuzz/fuzz_seeds/postgres_interval_alias_after_value.sql @@ -0,0 +1 @@ +SELECT INTERVAL '1' WEEK, INTERVAL '1' DAYS, INTERVAL '1' DAY diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index f296a2b452..b20fb610ee 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1369,6 +1369,47 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if `kw` can start the qualifier of an `INTERVAL` value, e.g. `DAY` in `INTERVAL '1' DAY`. + fn is_interval_qualifier(&self, kw: Keyword) -> bool { + matches!( + kw, + 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 + | Keyword::DOY + | Keyword::EPOCH + | Keyword::ISODOW + | Keyword::ISOYEAR + | Keyword::JULIAN + | Keyword::MICROSECOND + | Keyword::MICROSECONDS + | Keyword::MILLENIUM + | Keyword::MILLENNIUM + | Keyword::MILLISECOND + | Keyword::MILLISECONDS + | Keyword::NANOSECOND + | Keyword::NANOSECONDS + | Keyword::QUARTER + | Keyword::TIMEZONE + | Keyword::TIMEZONE_HOUR + | Keyword::TIMEZONE_MINUTE + ) + } + /// Returns true if the dialect supports `EXPLAIN` statements with utility options /// e.g. `EXPLAIN (ANALYZE TRUE, BUFFERS TRUE) SELECT * FROM tbl;` fn supports_explain_with_utility_options(&self) -> bool { diff --git a/src/dialect/postgresql.rs b/src/dialect/postgresql.rs index 9d15a87c97..15ae25cf96 100644 --- a/src/dialect/postgresql.rs +++ b/src/dialect/postgresql.rs @@ -544,6 +544,19 @@ impl Dialect for PostgreSqlDialect { true } + /// See `opt_interval` in + fn is_interval_qualifier(&self, kw: Keyword) -> bool { + matches!( + kw, + Keyword::YEAR + | Keyword::MONTH + | Keyword::DAY + | Keyword::HOUR + | Keyword::MINUTE + | Keyword::SECOND + ) + } + fn supports_insert_table_alias(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 58ab4ca112..3cc55b5976 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3566,47 +3566,10 @@ impl<'a> Parser<'a> { /// Peek at the next token and determine if it is a temporal unit /// like `second`. pub fn next_token_is_temporal_unit(&mut self) -> bool { - if let Token::Word(word) = &self.peek_token_ref().token { - 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 - | Keyword::DOY - | Keyword::EPOCH - | Keyword::ISODOW - | Keyword::ISOYEAR - | Keyword::JULIAN - | Keyword::MICROSECOND - | Keyword::MICROSECONDS - | Keyword::MILLENIUM - | Keyword::MILLENNIUM - | Keyword::MILLISECOND - | Keyword::MILLISECONDS - | Keyword::NANOSECOND - | Keyword::NANOSECONDS - | Keyword::QUARTER - | Keyword::TIMEZONE - | Keyword::TIMEZONE_HOUR - | Keyword::TIMEZONE_MINUTE - ) - } else { - false - } + matches!( + &self.peek_token_ref().token, + Token::Word(word) if self.dialect.is_interval_qualifier(word.keyword) + ) } /// Syntax diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index 379e37a660..2188d278d5 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -7080,8 +7080,11 @@ fn parse_interval_all() { expr_from_projection(only(&select.projection)), ); + // PostgreSQL reads `WEEK` and the plural units as a column alias. + let dialects = all_dialects_except(|d| d.is::()); + let sql = "SELECT INTERVAL 5 DAYS"; - let select = verified_only_select(sql); + let select = dialects.verified_only_select(sql); assert_eq!( &Expr::Interval(Interval { value: Box::new(Expr::value(number("5"))), @@ -7122,18 +7125,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"); + dialects.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"); + dialects.verified_only_select("SELECT INTERVAL '1' YEARS"); + dialects.verified_only_select("SELECT INTERVAL '1' MONTHS"); + dialects.verified_only_select("SELECT INTERVAL '1' WEEKS"); + dialects.verified_only_select("SELECT INTERVAL '1' DAYS"); + dialects.verified_only_select("SELECT INTERVAL '1' HOURS"); + dialects.verified_only_select("SELECT INTERVAL '1' MINUTES"); + dialects.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"); @@ -7143,18 +7146,18 @@ 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"); + dialects.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"); + dialects.verified_only_select("SELECT INTERVAL 1 YEARS"); + dialects.verified_only_select("SELECT INTERVAL 1 MONTHS"); + dialects.verified_only_select("SELECT INTERVAL 1 WEEKS"); + dialects.verified_only_select("SELECT INTERVAL 1 DAYS"); + dialects.verified_only_select("SELECT INTERVAL 1 HOURS"); + dialects.verified_only_select("SELECT INTERVAL 1 MINUTES"); + dialects.verified_only_select("SELECT INTERVAL 1 SECONDS"); verified_only_select( "SELECT '2 years 15 months 100 weeks 99 hours 123456789 milliseconds'::INTERVAL", ); diff --git a/tests/sqlparser_postgres.rs b/tests/sqlparser_postgres.rs index 7fb4bb23fe..7249394de9 100644 --- a/tests/sqlparser_postgres.rs +++ b/tests/sqlparser_postgres.rs @@ -11671,3 +11671,40 @@ fn parse_drop_group_is_dialect_gated() { )) ); } + +#[test] +fn parse_interval_followed_by_alias() { + for alias in [ + "WEEK", "WEEKS", "DAYS", "YEARS", "DECADE", "QUARTER", "EPOCH", + ] { + let select = pg().verified_only_select_with_canonical( + &format!("SELECT INTERVAL '1' {alias}"), + &format!("SELECT INTERVAL '1' AS {alias}"), + ); + match only(&select.projection) { + SelectItem::ExprWithAlias { + expr: Expr::Interval(interval), + alias: ident, + } => { + assert_eq!(interval.leading_field, None); + assert_eq!(ident.value, alias); + } + item => panic!("unexpected projection: {item:?}"), + } + } + + let select = pg().verified_only_select("SELECT INTERVAL '1' DAY"); + assert!(matches!( + only(&select.projection), + SelectItem::UnnamedExpr(Expr::Interval(Interval { + leading_field: Some(DateTimeField::Day), + .. + })) + )); + + assert_eq!( + pg().parse_sql_statements("SELECT 1 WHERE x > INTERVAL '1' WEEK") + .unwrap_err(), + ParserError::ParserError("Expected: end of statement, found: WEEK".to_string()) + ); +}