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 11a79a6217 Guard date_trunc lower-bound truncation (#22303)
11a79a6217 is described below
commit 11a79a6217891cff67c8f963265f5673e6f63267
Author: Sean Kenneth Doherty <[email protected]>
AuthorDate: Wed May 27 17:06:07 2026 -0500
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.
---
datafusion/functions/src/datetime/date_trunc.rs | 40 ++++++++++++++++++++--
.../test_files/datetime/timestamps.slt | 7 ++++
2 files changed, 45 insertions(+), 2 deletions(-)
diff --git a/datafusion/functions/src/datetime/date_trunc.rs
b/datafusion/functions/src/datetime/date_trunc.rs
index 784f593c25..a4b244405c 100644
--- a/datafusion/functions/src/datetime/date_trunc.rs
+++ b/datafusion/functions/src/datetime/date_trunc.rs
@@ -15,6 +15,7 @@
// specific language governing permissions and limitations
// under the License.
+use std::fmt;
use std::num::NonZeroI64;
use std::ops::{Add, Sub};
use std::str::FromStr;
@@ -135,6 +136,24 @@ impl DateTruncGranularity {
}
}
+impl fmt::Display for DateTruncGranularity {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let value = match self {
+ Self::Microsecond => "microsecond",
+ Self::Millisecond => "millisecond",
+ Self::Second => "second",
+ Self::Minute => "minute",
+ Self::Hour => "hour",
+ Self::Day => "day",
+ Self::Week => "week",
+ Self::Month => "month",
+ Self::Quarter => "quarter",
+ Self::Year => "year",
+ };
+ f.write_str(value)
+ }
+}
+
#[user_doc(
doc_section(label = "Time and Date Functions"),
description = "Truncates a timestamp or time value to a specified
precision.",
@@ -629,6 +648,7 @@ fn date_trunc_coarse(
value: i64,
tz: Option<Tz>,
) -> Result<i64> {
+ let input = value;
let value = match tz {
Some(tz) => {
// Use chrono DateTime<Tz> to clear the various fields because
need to clear per timezone,
@@ -645,8 +665,11 @@ fn date_trunc_coarse(
}
}?;
- // `with_x(0)` are infallible because `0` are always a valid
- Ok(value.unwrap())
+ value.ok_or_else(|| {
+ exec_datafusion_err!(
+ "Timestamp {input} out of range after truncating to {granularity}"
+ )
+ })
}
/// Fast path for fine granularities (hour and smaller) that can be handled
@@ -879,6 +902,19 @@ mod tests {
});
}
+ #[test]
+ fn date_trunc_out_of_range_lower_bound_returns_error() {
+ let timestamp =
string_to_timestamp_nanos("1677-09-22T00:00:00Z").unwrap();
+ let err = date_trunc_coarse(DateTruncGranularity::Year, timestamp,
None)
+ .unwrap_err()
+ .to_string();
+
+ assert!(
+ err.contains("out of range after truncating to year"),
+ "{err}"
+ );
+ }
+
#[test]
fn test_date_trunc_timezones() {
let cases = [
diff --git a/datafusion/sqllogictest/test_files/datetime/timestamps.slt
b/datafusion/sqllogictest/test_files/datetime/timestamps.slt
index e045abc0f2..958ff86b4f 100644
--- a/datafusion/sqllogictest/test_files/datetime/timestamps.slt
+++ b/datafusion/sqllogictest/test_files/datetime/timestamps.slt
@@ -5345,6 +5345,13 @@ SELECT to_timestamp_millis(arrow_cast(-1.9, 'Float64'));
----
1969-12-31T23:59:59.999
+# Regression test for https://github.com/apache/datafusion/issues/22214
+query error .*out of range after truncating to year
+SELECT date_trunc(
+ 'year',
+ arrow_cast(TIMESTAMP '1677-09-22 00:00:00', 'Timestamp(Nanosecond, None)')
+);
+
##########
## Common timestamp data
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]