Skip to content

Commit 53ef65a

Browse files
committed
fix: generate_series overflow panics at i64 boundary and out-of-range dates
Two overflow fixes in the generate_series/range table functions: - Integer series: advancing past i64::MAX (or i64::MIN for negative steps) panicked in debug builds and silently wrapped in release. advance() now uses checked_add and clamps the series end so iteration stops after the last reachable value, matching PostgreSQL/DuckDB (e.g. generate_series(9223372036854775806, 9223372036854775807, 2) returns one row 9223372036854775806). - Date series: converting Date32 days to timestamp nanoseconds used an unchecked multiplication, panicking at planning time for dates outside the nanosecond timestamp range. It now returns a planning error. Supersedes #22250 (closed unmerged); the integer-side approach follows the one approved there. Closes #22208, closes #22193.
1 parent 39e2f23 commit 53ef65a

2 files changed

Lines changed: 81 additions & 8 deletions

File tree

datafusion/functions-table/src/generate_series.rs

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use async_trait::async_trait;
2727
use datafusion_catalog::TableFunctionImpl;
2828
use datafusion_catalog::TableProvider;
2929
use datafusion_catalog::{Session, TableFunctionArgs};
30-
use datafusion_common::{Result, ScalarValue, plan_err};
30+
use datafusion_common::{Result, ScalarValue, plan_datafusion_err, plan_err};
3131
use datafusion_expr::{Expr, TableType};
3232
use datafusion_physical_expr::PhysicalSortExpr;
3333
use datafusion_physical_expr::expressions::Column;
@@ -80,7 +80,12 @@ pub trait SeriesValue: fmt::Debug + Clone + Send + Sync + 'static {
8080
fn should_stop(&self, end: Self, step: &Self::StepType, include_end: bool) -> bool;
8181

8282
/// Advance to the next value in the series
83-
fn advance(&mut self, step: &Self::StepType) -> Result<()>;
83+
///
84+
/// If advancing would overflow the value range, `end` is updated so that
85+
/// the series terminates after the current value (matching the behavior
86+
/// of PostgreSQL and DuckDB, which return the reachable values instead of
87+
/// erroring).
88+
fn advance(&mut self, end: &mut Self, step: &Self::StepType) -> Result<()>;
8489

8590
/// Create an Arrow array from a vector of values
8691
fn create_array(&self, values: Vec<Self::ValueType>) -> Result<ArrayRef>;
@@ -100,8 +105,19 @@ impl SeriesValue for i64 {
100105
reach_end_int64(*self, end, *step, include_end)
101106
}
102107

103-
fn advance(&mut self, step: &Self::StepType) -> Result<()> {
104-
*self += step;
108+
fn advance(&mut self, end: &mut Self, step: &Self::StepType) -> Result<()> {
109+
if let Some(next) = self.checked_add(*step) {
110+
*self = next;
111+
} else {
112+
// Advancing would overflow: clamp `end` so the series stops after
113+
// the current (last reachable) value instead of panicking or
114+
// wrapping around.
115+
*end = if *step > 0 {
116+
self.saturating_sub(1)
117+
} else {
118+
self.saturating_add(1)
119+
};
120+
}
105121
Ok(())
106122
}
107123

@@ -155,7 +171,7 @@ impl SeriesValue for TimestampValue {
155171
}
156172
}
157173

158-
fn advance(&mut self, step: &Self::StepType) -> Result<()> {
174+
fn advance(&mut self, _end: &mut Self, step: &Self::StepType) -> Result<()> {
159175
let tz = self
160176
.parsed_tz
161177
.unwrap_or_else(|| Tz::from_str("+00:00").unwrap());
@@ -417,7 +433,15 @@ impl<T: SeriesValue> LazyBatchGenerator for GenericSeriesState<T> {
417433
.should_stop(self.end.clone(), &self.step, self.include_end)
418434
{
419435
buf.push(self.current.to_value_type());
420-
self.current.advance(&self.step)?;
436+
if self
437+
.current
438+
.should_stop(self.end.clone(), &self.step, false)
439+
{
440+
self.current.advance(&mut self.end, &self.step)?;
441+
break;
442+
}
443+
444+
self.current.advance(&mut self.end, &self.step)?;
421445
}
422446

423447
if buf.is_empty() {
@@ -740,8 +764,20 @@ impl GenerateSeriesFuncImpl {
740764
// Date32 is days since 1970-01-01, so multiply by nanoseconds per day
741765
const NANOS_PER_DAY: i64 = 24 * 60 * 60 * 1_000_000_000;
742766

743-
let start_ts = start_date as i64 * NANOS_PER_DAY;
744-
let end_ts = end_date as i64 * NANOS_PER_DAY;
767+
// Dates outside the nanosecond timestamp range (1677-09-21 to
768+
// 2262-04-11) cannot be represented; return an error instead of
769+
// panicking (debug) or silently wrapping (release).
770+
let date_to_ts_nanos = |date: i32, arg: &str| {
771+
(date as i64).checked_mul(NANOS_PER_DAY).ok_or_else(|| {
772+
plan_datafusion_err!(
773+
"{arg} for {} is out of range of nanosecond timestamps",
774+
self.name
775+
)
776+
})
777+
};
778+
779+
let start_ts = date_to_ts_nanos(start_date, "First argument")?;
780+
let end_ts = date_to_ts_nanos(end_date, "Second argument")?;
745781

746782
// Validate step interval
747783
validate_interval_step(step_interval)?;

datafusion/sqllogictest/test_files/table_functions.slt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,43 @@ SELECT * FROM generate_series(1, 2, 3, 4)
197197
statement error DataFusion error: Error during planning: Argument \#1 must be an INTEGER, TIMESTAMP, DATE or NULL, got Utf8
198198
SELECT * FROM generate_series('foo', 'bar')
199199

200+
# Regression test for https://github.com/apache/datafusion/issues/22208
201+
# A step that would overflow i64 after the last reachable value must return the
202+
# reachable values instead of panicking, matching PostgreSQL/DuckDB behavior.
203+
query I
204+
SELECT * FROM generate_series(9223372036854775806, 9223372036854775807, 2)
205+
----
206+
9223372036854775806
207+
208+
# Same, in the descending direction
209+
query I
210+
SELECT * FROM generate_series(-9223372036854775806, -9223372036854775808, -2)
211+
----
212+
-9223372036854775806
213+
-9223372036854775808
214+
215+
# Landing exactly on i64::MAX must include it
216+
query I
217+
SELECT * FROM generate_series(9223372036854775805, 9223372036854775807, 2)
218+
----
219+
9223372036854775805
220+
9223372036854775807
221+
222+
# Same overflow behavior for `range` (end exclusive)
223+
query I
224+
SELECT * FROM range(9223372036854775806, 9223372036854775807, 2)
225+
----
226+
9223372036854775806
227+
228+
# Regression test for https://github.com/apache/datafusion/issues/22193
229+
# Dates outside the nanosecond timestamp range must produce a clean planning
230+
# error instead of panicking (debug) or silently wrapping (release).
231+
statement error DataFusion error: Error during planning: First argument for generate_series is out of range of nanosecond timestamps
232+
SELECT * FROM generate_series(DATE '0001-01-01', DATE '2000-01-01', INTERVAL '1' DAY)
233+
234+
statement error DataFusion error: Error during planning: Second argument for generate_series is out of range of nanosecond timestamps
235+
SELECT * FROM generate_series(DATE '2000-01-01', DATE '3000-01-01', INTERVAL '1' DAY)
236+
200237
# UDF and UDTF `generate_series` can be used simultaneously
201238
query ? rowsort
202239
SELECT generate_series(1, t1.end) FROM generate_series(3, 5) as t1(end)

0 commit comments

Comments
 (0)