Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions src/dialect/databricks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ impl Dialect for DatabricksDialect {
false
}

/// See <https://spark.apache.org/docs/latest/sql-ref-literals.html#interval-literal>
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
Expand Down
6 changes: 6 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions src/dialect/spark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ impl Dialect for SparkSqlDialect {
false
}

/// See <https://spark.apache.org/docs/latest/sql-ref-literals.html#interval-literal>
fn supports_interval_multi_units(&self) -> bool {
true
}

fn supports_bang_not_operator(&self) -> bool {
true
}
Expand Down
54 changes: 54 additions & 0 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<String> {
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,
}
}
Comment thread
moshap-firebolt marked this conversation as resolved.

/// 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 {
Expand Down
42 changes: 42 additions & 0 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Loading