diff --git a/src/dialect/databricks.rs b/src/dialect/databricks.rs index 1ca58ed1a7..324a45ec39 100644 --- a/src/dialect/databricks.rs +++ b/src/dialect/databricks.rs @@ -114,6 +114,11 @@ impl Dialect for DatabricksDialect { false } + /// See + fn supports_interval_multi_units(&self) -> bool { + true + } + // See https://docs.databricks.com/en/sql/language-manual/functions/struct.html fn supports_struct_literal(&self) -> bool { true diff --git a/src/dialect/mod.rs b/src/dialect/mod.rs index 801c5351bd..dfd0a0fba0 100644 --- a/src/dialect/mod.rs +++ b/src/dialect/mod.rs @@ -1182,6 +1182,12 @@ pub trait Dialect: Debug + Any { false } + /// Returns true if an interval literal may list several value / unit pairs, + /// e.g. `INTERVAL 10 YEAR 20 MONTH`. + fn supports_interval_multi_units(&self) -> bool { + false + } + /// 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/spark.rs b/src/dialect/spark.rs index b3be7c9b07..3a1d2bd1ef 100644 --- a/src/dialect/spark.rs +++ b/src/dialect/spark.rs @@ -142,6 +142,11 @@ impl Dialect for SparkSqlDialect { false } + /// See + fn supports_interval_multi_units(&self) -> bool { + true + } + fn supports_bang_not_operator(&self) -> bool { true } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 4f6c77077c..a1adfe59bc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3444,6 +3444,39 @@ impl<'a> Parser<'a> { None }; + // The multi-units form, `INTERVAL 10 YEAR 20 MONTH`, means the same as + // the string form `INTERVAL '10 YEAR 20 MONTH'`, which is what it is + // read as. + if self.dialect.supports_interval_multi_units() { + if let (Some(first_value), Some(first_unit)) = + (Self::interval_unit_value(&value), leading_field.as_ref()) + { + let mut text = format!("{first_value} {first_unit}"); + let mut more = false; + while let Some((v, unit)) = self.maybe_parse(|p| { + let v = p.parse_prefix()?; + let v = Self::interval_unit_value(&v) + .ok_or_else(|| ParserError::ParserError("not an interval value".into()))?; + if !p.next_token_is_temporal_unit() { + return p.expected_ref("an interval unit", p.peek_token_ref()); + } + Ok((v, p.parse_date_time_field()?)) + })? { + text.push_str(&format!(" {v} {unit}")); + more = true; + } + if more { + return Ok(Expr::Interval(Interval { + value: Box::new(Expr::value(Value::SingleQuotedString(text))), + leading_field: None, + leading_precision: None, + last_field: None, + fractional_seconds_precision: None, + })); + } + } + } + let (leading_precision, last_field, fsec_precision) = if leading_field == Some(DateTimeField::Second) { // SQL mandates special syntax for `SECOND TO SECOND` literals. @@ -3478,6 +3511,27 @@ impl<'a> Parser<'a> { })) } + /// The text of one multi-units interval value: a number or string literal, + /// optionally signed. + fn interval_unit_value(value: &Expr) -> Option { + match value { + Expr::Value(v) => match &v.value { + Value::Number(n, _) => Some(n.to_string()), + Value::SingleQuotedString(s) => Some(s.clone()), + _ => None, + }, + Expr::UnaryOp { + op: UnaryOperator::Minus, + expr, + } => Self::interval_unit_value(expr).map(|v| format!("-{v}")), + Expr::UnaryOp { + op: UnaryOperator::Plus, + expr, + } => Self::interval_unit_value(expr), + _ => None, + } + } + /// 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 { diff --git a/tests/sqlparser_common.rs b/tests/sqlparser_common.rs index ef800256d3..31524da26e 100644 --- a/tests/sqlparser_common.rs +++ b/tests/sqlparser_common.rs @@ -6763,6 +6763,48 @@ fn parse_interval_require_unit() { ) } +#[test] +fn parse_interval_multi_units() { + let dialects = all_dialects_where(|d| d.supports_interval_multi_units()); + dialects.one_statement_parses_to( + "SELECT INTERVAL 10 YEAR 20 MONTH", + "SELECT INTERVAL '10 YEAR 20 MONTH'", + ); + dialects.one_statement_parses_to( + "SELECT INTERVAL 1 DAY -2 HOURS '30' MINUTE", + "SELECT INTERVAL '1 DAY -2 HOURS 30 MINUTE'", + ); + dialects.one_statement_parses_to( + "SELECT INTERVAL -1 DAY 2 HOURS AS i", + "SELECT INTERVAL '-1 DAY 2 HOURS' AS i", + ); + dialects.one_statement_parses_to( + "SELECT INTERVAL +1 DAY +2 HOURS", + "SELECT INTERVAL '1 DAY 2 HOURS'", + ); + // A single unit, or a unit followed by something else, is left as it is. + dialects.verified_stmt("SELECT INTERVAL 3 DAY"); + dialects.verified_stmt("SELECT INTERVAL 3 DAY + 1"); + dialects.verified_stmt("SELECT d + INTERVAL 1 DAY FROM t"); + dialects.verified_stmt("SELECT INTERVAL 1 DAY, 2 FROM t"); +} + +#[test] +fn parse_interval_multi_units_unsupported() { + let dialects = all_dialects_where(|d| !d.supports_interval_multi_units()); + for sql in [ + "SELECT INTERVAL 10 YEAR 20 MONTH", + "SELECT INTERVAL 1 DAY 2 HOURS", + ] { + assert!( + dialects.parse_sql_statements(sql).is_err(), + "{sql} should not parse without supports_interval_multi_units" + ); + } + // A single unit still parses. + dialects.verified_stmt("SELECT INTERVAL 1 DAY"); +} + #[test] fn parse_interval_require_qualifier() { let dialects = all_dialects_where(|d| d.require_interval_qualifier());