This is an automated email from the ASF dual-hosted git repository.
gengliangwang pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.x by this push:
new 8ce1a124ba1e [SPARK-56915][SQL] Simplify MakeDate/MakeInterval codegen
under ANSI mode
8ce1a124ba1e is described below
commit 8ce1a124ba1ebb160ecab5e4327245454cb0825e
Author: Gengliang Wang <[email protected]>
AuthorDate: Tue May 19 15:55:17 2026 -0700
[SPARK-56915][SQL] Simplify MakeDate/MakeInterval codegen under ANSI mode
### What changes were proposed in this pull request?
Introduce `DateTimeExpressionUtils.java` with two static helpers:
* `makeDateExact(int year, int month, int day)`: wraps `LocalDate.of(...)`
in an `ansiDateTimeArgumentOutOfRange(DateTimeException)` try/catch, then calls
`DateTimeUtils.localDateToDays(...)`. The `ArithmeticException` from
`localDateToDays`'s internal `toIntExact` is intentionally not caught — it
propagates as-is (matching the pre-PR behavior for that overflow case).
* `makeIntervalExact(int years, int months, int weeks, int days, int hours,
int mins, Decimal secs)`: wraps `IntervalUtils.makeInterval` in the
`arithmeticOverflowError(message, "", null)` try/catch.
`datetimeExpressions.MakeDate` and `intervalExpressions.MakeInterval`
delegate to the helpers in their `failOnError = true` codegen + eval paths. The
non-ANSI branch keeps the inline `try/catch -> null` form because it sets
`isNull = true` on failure.
### Why are the changes needed?
Part of SPARK-56908 (umbrella). The try/catch wrapper that maps
`DateTimeException` (for `MakeDate`) / `ArithmeticException` (for
`MakeInterval`) to the user-facing ANSI error was emitted inline in
`doGenCode`; the helper collapses it to a single call per call site without
losing the wrapped exception's message (no `QueryContext` argument is involved,
so this PR introduces no per-row `references[]` regression).
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
```
build/sbt "catalyst/testOnly *DateExpressionsSuite
*IntervalExpressionsSuite"
```
96/96 pass.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Cursor 1.x
Closes #55940 from gengliangwang/SPARK-56915-make-date-interval.
Authored-by: Gengliang Wang <[email protected]>
Signed-off-by: Gengliang Wang <[email protected]>
(cherry picked from commit dde0863fe761d4c5863c16f5f5ac67ca8628ffd5)
Signed-off-by: Gengliang Wang <[email protected]>
---
.../expressions/DateTimeExpressionUtils.java | 71 ++++++++++++++++++++++
.../catalyst/expressions/datetimeExpressions.scala | 42 +++++++------
.../catalyst/expressions/intervalExpressions.scala | 67 ++++++++++++--------
3 files changed, 136 insertions(+), 44 deletions(-)
diff --git
a/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/DateTimeExpressionUtils.java
b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/DateTimeExpressionUtils.java
new file mode 100644
index 000000000000..0413278d0cb8
--- /dev/null
+++
b/sql/catalyst/src/main/java/org/apache/spark/sql/catalyst/expressions/DateTimeExpressionUtils.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+package org.apache.spark.sql.catalyst.expressions;
+
+import java.time.DateTimeException;
+import java.time.LocalDate;
+
+import org.apache.spark.sql.catalyst.util.DateTimeUtils;
+import org.apache.spark.sql.catalyst.util.IntervalUtils;
+import org.apache.spark.sql.errors.QueryExecutionErrors;
+import org.apache.spark.sql.types.Decimal;
+import org.apache.spark.unsafe.types.CalendarInterval;
+
+/**
+ * Static helpers shared by date/time/interval expression {@code doGenCode}
+ * paths (and their corresponding eval paths). The methods here wrap the
+ * {@code DateTimeUtils} / {@code IntervalUtils} routines whose checked
+ * exceptions need to be translated into the user-facing ANSI errors.
+ */
+public final class DateTimeExpressionUtils {
+
+ private DateTimeExpressionUtils() {}
+
+ /**
+ * Builds a day count for {@code MakeDate(year, month, day)} in ANSI mode.
+ * Only the {@link DateTimeException} thrown by
+ * {@link LocalDate#of(int, int, int)} for invalid year/month/day is caught
+ * and converted to {@code ansiDateTimeArgumentOutOfRange}. Any other
+ * exception thrown by {@code DateTimeUtils.localDateToDays} (e.g. a
+ * day-count overflow from its internal {@code toIntExact}) propagates to
+ * the caller unchanged.
+ */
+ public static int makeDateExact(int year, int month, int day) {
+ try {
+ return DateTimeUtils.localDateToDays(LocalDate.of(year, month, day));
+ } catch (DateTimeException e) {
+ throw QueryExecutionErrors.ansiDateTimeArgumentOutOfRange(e);
+ }
+ }
+
+ /**
+ * Builds a {@link CalendarInterval} for
+ * {@code MakeInterval(years, months, weeks, days, hours, mins, secs)} in
+ * ANSI mode. Throws a {@code SparkArithmeticException} if any of the
+ * intermediate {@code Math.*Exact} calls overflows.
+ */
+ public static CalendarInterval makeIntervalExact(
+ int years, int months, int weeks, int days,
+ int hours, int mins, Decimal secs) {
+ try {
+ return IntervalUtils.makeInterval(years, months, weeks, days, hours,
mins, secs);
+ } catch (ArithmeticException e) {
+ throw QueryExecutionErrors.arithmeticOverflowError(e.getMessage(), "",
null);
+ }
+ }
+}
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/datetimeExpressions.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/datetimeExpressions.scala
index aa4ed692d574..a724f02cd107 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/datetimeExpressions.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/datetimeExpressions.scala
@@ -2572,30 +2572,36 @@ case class MakeDate(
override def nullable: Boolean = if (failOnError)
children.exists(_.nullable) else true
override def nullSafeEval(year: Any, month: Any, day: Any): Any = {
- try {
- val ld = LocalDate.of(year.asInstanceOf[Int], month.asInstanceOf[Int],
day.asInstanceOf[Int])
- localDateToDays(ld)
- } catch {
- case e: java.time.DateTimeException =>
- if (failOnError) throw
QueryExecutionErrors.ansiDateTimeArgumentOutOfRange(e) else null
+ if (failOnError) {
+ DateTimeExpressionUtils.makeDateExact(
+ year.asInstanceOf[Int], month.asInstanceOf[Int], day.asInstanceOf[Int])
+ } else {
+ try {
+ val ld = LocalDate.of(
+ year.asInstanceOf[Int], month.asInstanceOf[Int],
day.asInstanceOf[Int])
+ localDateToDays(ld)
+ } catch {
+ case _: java.time.DateTimeException => null
+ }
}
}
override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {
- val dtu = DateTimeUtils.getClass.getName.stripSuffix("$")
- val failOnErrorBranch = if (failOnError) {
- "throw QueryExecutionErrors.ansiDateTimeArgumentOutOfRange(e);"
+ if (failOnError) {
+ val utils = classOf[DateTimeExpressionUtils].getName
+ defineCodeGen(ctx, ev, (year, month, day) =>
+ s"$utils.makeDateExact($year, $month, $day)")
} else {
- s"${ev.isNull} = true;"
+ val dtu = DateTimeUtils.getClass.getName.stripSuffix("$")
+ nullSafeCodeGen(ctx, ev, (year, month, day) => {
+ s"""
+ try {
+ ${ev.value} = $dtu.localDateToDays(java.time.LocalDate.of($year,
$month, $day));
+ } catch (java.time.DateTimeException e) {
+ ${ev.isNull} = true;
+ }"""
+ })
}
- nullSafeCodeGen(ctx, ev, (year, month, day) => {
- s"""
- try {
- ${ev.value} = $dtu.localDateToDays(java.time.LocalDate.of($year,
$month, $day));
- } catch (java.time.DateTimeException e) {
- $failOnErrorBranch
- }"""
- })
}
override def prettyName: String = "make_date"
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/intervalExpressions.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/intervalExpressions.scala
index 653ee9f836ed..3e4d6772c4fc 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/intervalExpressions.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/intervalExpressions.scala
@@ -399,42 +399,57 @@ case class MakeInterval(
hour: Any,
min: Any,
sec: Option[Any]): Any = {
- try {
- IntervalUtils.makeInterval(
+ val secs = sec.map(_.asInstanceOf[Decimal]).getOrElse(Decimal(0,
Decimal.MAX_LONG_DIGITS, 6))
+ if (failOnError) {
+ DateTimeExpressionUtils.makeIntervalExact(
year.asInstanceOf[Int],
month.asInstanceOf[Int],
week.asInstanceOf[Int],
day.asInstanceOf[Int],
hour.asInstanceOf[Int],
min.asInstanceOf[Int],
- sec.map(_.asInstanceOf[Decimal]).getOrElse(Decimal(0,
Decimal.MAX_LONG_DIGITS, 6)))
- } catch {
- case e: ArithmeticException =>
- if (failOnError) {
- throw QueryExecutionErrors.arithmeticOverflowError(e.getMessage)
- } else {
- null
- }
+ secs)
+ } else {
+ try {
+ IntervalUtils.makeInterval(
+ year.asInstanceOf[Int],
+ month.asInstanceOf[Int],
+ week.asInstanceOf[Int],
+ day.asInstanceOf[Int],
+ hour.asInstanceOf[Int],
+ min.asInstanceOf[Int],
+ secs)
+ } catch {
+ case _: ArithmeticException => null
+ }
}
}
override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = {
- nullSafeCodeGen(ctx, ev, (year, month, week, day, hour, min, sec) => {
- val iu = IntervalUtils.getClass.getName.stripSuffix("$")
- val secFrac = sec.getOrElse("0")
- val failOnErrorBranch = if (failOnError) {
- """throw QueryExecutionErrors.arithmeticOverflowError(e.getMessage(),
"", null);"""
- } else {
- s"${ev.isNull} = true;"
- }
- s"""
- try {
- ${ev.value} = $iu.makeInterval($year, $month, $week, $day, $hour,
$min, $secFrac);
- } catch (java.lang.ArithmeticException e) {
- $failOnErrorBranch
- }
- """
- })
+ if (failOnError) {
+ val utils = classOf[DateTimeExpressionUtils].getName
+ nullSafeCodeGen(ctx, ev, (year, month, week, day, hour, min, sec) => {
+ // `MakeInterval` always passes 7 children (auxiliary constructors fill
+ // missing slots with `Literal(0)` / `Literal(Decimal(0, ...))`), so
the
+ // 7th codegen value is always present.
+ val secFrac = sec.get
+ s"${ev.value} = $utils.makeIntervalExact(" +
+ s"$year, $month, $week, $day, $hour, $min, $secFrac);"
+ })
+ } else {
+ nullSafeCodeGen(ctx, ev, (year, month, week, day, hour, min, sec) => {
+ val iu = IntervalUtils.getClass.getName.stripSuffix("$")
+ // `MakeInterval` always passes 7 children (see note above).
+ val secFrac = sec.get
+ s"""
+ try {
+ ${ev.value} = $iu.makeInterval($year, $month, $week, $day, $hour,
$min, $secFrac);
+ } catch (java.lang.ArithmeticException e) {
+ ${ev.isNull} = true;
+ }
+ """
+ })
+ }
}
override def prettyName: String = "make_interval"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]