This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git
The following commit(s) were added to refs/heads/main by this push:
new 923d64fd54 fix: reject mixed-sign intervals in range functions (#25520)
923d64fd54 is described below
commit 923d64fd5448121f538612a7173c99108c108ae7
Author: Liang-Chi Hsieh <[email protected]>
AuthorDate: Sun Sep 20 16:30:46 2026 +0000
fix: reject mixed-sign intervals in range functions (#25520)
## Which issue does this PR close?
N/A
## Rationale for this change
Timestamp and date interval steps contain independent month, day, and
nanosecond components. When those components have mixed signs, the
effective
direction can depend on the current calendar date.
For example, adding `INTERVAL '1 MONTH -29 DAY'` to `2024-01-31`
produces the
same date. Such steps could cause `generate_series` and `range` to
return an
empty result unexpectedly or fail to terminate.
## What changes are included in this PR?
Following DuckDB's behavior, reject interval steps containing both
positive and
negative components.
The validation applies consistently to:
- Scalar/list and table-function forms
- `generate_series` and `range`
- DATE and TIMESTAMP arguments
Zero-step and same-sign interval behavior remains unchanged.
## What is the testing strategy for this PR?
- Added unit tests for positive, negative, and mixed-sign interval
components.
- Added SQL logic tests covering both mixed-sign directions.
- Tested scalar/list and table-function forms.
- Ran `cargo fmt --all`.
- Ran Clippy with `-D warnings` for both affected crates.
- Ran the relevant `table_functions.slt` and `array_range.slt` tests.
## Are there any user-facing changes?
Yes. Mixed-sign interval steps are now rejected with an error instead of
potentially returning an unexpected empty result or producing a
non-terminating
series.
There are no public API changes.
---
datafusion/functions-nested/src/range.rs | 31 +++++++++++++---------
datafusion/functions-table/src/generate_series.rs | 23 +++++++++++++---
.../sqllogictest/test_files/array/array_range.slt | 9 +++++++
.../sqllogictest/test_files/table_functions.slt | 9 +++++++
4 files changed, 57 insertions(+), 15 deletions(-)
diff --git a/datafusion/functions-nested/src/range.rs
b/datafusion/functions-nested/src/range.rs
index f44b0480aa..264bd70cff 100644
--- a/datafusion/functions-nested/src/range.rs
+++ b/datafusion/functions-nested/src/range.rs
@@ -50,7 +50,6 @@ use datafusion_expr::{
TypeSignature, TypeSignatureClass, Volatility,
};
use datafusion_macros::user_doc;
-use std::cmp::Ordering;
use std::iter::from_fn;
use std::str::FromStr;
use std::sync::Arc;
@@ -64,6 +63,22 @@ make_udf_expr_and_func!(
Range::new
);
+fn interval_step_is_negative(
+ name: &str,
+ months: i32,
+ days: i32,
+ nanoseconds: i64,
+) -> Result<bool> {
+ let has_positive_component = months > 0 || days > 0 || nanoseconds > 0;
+ let has_negative_component = months < 0 || days < 0 || nanoseconds < 0;
+
+ if has_positive_component && has_negative_component {
+ return exec_err!("Interval argument to {name} must not have mixed
signs");
+ }
+
+ Ok(has_negative_component)
+}
+
make_udf_expr_and_func!(
GenSeries,
gen_series,
@@ -374,10 +389,11 @@ impl Range {
let stop = stop.value(idx);
let step = step.value(idx);
- let (months, days, _) = IntervalMonthDayNanoType::to_parts(step);
+ let (months, days, nanoseconds) =
IntervalMonthDayNanoType::to_parts(step);
if months == 0 && days == 0 {
return exec_err!("Cannot generate date range less than 1
day.");
}
+ let neg = interval_step_is_negative(self.name(), months, days,
nanoseconds)?;
let stop = if !self.include_upper_bound {
Date32Type::subtract_month_day_nano_opt(stop,
step).ok_or_else(|| {
@@ -390,7 +406,6 @@ impl Range {
stop
};
- let neg = months < 0 || days < 0;
let mut new_date = Some(start);
let values = from_fn(|| {
@@ -459,15 +474,7 @@ impl Range {
if months == 0 && days == 0 && ns == 0 {
return exec_err!("Interval argument to {} must not be 0",
self.name());
}
-
- let neg = TimestampNanosecondType::add_month_day_nano(start, step,
start_tz)
- .ok_or_else(|| {
- exec_datafusion_err!(
- "Cannot generate timestamp range where start + step
overflows"
- )
- })?
- .cmp(&start)
- == Ordering::Less;
+ let neg = interval_step_is_negative(self.name(), months, days,
ns)?;
let stop_dt =
as_datetime_with_timezone::<TimestampNanosecondType>(stop,
stop_tz)
diff --git a/datafusion/functions-table/src/generate_series.rs
b/datafusion/functions-table/src/generate_series.rs
index 94e69e17c3..bca6081ebd 100644
--- a/datafusion/functions-table/src/generate_series.rs
+++ b/datafusion/functions-table/src/generate_series.rs
@@ -583,9 +583,15 @@ fn timestamp_arg_to_nanos(
}
fn validate_interval_step(step: IntervalMonthDayNano) -> Result<()> {
- if step.months == 0 && step.days == 0 && step.nanoseconds == 0 {
+ let has_positive_component = step.months > 0 || step.days > 0 ||
step.nanoseconds > 0;
+ let has_negative_component = step.months < 0 || step.days < 0 ||
step.nanoseconds < 0;
+
+ if !has_positive_component && !has_negative_component {
return plan_err!("Step interval cannot be zero");
}
+ if has_positive_component && has_negative_component {
+ return plan_err!("Step interval cannot have mixed signs");
+ }
Ok(())
}
@@ -939,14 +945,25 @@ impl TableFunctionImpl for RangeFunc {
mod generate_series_tests {
use std::sync::Arc;
- use arrow::datatypes::{DataType, Field, Schema};
+ use arrow::datatypes::{DataType, Field, IntervalMonthDayNano, Schema};
use datafusion_common::Result;
use datafusion_physical_plan::memory::LazyBatchGenerator;
use crate::generate_series::{
- GenSeriesArgs, GenerateSeriesTable, GenericSeriesState,
+ GenSeriesArgs, GenerateSeriesTable, GenericSeriesState,
validate_interval_step,
};
+ #[test]
+ fn rejects_mixed_sign_interval_steps() {
+ assert!(validate_interval_step(IntervalMonthDayNano::new(1, -29,
0)).is_err());
+ assert!(validate_interval_step(IntervalMonthDayNano::new(-1, 29,
0)).is_err());
+
+ validate_interval_step(IntervalMonthDayNano::new(1, 29, 1))
+ .expect("positive interval should be valid");
+ validate_interval_step(IntervalMonthDayNano::new(-1, -29, -1))
+ .expect("negative interval should be valid");
+ }
+
#[test]
fn generate_series_rejects_zero_batch_size() {
let schema = Arc::new(Schema::new(vec![Field::new("a",
DataType::Int64, false)]));
diff --git a/datafusion/sqllogictest/test_files/array/array_range.slt
b/datafusion/sqllogictest/test_files/array/array_range.slt
index b2a39634ef..022740239a 100644
--- a/datafusion/sqllogictest/test_files/array/array_range.slt
+++ b/datafusion/sqllogictest/test_files/array/array_range.slt
@@ -359,6 +359,15 @@ select generate_series(1, 1, 0);
query error DataFusion error: Execution error: Interval argument to
generate_series must not be 0
select generate_series(TIMESTAMP '2000-01-02', TIMESTAMP '2000-01-01',
INTERVAL '0' MINUTE);
+# Mixed-sign interval components can change direction depending on the current
+# calendar date, so they are rejected rather than risking a non-terminating
+# series.
+query error DataFusion error: Execution error: Interval argument to
generate_series must not have mixed signs
+select generate_series(TIMESTAMP '2024-01-31', TIMESTAMP '2024-02-01',
INTERVAL '1 MONTH -29 DAY');
+
+query error DataFusion error: Execution error: Interval argument to range must
not have mixed signs
+select range(DATE '2024-02-01', DATE '2024-01-01', INTERVAL '-1 MONTH 29 DAY');
+
# Range too large to materialize should error instead of panicking
query error DataFusion error: Execution error: Range too large to materialize
select generate_series(0, 9223372036854775807);
diff --git a/datafusion/sqllogictest/test_files/table_functions.slt
b/datafusion/sqllogictest/test_files/table_functions.slt
index 3b9164df3d..168f5a0796 100644
--- a/datafusion/sqllogictest/test_files/table_functions.slt
+++ b/datafusion/sqllogictest/test_files/table_functions.slt
@@ -259,6 +259,15 @@ SELECT * FROM generate_series(DATE '2262-04-10', DATE
'2262-04-11', INTERVAL '2'
----
2262-04-10T00:00:00
+# Mixed-sign interval components do not have a stable direction because month
+# arithmetic depends on the current calendar date. Reject them rather than
+# returning an empty series or failing to make progress.
+statement error DataFusion error: Error during planning: Step interval cannot
have mixed signs
+SELECT * FROM generate_series(TIMESTAMP '2024-01-31', TIMESTAMP '2024-02-01',
INTERVAL '1 MONTH -29 DAY')
+
+statement error DataFusion error: Error during planning: Step interval cannot
have mixed signs
+SELECT * FROM range(DATE '2024-02-01', DATE '2024-01-01', INTERVAL '-1 MONTH
29 DAY')
+
#
# Timestamp bounds of every precision
# https://github.com/apache/datafusion/issues/25169
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]