Skip to content

Commit e5bcfff

Browse files
authored
Match Spark 4.2 date_trunc overflow at Long.MinValue [databricks] (NVIDIA#15416)
Fixes NVIDIA#15382. ### Description - Match Spark 4.2 (`SPARK-56663`) `date_trunc` semantics at `Long.MinValue` micros: truncation underflow must raise `ArithmeticException` instead of returning a wrapped positive timestamp on GPU. - Route the check through `TruncTimestampShims` so Spark 4.2 enables the overflow check while older supported Spark lines keep the previous no-op behavior, without runtime version compares in common code. - Add `test_date_trunc_long_min_value_overflow` for `YEAR` and `MILLISECOND` on Spark 4.2+, comparing CPU/GPU exception behavior and validating the GPU plan includes `GpuProjectExec`. - Validated with: - `mvn -s /home/liangcail/.m2/settings_art.xml -f scala2.13/pom.xml -pl sql-plugin -Dbuildver=420 -Dcuda.version=cuda13 -DskipTests clean compile` - `mvn -s /home/liangcail/.m2/settings_art.xml -f scala2.13/pom.xml -pl sql-plugin -Dbuildver=413 -Dcuda.version=cuda13 -DskipTests clean compile` - `mvn -s /home/liangcail/.m2/settings_art.xml -f scala2.13/pom.xml -Dbuildver=420 -Dcuda.version=cuda13 -DskipTests validate` - `mvn -s /home/liangcail/.m2/settings_art.xml -f scala2.13/pom.xml -Dbuildver=413 -Dcuda.version=cuda13 -DskipTests validate` ### Checklists Documentation - [ ] Updated for new or modified user-facing features or behaviors - [x] No user-facing change Testing - [x] Added or modified tests to cover new code paths - [ ] Covered by existing tests (Please provide the names of the existing tests in the PR description.) - [ ] Not required Performance - [ ] Tests ran and results are added in the PR description - [ ] Issue filed with a link in the PR description - [x] Not required --------- Signed-off-by: Firestarman <firestarmanllc@gmail.com>
1 parent ced237d commit e5bcfff

4 files changed

Lines changed: 197 additions & 2 deletions

File tree

integration_tests/src/main/python/date_time_test.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from dateutil import tz
2121
from marks import allow_non_gpu, approximate_float, datagen_overrides, disable_ansi_mode, ignore_order, incompat, tz_sensitive_test
2222
from pyspark.sql.types import *
23-
from spark_session import with_cpu_session, is_before_spark_350, is_before_spark_400
23+
from spark_session import with_cpu_session, is_before_spark_350, is_before_spark_400, is_spark_420_or_later
2424
import pyspark.sql.functions as f
2525
from timezones import all_timezones, fixed_offset_timezones, fixed_offset_timezones_iana, variable_offset_timezones, variable_offset_timezones_iana
2626

@@ -1174,3 +1174,37 @@ def test_trunc_timestamp_single_format(data_gen):
11741174
'date_trunc("MILLISECOND", a)',
11751175
'date_trunc("MICROSECOND", a)',
11761176
'date_trunc("invalid", a)'))
1177+
1178+
_LONG_MIN_TIMESTAMP_MICROS = -9223372036854775808
1179+
1180+
# SPARK-56663: Spark 4.2+ throws ArithmeticException for date_trunc at Long.MinValue micros.
1181+
_date_trunc_long_min_overflow_formats = [
1182+
pytest.param('YEAR', id='YEAR'),
1183+
pytest.param('MILLISECOND', id='MILLISECOND'),
1184+
]
1185+
1186+
@allow_non_gpu(*non_utc_tz_allow)
1187+
@pytest.mark.skipif(not is_spark_420_or_later(),
1188+
reason='date_trunc Long.MinValue overflow is supported on Spark 4.2+')
1189+
@pytest.mark.parametrize('trunc_format', _date_trunc_long_min_overflow_formats)
1190+
def test_date_trunc_long_min_value_overflow(trunc_format):
1191+
def run(spark):
1192+
spark.conf.set('spark.rapids.sql.test.validateExecsInGpuPlan', 'GpuProjectExec')
1193+
return spark.sql(
1194+
"select date_trunc('{0}', timestamp_micros({1}L))".format(
1195+
trunc_format, _LONG_MIN_TIMESTAMP_MICROS)).collect()
1196+
assert_gpu_and_cpu_error(run, conf={}, error_message='ArithmeticException')
1197+
1198+
@allow_non_gpu(*non_utc_tz_allow)
1199+
@pytest.mark.skipif(not is_spark_420_or_later(),
1200+
reason='date_trunc Long.MinValue overflow is supported on Spark 4.2+')
1201+
def test_date_trunc_long_min_value_overflow_column_format():
1202+
# Exercise the scalar-timestamp / column-format overload so overflow checks compare
1203+
# equal-length columns instead of a one-row timestamp against a multi-row result.
1204+
def run(spark):
1205+
spark.conf.set('spark.rapids.sql.test.validateExecsInGpuPlan', 'GpuProjectExec')
1206+
return spark.sql(
1207+
"select date_trunc(fmt, timestamp_micros({0}L)) "
1208+
"from values ('YEAR'), ('MILLISECOND') as t(fmt)".format(
1209+
_LONG_MIN_TIMESTAMP_MICROS)).collect()
1210+
assert_gpu_and_cpu_error(run, conf={}, error_message='ArithmeticException')

sql-plugin/src/main/scala/org/apache/spark/sql/rapids/datetimeExpressions.scala

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import com.nvidia.spark.rapids.ExprMeta
2828
import com.nvidia.spark.rapids.GpuOverrides.{extractStringLit, getTimeParserPolicy}
2929
import com.nvidia.spark.rapids.RapidsPluginImplicits._
3030
import com.nvidia.spark.rapids.jni.{Arithmetic, CastStrings, DateTimeUtils, GpuTimeZoneDB}
31-
import com.nvidia.spark.rapids.shims.{NullIntolerantShim, ShimBinaryExpression, ShimExpression}
31+
import com.nvidia.spark.rapids.shims.{NullIntolerantShim, ShimBinaryExpression, ShimExpression,
32+
TruncTimestampShims}
3233

3334
import org.apache.spark.sql.catalyst.expressions.{BinaryExpression, ExpectsInputTypes, Expression, FromUnixTime, FromUTCTimestamp, ImplicitCastInputTypes, MonthsBetween, TimeZoneAwareExpression, ToUTCTimestamp, TruncDate, TruncTimestamp}
3435
import org.apache.spark.sql.catalyst.util.DateTimeConstants
@@ -1534,6 +1535,59 @@ case class GpuTruncTimestamp(fmt: Expression, timestamp: Expression, timeZoneId:
15341535

15351536
override def prettyName: String = "date_trunc"
15361537

1538+
override protected def truncate(datetimeCol: GpuColumnVector, fmtCol: GpuColumnVector)
1539+
: ColumnVector = {
1540+
closeOnExcept(DateTimeUtils.truncate(datetimeCol.getBase, fmtCol.getBase)) { truncated =>
1541+
TruncTimestampShims.checkOverflow(datetimeCol.getBase, truncated)
1542+
truncated
1543+
}
1544+
}
1545+
1546+
override protected def truncate(datetimeVal: GpuScalar, fmtCol: GpuColumnVector): ColumnVector = {
1547+
// Keep the one-row scalar broadcast into JNI truncate, and compare the multi-row result
1548+
// against the scalar via TruncTimestampShims.checkOverflow(Scalar, ColumnVector).
1549+
withResource(ColumnVector.fromScalar(datetimeVal.getBase, 1)) { datetimeCol =>
1550+
closeOnExcept(DateTimeUtils.truncate(datetimeCol, fmtCol.getBase)) { truncated =>
1551+
TruncTimestampShims.checkOverflow(datetimeVal.getBase, truncated)
1552+
truncated
1553+
}
1554+
}
1555+
}
1556+
1557+
override protected def truncate(datetimeCol: GpuColumnVector, fmtVal: GpuScalar): ColumnVector = {
1558+
fmtStr match {
1559+
case Some(fmt) =>
1560+
closeOnExcept(DateTimeUtils.truncate(datetimeCol.getBase, fmt)) { truncated =>
1561+
TruncTimestampShims.checkOverflow(datetimeCol.getBase, truncated)
1562+
truncated
1563+
}
1564+
case None =>
1565+
GpuColumnVector.columnVectorFromNull(datetimeCol.getRowCount.toInt, dataType)
1566+
}
1567+
}
1568+
1569+
override protected def truncate(numRows: Int, datetimeVal: GpuScalar, fmtVal: GpuScalar)
1570+
: ColumnVector = {
1571+
fmtStr match {
1572+
case Some(fmt) =>
1573+
withResource(ColumnVector.fromScalar(datetimeVal.getBase, 1)) { datetimeCol =>
1574+
closeOnExcept(DateTimeUtils.truncate(datetimeCol, fmt)) { truncated =>
1575+
TruncTimestampShims.checkOverflow(datetimeVal.getBase, truncated)
1576+
if (numRows == 1) {
1577+
truncated
1578+
} else {
1579+
withResource(truncated) { _ =>
1580+
withResource(truncated.getScalarElement(0)) { truncatedScalar =>
1581+
ColumnVector.fromScalar(truncatedScalar, numRows)
1582+
}
1583+
}
1584+
}
1585+
}
1586+
}
1587+
case None => GpuColumnVector.columnVectorFromNull(numRows, dataType)
1588+
}
1589+
}
1590+
15371591
// Since the input order of this class is opposite compared to the `GpuTruncDate` class,
15381592
// we need to switch `lhs` and `rhs` in the `doColumnar` methods below.
15391593

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright (c) 2026, NVIDIA CORPORATION.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
/*** spark-rapids-shim-json-lines
17+
{"spark": "330"}
18+
{"spark": "331"}
19+
{"spark": "332"}
20+
{"spark": "333"}
21+
{"spark": "334"}
22+
{"spark": "340"}
23+
{"spark": "341"}
24+
{"spark": "342"}
25+
{"spark": "343"}
26+
{"spark": "344"}
27+
{"spark": "350"}
28+
{"spark": "350db143"}
29+
{"spark": "351"}
30+
{"spark": "352"}
31+
{"spark": "353"}
32+
{"spark": "354"}
33+
{"spark": "355"}
34+
{"spark": "356"}
35+
{"spark": "357"}
36+
{"spark": "358"}
37+
{"spark": "359"}
38+
{"spark": "400"}
39+
{"spark": "400db173"}
40+
{"spark": "401"}
41+
{"spark": "402"}
42+
{"spark": "403"}
43+
{"spark": "404"}
44+
{"spark": "411"}
45+
{"spark": "412"}
46+
{"spark": "413"}
47+
spark-rapids-shim-json-lines ***/
48+
49+
package com.nvidia.spark.rapids.shims
50+
51+
import ai.rapids.cudf.{ColumnVector, Scalar}
52+
53+
object TruncTimestampShims {
54+
def checkOverflow(datetimeCol: ColumnVector, truncated: ColumnVector): Unit = {}
55+
56+
def checkOverflow(datetime: Scalar, truncated: ColumnVector): Unit = {}
57+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright (c) 2026, NVIDIA CORPORATION.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
/*** spark-rapids-shim-json-lines
17+
{"spark": "420"}
18+
spark-rapids-shim-json-lines ***/
19+
20+
package com.nvidia.spark.rapids.shims
21+
22+
import ai.rapids.cudf.{ColumnVector, Scalar}
23+
import com.nvidia.spark.rapids.Arm.withResource
24+
25+
object TruncTimestampShims {
26+
/**
27+
* SPARK-56663 uses checked subtraction and throws when truncation would underflow. The JNI
28+
* kernel uses unchecked chrono arithmetic, so detect a wrapped result by verifying that
29+
* truncation never moves a timestamp forward.
30+
*/
31+
def checkOverflow(datetimeCol: ColumnVector, truncated: ColumnVector): Unit = {
32+
withResource(truncated.greaterThan(datetimeCol)) { overflow =>
33+
checkAnyOverflow(overflow)
34+
}
35+
}
36+
37+
def checkOverflow(datetime: Scalar, truncated: ColumnVector): Unit = {
38+
withResource(truncated.greaterThan(datetime)) { overflow =>
39+
checkAnyOverflow(overflow)
40+
}
41+
}
42+
43+
private def checkAnyOverflow(overflow: ColumnVector): Unit = {
44+
withResource(overflow.any()) { any =>
45+
if (any.isValid && any.getBoolean) {
46+
throw new ArithmeticException("long overflow")
47+
}
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)