Skip to content

Spark, Databricks: allow an interval string without a unit - #2614

Open
moshap-firebolt wants to merge 2 commits into
apache:mainfrom
firebolt-analytics:moshap/upstream-spark-interval-strings
Open

moshap-firebolt wants to merge 2 commits into
apache:mainfrom
firebolt-analytics:moshap/upstream-spark-interval-strings

Conversation

@moshap-firebolt

@moshap-firebolt moshap-firebolt commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Spark's multi-units interval syntax lets the string carry its own units, so no unit follows the literal:

SELECT INTERVAL '1 YEAR 2 DAYS 3 HOURS';
SELECT INTERVAL '2 seconds' * 2;

Both SparkSqlDialect and DatabricksDialect set require_interval_qualifier, so these fail with INTERVAL requires a unit after the literal value. Databricks Runtime parses with Spark's grammar and accepts them too; the Databricks docs show only the qualified form.

This sets require_interval_qualifier to false for both dialects. A unit after a literal is still parsed, so INTERVAL '1' DAY, INTERVAL 12 HOURS and INTERVAL '1-2' YEAR TO MONTH are unchanged. The form this gives up is an expression as the interval value (INTERVAL 1 + 1 DAY), which Spark's grammar does not allow: it takes a literal there.

Tests cover the string forms, the qualified forms, signs and arithmetic for both dialects.

Spark's multi-units interval syntax lets the string carry its own
units, as in INTERVAL '1 YEAR 2 DAYS 3 HOURS', so no unit follows the
literal. Databricks Runtime parses with the same grammar. A unit after
a literal is still read; the expression form this gives up
(INTERVAL 1 + 1 DAY) is not Spark syntax.
@codecov-commenter

codecov-commenter commented Oct 5, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.34884% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.16%. Comparing base (14cbf75) to head (8ea987e).

Files with missing lines Patch % Lines
src/dialect/spark.rs 83.33% 0 Missing and 1 partial ⚠️
src/parser/mod.rs 96.42% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2614      +/-   ##
==========================================
+ Coverage   81.14%   81.16%   +0.01%     
==========================================
  Files          42       42              
  Lines       33736    33778      +42     
  Branches    33736    33778      +42     
==========================================
+ Hits        27376    27416      +40     
  Misses       2797     2797              
- Partials     3563     3565       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@LucaCappelletti94 LucaCappelletti94 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could gate unqualified intervals on string literals through a dialect capability, retaining qualifiers for numeric literals and excluding identifiers from literal values.

Setting require_interval_qualifier() to false makes both dialects accept SELECT INTERVAL 1, which Spark 4.0.1 rejects, and misparse SELECT interval FROM t into SELECT INTERVAL FROM AS t, losing the table reference.

I added some red tests to help out.

Comment thread tests/sqlparser_spark.rs
Expr::Interval(i) => assert!(i.leading_field.is_none()),
other => panic!("Expected an interval, got {other:?}"),
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}
#[test]
fn test_interval_numeric_requires_unit() {
assert_eq!(
spark()
.parse_sql_statements("SELECT INTERVAL 1")
.unwrap_err()
.to_string(),
"sql parser error: INTERVAL requires a unit after the literal value"
);
}
#[test]
fn test_interval_rejects_value_arithmetic() {
assert!(spark()
.parse_sql_statements("SELECT INTERVAL 1 + 1 DAY")
.is_err());
}
#[test]
fn test_interval_preserves_column_query() {
let query = spark()
.run_parser_method("SELECT interval FROM t", |parser| parser.parse_query())
.unwrap();
let SetExpr::Select(select) = *query.body else {
panic!("Expected SELECT");
};
assert_eq!(
select.from,
vec![TableWithJoins {
relation: table("t"),
joins: vec![],
}]
);
assert_eq!(
select.projection,
vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident::new(
"interval"
)))]
);
}

Expr::Interval(i) => assert!(i.leading_field.is_none()),
other => panic!("Expected an interval, got {other:?}"),
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}
#[test]
fn test_interval_numeric_requires_unit() {
assert_eq!(
databricks()
.parse_sql_statements("SELECT INTERVAL 1")
.unwrap_err()
.to_string(),
"sql parser error: INTERVAL requires a unit after the literal value"
);
}
#[test]
fn test_interval_rejects_value_arithmetic() {
assert!(databricks()
.parse_sql_statements("SELECT INTERVAL 1 + 1 DAY")
.is_err());
}
#[test]
fn test_interval_preserves_column_query() {
let query = databricks()
.run_parser_method("SELECT interval FROM t", |parser| parser.parse_query())
.unwrap();
let SetExpr::Select(select) = *query.body else {
panic!("Expected SELECT");
};
assert_eq!(
select.from,
vec![TableWithJoins {
relation: table("t"),
joins: vec![],
}]
);
assert_eq!(
select.projection,
vec![SelectItem::UnnamedExpr(Expr::Identifier(Ident::new(
"interval"
)))]
);
}

Address review: turning require_interval_qualifier off accepted
INTERVAL 1 and read SELECT interval FROM t as SELECT INTERVAL FROM AS t.
Keep it on and add supports_interval_string_without_qualifier: the value
must be a literal, and only an unsigned string may omit the qualifier.
INTERVAL is not reserved for these dialects, so SELECT interval FROM t
reads a column.
@moshap-firebolt

Copy link
Copy Markdown
Contributor Author

You could gate unqualified intervals on string literals through a dialect capability, retaining qualifiers for numeric literals and excluding identifiers from literal values.

Setting require_interval_qualifier() to false makes both dialects accept SELECT INTERVAL 1, which Spark 4.0.1 rejects, and misparse SELECT interval FROM t into SELECT INTERVAL FROM AS t, losing the table reference.

Right, so kept require_interval_qualifier on true and added supports_interval_string_without_qualifier enabled for Spark/Databricks; also unreserved INTERVAL for both dialects similar to PG

I added some red tests to help out.

Thanks - that was super helpful 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants