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]

Reply via email to