Skip to content

Commit e3732a5

Browse files
Spark, Databricks: parse multi-unit interval literals
Spark's multi-units interval syntax lists several value / unit pairs, INTERVAL 10 YEAR 20 MONTH, documented as the same as the string form INTERVAL '10 YEAR 20 MONTH'. A new supports_interval_multi_units hook, enabled for SparkSqlDialect and DatabricksDialect, reads the extra pairs into that string form, so the AST is unchanged.
1 parent 14cbf75 commit e3732a5

5 files changed

Lines changed: 112 additions & 0 deletions

File tree

‎src/dialect/databricks.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ impl Dialect for DatabricksDialect {
7575
true
7676
}
7777

78+
/// See <https://spark.apache.org/docs/latest/sql-ref-literals.html#interval-literal>
79+
fn supports_interval_multi_units(&self) -> bool {
80+
true
81+
}
82+
7883
// See https://docs.databricks.com/en/sql/language-manual/functions/struct.html
7984
fn supports_struct_literal(&self) -> bool {
8085
true

‎src/dialect/mod.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,12 @@ pub trait Dialect: Debug + Any {
12281228
false
12291229
}
12301230

1231+
/// Returns true if an interval literal may list several value / unit pairs,
1232+
/// e.g. `INTERVAL 10 YEAR 20 MONTH`.
1233+
fn supports_interval_multi_units(&self) -> bool {
1234+
false
1235+
}
1236+
12311237
/// Returns true if the dialect supports `EXPLAIN` statements with utility options
12321238
/// e.g. `EXPLAIN (ANALYZE TRUE, BUFFERS TRUE) SELECT * FROM tbl;`
12331239
fn supports_explain_with_utility_options(&self) -> bool {

‎src/dialect/spark.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,11 @@ impl Dialect for SparkSqlDialect {
104104
true
105105
}
106106

107+
/// See <https://spark.apache.org/docs/latest/sql-ref-literals.html#interval-literal>
108+
fn supports_interval_multi_units(&self) -> bool {
109+
true
110+
}
111+
107112
fn supports_bang_not_operator(&self) -> bool {
108113
true
109114
}

‎src/parser/mod.rs‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3456,6 +3456,39 @@ impl<'a> Parser<'a> {
34563456
None
34573457
};
34583458

3459+
// The multi-units form, `INTERVAL 10 YEAR 20 MONTH`, means the same as
3460+
// the string form `INTERVAL '10 YEAR 20 MONTH'`, which is what it is
3461+
// read as.
3462+
if self.dialect.supports_interval_multi_units() {
3463+
if let (Some(first_value), Some(first_unit)) =
3464+
(Self::interval_unit_value(&value), leading_field.as_ref())
3465+
{
3466+
let mut text = format!("{first_value} {first_unit}");
3467+
let mut more = false;
3468+
while let Some((v, unit)) = self.maybe_parse(|p| {
3469+
let v = p.parse_prefix()?;
3470+
let v = Self::interval_unit_value(&v)
3471+
.ok_or_else(|| ParserError::ParserError("not an interval value".into()))?;
3472+
if !p.next_token_is_temporal_unit() {
3473+
return p.expected_ref("an interval unit", p.peek_token_ref());
3474+
}
3475+
Ok((v, p.parse_date_time_field()?))
3476+
})? {
3477+
text.push_str(&format!(" {v} {unit}"));
3478+
more = true;
3479+
}
3480+
if more {
3481+
return Ok(Expr::Interval(Interval {
3482+
value: Box::new(Expr::value(Value::SingleQuotedString(text))),
3483+
leading_field: None,
3484+
leading_precision: None,
3485+
last_field: None,
3486+
fractional_seconds_precision: None,
3487+
}));
3488+
}
3489+
}
3490+
}
3491+
34593492
let (leading_precision, last_field, fsec_precision) =
34603493
if leading_field == Some(DateTimeField::Second) {
34613494
// SQL mandates special syntax for `SECOND TO SECOND` literals.
@@ -3490,6 +3523,27 @@ impl<'a> Parser<'a> {
34903523
}))
34913524
}
34923525

3526+
/// The text of one multi-units interval value: a number or string literal,
3527+
/// optionally signed.
3528+
fn interval_unit_value(value: &Expr) -> Option<String> {
3529+
match value {
3530+
Expr::Value(v) => match &v.value {
3531+
Value::Number(n, _) => Some(n.to_string()),
3532+
Value::SingleQuotedString(s) => Some(s.clone()),
3533+
_ => None,
3534+
},
3535+
Expr::UnaryOp {
3536+
op: UnaryOperator::Minus,
3537+
expr,
3538+
} => Self::interval_unit_value(expr).map(|v| format!("-{v}")),
3539+
Expr::UnaryOp {
3540+
op: UnaryOperator::Plus,
3541+
expr,
3542+
} => Self::interval_unit_value(expr),
3543+
_ => None,
3544+
}
3545+
}
3546+
34933547
/// Peek at the next token and determine if it is a temporal unit
34943548
/// like `second`.
34953549
pub fn next_token_is_temporal_unit(&mut self) -> bool {

‎tests/sqlparser_common.rs‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6764,6 +6764,48 @@ fn parse_interval_require_unit() {
67646764
)
67656765
}
67666766

6767+
#[test]
6768+
fn parse_interval_multi_units() {
6769+
let dialects = all_dialects_where(|d| d.supports_interval_multi_units());
6770+
dialects.one_statement_parses_to(
6771+
"SELECT INTERVAL 10 YEAR 20 MONTH",
6772+
"SELECT INTERVAL '10 YEAR 20 MONTH'",
6773+
);
6774+
dialects.one_statement_parses_to(
6775+
"SELECT INTERVAL 1 DAY -2 HOURS '30' MINUTE",
6776+
"SELECT INTERVAL '1 DAY -2 HOURS 30 MINUTE'",
6777+
);
6778+
dialects.one_statement_parses_to(
6779+
"SELECT INTERVAL -1 DAY 2 HOURS AS i",
6780+
"SELECT INTERVAL '-1 DAY 2 HOURS' AS i",
6781+
);
6782+
dialects.one_statement_parses_to(
6783+
"SELECT INTERVAL +1 DAY +2 HOURS",
6784+
"SELECT INTERVAL '1 DAY 2 HOURS'",
6785+
);
6786+
// A single unit, or a unit followed by something else, is left as it is.
6787+
dialects.verified_stmt("SELECT INTERVAL 3 DAY");
6788+
dialects.verified_stmt("SELECT INTERVAL 3 DAY + 1");
6789+
dialects.verified_stmt("SELECT d + INTERVAL 1 DAY FROM t");
6790+
dialects.verified_stmt("SELECT INTERVAL 1 DAY, 2 FROM t");
6791+
}
6792+
6793+
#[test]
6794+
fn parse_interval_multi_units_unsupported() {
6795+
let dialects = all_dialects_where(|d| !d.supports_interval_multi_units());
6796+
for sql in [
6797+
"SELECT INTERVAL 10 YEAR 20 MONTH",
6798+
"SELECT INTERVAL 1 DAY 2 HOURS",
6799+
] {
6800+
assert!(
6801+
dialects.parse_sql_statements(sql).is_err(),
6802+
"{sql} should not parse without supports_interval_multi_units"
6803+
);
6804+
}
6805+
// A single unit still parses.
6806+
dialects.verified_stmt("SELECT INTERVAL 1 DAY");
6807+
}
6808+
67676809
#[test]
67686810
fn parse_interval_require_qualifier() {
67696811
let dialects = all_dialects_where(|d| d.require_interval_qualifier());

0 commit comments

Comments
 (0)