This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-25131-90dc4365886686b0bc19430574f9a1804e12c77b
in repository https://gitbox.apache.org/repos/asf/datafusion.git

commit b239d041b5430efa87755e4f42a3726da2db45a8
Author: Ben Bellick <[email protected]>
AuthorDate: Fri Sep 11 15:41:56 2026 +0000

    fix: prevent date_trunc array underflow (#25131)
    
    ## Which issue does this PR close?
    
    - Closes #25129.
    
    ## Rationale for this change
    
    `date_trunc` can panic in debug builds or return an incorrect timestamp
    in release builds when its array fast path truncates a timestamp near
    `i64::MIN`.
    
    ## What changes are included in this PR?
    
    Use checked subtraction in the fine-granularity array path and return an
    execution error when the truncated timestamp cannot be represented.
    
    ## What is the testing strategy for this PR?
    
    Added `test_date_trunc_fine_granularity_underflow`, which passes
    `i64::MIN` through the nanosecond timestamp array path and verifies that
    `date_trunc` returns an execution error.
    
    The original SQL reproduction was also verified with the locally
    compiled DataFusion CLI.
    
    ## Are there any user-facing changes?
    
    Invalid extreme timestamp truncations now return an execution error
    instead of panicking or returning a wrapped timestamp.
    
    ---
    Note: This PR was developed with AI assistance. All changes have been
    reviewed, and I take full responsibility for this contribution.
---
 datafusion/functions/src/datetime/date_trunc.rs | 137 ++++++++++++++++++++++--
 1 file changed, 127 insertions(+), 10 deletions(-)

diff --git a/datafusion/functions/src/datetime/date_trunc.rs 
b/datafusion/functions/src/datetime/date_trunc.rs
index 22c8af262a..0ed61a29b3 100644
--- a/datafusion/functions/src/datetime/date_trunc.rs
+++ b/datafusion/functions/src/datetime/date_trunc.rs
@@ -778,13 +778,30 @@ fn general_date_trunc_array_fine_granularity<T: 
ArrowTimestampType>(
 
     if let Some(unit) = unit {
         let unit = unit.get();
-        let array = PrimitiveArray::<T>::from_iter_values_with_nulls(
-            array
-                .values()
-                .iter()
-                .map(|v| *v - i64::rem_euclid(*v, unit)),
-            array.nulls().cloned(),
-        )
+        // Truncation can only underflow within one `unit` of `i64::MIN`.
+        // Track that possibility while computing the common case so the loop
+        // remains infallible and can be vectorized.
+        let underflow_bound = i64::MIN + unit;
+        let mut maybe_underflow = false;
+        let values: Vec<i64> = array
+            .values()
+            .iter()
+            .map(|value| {
+                maybe_underflow |= *value < underflow_bound;
+                value.wrapping_sub(value.rem_euclid(unit))
+            })
+            .collect();
+        let array: PrimitiveArray<T> = if maybe_underflow {
+            array.try_unary(|value| {
+                value.checked_sub(value.rem_euclid(unit)).ok_or_else(|| {
+                    exec_datafusion_err!(
+                        "Timestamp {value} out of range after truncating to 
{granularity}"
+                    )
+                })
+            })?
+        } else {
+            PrimitiveArray::new(values.into(), array.nulls().cloned())
+        }
         .with_timezone_opt(tz_opt);
         Ok(Arc::new(array))
     } else {
@@ -856,12 +873,17 @@ mod tests {
     use std::sync::Arc;
 
     use crate::datetime::date_trunc::{
-        DateTruncFunc, date_trunc_coarse, parse_granularity,
+        DateTruncFunc, NANOS_PER_MICROSECOND, date_trunc_coarse,
+        general_date_trunc_array_fine_granularity, parse_granularity,
     };
 
     use arrow::array::cast::as_primitive_array;
-    use arrow::array::types::TimestampNanosecondType;
-    use arrow::array::{Array, TimestampNanosecondArray};
+    use arrow::array::types::{ArrowTimestampType, TimestampNanosecondType};
+    use arrow::array::{
+        Array, PrimitiveArray, TimestampMicrosecondArray, 
TimestampMillisecondArray,
+        TimestampNanosecondArray, TimestampSecondArray,
+    };
+    use arrow::buffer::NullBuffer;
     use arrow::compute::DatePart;
     use arrow::compute::kernels::cast_utils::string_to_timestamp_nanos;
     use arrow::datatypes::{DataType, Field, TimeUnit};
@@ -1339,6 +1361,101 @@ mod tests {
         }
     }
 
+    fn assert_fine_granularity_underflow<T: ArrowTimestampType>(
+        array: PrimitiveArray<T>,
+        granularity: DatePart,
+    ) {
+        let error =
+            general_date_trunc_array_fine_granularity(T::UNIT, &array, 
granularity, None)
+                .unwrap_err();
+        assert!(
+            error
+                .strip_backtrace()
+                .contains("Timestamp -9223372036854775808 out of range"),
+            "unexpected error: {error}"
+        );
+    }
+
+    #[test]
+    fn test_date_trunc_fine_granularity_underflow_for_each_timestamp_unit() {
+        assert_fine_granularity_underflow(
+            TimestampSecondArray::from(vec![i64::MIN]),
+            DatePart::Minute,
+        );
+        assert_fine_granularity_underflow(
+            TimestampMillisecondArray::from(vec![i64::MIN]),
+            DatePart::Second,
+        );
+        assert_fine_granularity_underflow(
+            TimestampMicrosecondArray::from(vec![i64::MIN]),
+            DatePart::Millisecond,
+        );
+        assert_fine_granularity_underflow(
+            TimestampNanosecondArray::from(vec![i64::MIN]),
+            DatePart::Microsecond,
+        );
+    }
+
+    #[test]
+    fn test_date_trunc_fine_granularity_minimum_safe_input() {
+        // The boundary below i64::MIN is unrepresentable. Inputs become safe
+        // at the next microsecond boundary.
+        let distance_to_next_boundary =
+            NANOS_PER_MICROSECOND - i64::MIN.rem_euclid(NANOS_PER_MICROSECOND);
+        let minimum_safe_input = i64::MIN + distance_to_next_boundary;
+        let unsafe_input = 
TimestampNanosecondArray::from(vec![minimum_safe_input - 1]);
+        assert!(
+            general_date_trunc_array_fine_granularity(
+                TimeUnit::Nanosecond,
+                &unsafe_input,
+                DatePart::Microsecond,
+                None,
+            )
+            .is_err()
+        );
+
+        let input = TimestampNanosecondArray::from(vec![
+            Some(minimum_safe_input),
+            Some(minimum_safe_input + 1),
+            None,
+        ]);
+        let result = general_date_trunc_array_fine_granularity(
+            TimeUnit::Nanosecond,
+            &input,
+            DatePart::Microsecond,
+            None,
+        )
+        .unwrap();
+        let result = as_primitive_array::<TimestampNanosecondType>(&result);
+        let expected = TimestampNanosecondArray::from(vec![
+            Some(minimum_safe_input),
+            Some(minimum_safe_input),
+            None,
+        ]);
+        assert_eq!(result, &expected);
+    }
+
+    #[test]
+    fn test_date_trunc_fine_granularity_ignores_null_slot_values() {
+        // Physical values behind null slots are arbitrary. An unsafe value in
+        // a null slot may trigger the checked fallback, but must not cause an
+        // error or alter the null bitmap.
+        let input = TimestampNanosecondArray::new(
+            vec![i64::MIN, 1].into(),
+            Some(NullBuffer::from(vec![false, true])),
+        );
+        let result = general_date_trunc_array_fine_granularity(
+            TimeUnit::Nanosecond,
+            &input,
+            DatePart::Microsecond,
+            None,
+        )
+        .unwrap();
+        let result = as_primitive_array::<TimestampNanosecondType>(&result);
+        let expected = TimestampNanosecondArray::from(vec![None, Some(0)]);
+        assert_eq!(result, &expected);
+    }
+
     #[test]
     fn test_date_trunc_fine_granularity_timezones() {
         let cases = [


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to