From 40335934b38226acf775ce4db52f313faceb25da Mon Sep 17 00:00:00 2001 From: Sean Doherty Date: Sun, 17 May 2026 05:34:00 -0500 Subject: [PATCH] Guard date_trunc lower-bound truncation --- .../functions/src/datetime/date_trunc.rs | 37 ++++++++++++++++++- .../test_files/date_trunc_boundaries.slt | 23 ++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 datafusion/sqllogictest/test_files/date_trunc_boundaries.slt diff --git a/datafusion/functions/src/datetime/date_trunc.rs b/datafusion/functions/src/datetime/date_trunc.rs index 784f593c2529..7aea98a164d0 100644 --- a/datafusion/functions/src/datetime/date_trunc.rs +++ b/datafusion/functions/src/datetime/date_trunc.rs @@ -133,6 +133,21 @@ impl DateTruncGranularity { | Self::Microsecond ) } + + fn as_str(self) -> &'static str { + 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", + } + } } #[user_doc( @@ -629,6 +644,7 @@ fn date_trunc_coarse( value: i64, tz: Option, ) -> Result { + let input = value; let value = match tz { Some(tz) => { // Use chrono DateTime to clear the various fields because need to clear per timezone, @@ -645,8 +661,12 @@ 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.as_str() + ) + }) } /// Fast path for fine granularities (hour and smaller) that can be handled @@ -879,6 +899,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/date_trunc_boundaries.slt b/datafusion/sqllogictest/test_files/date_trunc_boundaries.slt new file mode 100644 index 000000000000..1df10273d6f9 --- /dev/null +++ b/datafusion/sqllogictest/test_files/date_trunc_boundaries.slt @@ -0,0 +1,23 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# 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)') +);