Skip to content

Commit 11a79a6

Browse files
Guard date_trunc lower-bound truncation (#22303)
## Which issue does this PR close? - Closes #22214. ## Rationale for this change Truncating a near-lower-bound nanosecond timestamp to a coarser calendar unit can produce a timestamp outside Arrow's nanosecond range. `date_trunc_coarse` already used fallible timestamp conversion, but it unwrapped the final conversion back to nanoseconds and could panic. ## What changes are included in this PR? - Convert the final out-of-range truncation case into a DataFusion execution error. - Add a small string helper for user-facing granularity names in the error. - Add a unit regression and a sqllogictest regression for lower-bound nanosecond truncation. ## Are these changes tested? Yes: - `cargo fmt --check` - `git diff --check` - `CARGO_TARGET_DIR=/home/sean/Projects/datafusion-runtime-set-nonascii/target CARGO_BUILD_JOBS=2 cargo test -p datafusion-functions --lib date_trunc_out_of_range_lower_bound_returns_error` - `CARGO_TARGET_DIR=/home/sean/Projects/datafusion-runtime-set-nonascii/target CARGO_BUILD_JOBS=2 cargo test -p datafusion-sqllogictest --test sqllogictests -- date_trunc_boundaries.slt` - `CARGO_TARGET_DIR=/home/sean/Projects/datafusion-runtime-set-nonascii/target CARGO_BUILD_JOBS=2 cargo clippy -p datafusion-functions --lib -- -D warnings` ## Are there any user-facing changes? Instead of panicking, out-of-range `date_trunc` results now return an error.
1 parent 070d013 commit 11a79a6

2 files changed

Lines changed: 45 additions & 2 deletions

File tree

datafusion/functions/src/datetime/date_trunc.rs

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18+
use std::fmt;
1819
use std::num::NonZeroI64;
1920
use std::ops::{Add, Sub};
2021
use std::str::FromStr;
@@ -135,6 +136,24 @@ impl DateTruncGranularity {
135136
}
136137
}
137138

139+
impl fmt::Display for DateTruncGranularity {
140+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141+
let value = match self {
142+
Self::Microsecond => "microsecond",
143+
Self::Millisecond => "millisecond",
144+
Self::Second => "second",
145+
Self::Minute => "minute",
146+
Self::Hour => "hour",
147+
Self::Day => "day",
148+
Self::Week => "week",
149+
Self::Month => "month",
150+
Self::Quarter => "quarter",
151+
Self::Year => "year",
152+
};
153+
f.write_str(value)
154+
}
155+
}
156+
138157
#[user_doc(
139158
doc_section(label = "Time and Date Functions"),
140159
description = "Truncates a timestamp or time value to a specified precision.",
@@ -629,6 +648,7 @@ fn date_trunc_coarse(
629648
value: i64,
630649
tz: Option<Tz>,
631650
) -> Result<i64> {
651+
let input = value;
632652
let value = match tz {
633653
Some(tz) => {
634654
// Use chrono DateTime<Tz> to clear the various fields because need to clear per timezone,
@@ -645,8 +665,11 @@ fn date_trunc_coarse(
645665
}
646666
}?;
647667

648-
// `with_x(0)` are infallible because `0` are always a valid
649-
Ok(value.unwrap())
668+
value.ok_or_else(|| {
669+
exec_datafusion_err!(
670+
"Timestamp {input} out of range after truncating to {granularity}"
671+
)
672+
})
650673
}
651674

652675
/// Fast path for fine granularities (hour and smaller) that can be handled
@@ -879,6 +902,19 @@ mod tests {
879902
});
880903
}
881904

905+
#[test]
906+
fn date_trunc_out_of_range_lower_bound_returns_error() {
907+
let timestamp = string_to_timestamp_nanos("1677-09-22T00:00:00Z").unwrap();
908+
let err = date_trunc_coarse(DateTruncGranularity::Year, timestamp, None)
909+
.unwrap_err()
910+
.to_string();
911+
912+
assert!(
913+
err.contains("out of range after truncating to year"),
914+
"{err}"
915+
);
916+
}
917+
882918
#[test]
883919
fn test_date_trunc_timezones() {
884920
let cases = [

datafusion/sqllogictest/test_files/datetime/timestamps.slt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5345,6 +5345,13 @@ SELECT to_timestamp_millis(arrow_cast(-1.9, 'Float64'));
53455345
----
53465346
1969-12-31T23:59:59.999
53475347

5348+
# Regression test for https://github.com/apache/datafusion/issues/22214
5349+
query error .*out of range after truncating to year
5350+
SELECT date_trunc(
5351+
'year',
5352+
arrow_cast(TIMESTAMP '1677-09-22 00:00:00', 'Timestamp(Nanosecond, None)')
5353+
);
5354+
53485355

53495356
##########
53505357
## Common timestamp data

0 commit comments

Comments
 (0)