This is an automated email from the ASF dual-hosted git repository.
dongjoon-hyun 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 a693daa45c33 [SPARK-58236][SQL] Fix hash codegen dropping the
CalendarInterval days field
a693daa45c33 is described below
commit a693daa45c33143d36cf5ef44a5661c3cf728de7
Author: YangJie <[email protected]>
AuthorDate: Wed Jul 22 12:45:54 2026 -0700
[SPARK-58236][SQL] Fix hash codegen dropping the CalendarInterval days field
### What changes were proposed in this pull request?
The codegen path of `genHashCalendarInterval` (in the `HashExpression` base
class, used by `Murmur3Hash`, `XxHash64` and their collation-aware variants)
hashes only the `months` and `microseconds` fields of a `CalendarInterval`,
while the interpreted path in `InterpretedHashFunction.hash` folds all three
fields (`months`, `days`, `microseconds`).
`CalendarInterval` gained its third field, `days`, in SPARK-29486. That
change updated the interpreted path and
`HiveHashFunction.hashCalendarInterval`, but missed this codegen helper, which
has been unchanged since SPARK-18287. This PR adds the missing `days` hashing
round to the codegen path so it matches the interpreted path, updates the class
doc to describe the three-field folding order, and adds a SQL migration guide
note since the hash output changes.
### Why are the changes needed?
The codegen and interpreted paths produce different hashes for the same
`CalendarInterval` value. Because `Murmur3_x86_32.hashInt(0, seed) != seed`,
the extra hashing round for `days` shifts the seed chain, so the two paths
disagree for every interval value, not only those with a non-zero `days`.
`CalendarIntervalType` is a valid input to `hash()` / `xxhash64()`, and
since SPARK-46536 it is also a supported GROUP BY key whose shuffle uses
`Murmur3Hash` for partitioning. A value hashed via codegen in one place and via
the interpreted path in another (for example a foldable literal versus a column
reference, or when whole-stage codegen falls back to interpretation) can
produce different hashes, which is unsafe for hash-based grouping and joins.
### Does this PR introduce _any_ user-facing change?
Yes. The `hash()` / `xxhash64()` value of a `CalendarInterval` computed
through the codegen path changes: it now folds the `days` field, matching the
interpreted path.
On current releases the two paths disagree, which is directly observable.
For example `hash(INTERVAL '5' DAYS)` (a foldable literal, evaluated by the
interpreted path) and `hash(interval_col)` over the same value (evaluated by
codegen) return different numbers. Likewise `SELECT count(distinct
hash(make_interval(0, 0, 0, id, 0, 0, 0))) FROM range(100)` returns 1 instead
of 100, because the codegen hash ignores `days`. After this fix both paths
agree. Only `CalendarIntervalType` is affe [...]
### How was this patch tested?
Added a regression test in `HashExpressionsSuite` (`SPARK-58236`). It
asserts that `Murmur3Hash` / `XxHash64` change their hash when any single field
(`months`, `days`, `microseconds`) changes, and uses `checkEvaluation` (which
runs both the interpreted and codegen paths and asserts equality) on intervals
whose only non-zero field is `days`. The test fails on the current codegen path
and passes with the fix.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Opus 4.8)
Closes #57398 from LuciferYang/hash-calendarinterval-days.
Authored-by: YangJie <[email protected]>
Signed-off-by: Dongjoon Hyun <[email protected]>
(cherry picked from commit a2616be0f3b303ad7189f7bd7b2c0bc49c834a27)
Signed-off-by: Dongjoon Hyun <[email protected]>
---
docs/sql-migration-guide.md | 1 +
.../spark/sql/catalyst/expressions/hash.scala | 7 ++---
.../expressions/HashExpressionsSuite.scala | 30 +++++++++++++++++++++-
3 files changed, 34 insertions(+), 4 deletions(-)
diff --git a/docs/sql-migration-guide.md b/docs/sql-migration-guide.md
index 010e903eb3bb..9cd5b664e8eb 100644
--- a/docs/sql-migration-guide.md
+++ b/docs/sql-migration-guide.md
@@ -31,6 +31,7 @@ license: |
- Since Spark 4.3, the adaptive execution rule
`org.apache.spark.sql.execution.adaptive.DynamicJoinSelection` has been renamed
to `DemoteBroadcastHashJoin`, which now only demotes broadcast hash joins
(emitting `NO_BROADCAST_HASH`). Its selection of shuffled hash join over sort
merge join has moved to a new physical rule gated by
`spark.sql.adaptive.convertSortMergeJoinToShuffledHashJoin.enabled` (default
`true`). If you previously disabled the shuffled-hash-join preference by
listing `o [...]
- Since Spark 4.3, the exact `percentile`, `percentile_cont`, and `median`
aggregate functions (including their `WITHIN GROUP (ORDER BY ...)` forms)
compute the linear interpolation between two neighboring values as `lower +
fraction * (higher - lower)` instead of `(1 - fraction) * lower + fraction *
higher`. The two are equal in exact arithmetic, but the new form is
monotonically non-decreasing in the requested percentage and avoids a rounding
error the old form could introduce. As a re [...]
- Since Spark 4.3, the new `COMMENT ON COLUMN ... IS NULL` syntax removes a
column comment by passing a `null` comment to
`TableChange.updateColumnComment(String[], String)`, so a `UpdateColumnComment`
table change may now carry a `null` `newComment()`. Previously `newComment()`
was always non-null. DataSource V2 catalogs that handle `UpdateColumnComment`
should null-check `newComment()` and treat `null` as "remove the column
comment" (mirroring how `UpdateColumnDefaultValue` already car [...]
+- Since Spark 4.3, `hash()` and `xxhash64()` include the `days` field of
`CalendarInterval` when computing the hash, so their output for interval values
differs from earlier releases. Previously the codegen path dropped `days`,
disagreeing with interpreted evaluation.
## Upgrading from Spark SQL 4.1 to 4.2
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/hash.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/hash.scala
index 2a9a140ef2a9..085586041b07 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/hash.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/hash.scala
@@ -264,8 +264,8 @@ case class Crc32(child: Expression)
* and hash it.
* - decimal: if it's a small decimal, i.e. precision <= 18,
turn it into long
* and hash it. Else, turn it into bytes and hash
it.
- * - calendar interval: hash `microseconds` first, and use the result
as seed
- * to hash `months`.
+ * - calendar interval: hash `microseconds` first, use the result as
seed to hash `days`,
+ * then use that result as seed to hash `months`.
* - interval day to second: it store long value of `microseconds`, use
murmur3 to hash the long
* input with seed.
* - interval year to month: it store int value of `months`, use murmur3 to
hash the int
@@ -441,7 +441,8 @@ abstract class HashExpression[E] extends Expression {
protected def genHashCalendarInterval(input: String, result: String): String
= {
val microsecondsHash = s"$hasherClassName.hashLong($input.microseconds,
$result)"
- s"$result = $hasherClassName.hashInt($input.months, $microsecondsHash);"
+ val daysHash = s"$hasherClassName.hashInt($input.days, $microsecondsHash)"
+ s"$result = $hasherClassName.hashInt($input.months, $daysHash);"
}
protected def genHashTimestampNanos(input: String, result: String): String =
{
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/HashExpressionsSuite.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/HashExpressionsSuite.scala
index b26c48d65796..02846b560ad0 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/HashExpressionsSuite.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/HashExpressionsSuite.scala
@@ -34,7 +34,7 @@ import org.apache.spark.sql.catalyst.util.{ArrayBasedMapData,
CollationFactory,
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.types.{ArrayType, StructType, _}
import org.apache.spark.unsafe.hash.Murmur3_x86_32
-import org.apache.spark.unsafe.types.{TimestampNanosVal, UTF8String}
+import org.apache.spark.unsafe.types.{CalendarInterval, TimestampNanosVal,
UTF8String}
import org.apache.spark.util.ArrayImplicits._
class HashExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper {
@@ -942,6 +942,34 @@ class HashExpressionsSuite extends SparkFunSuite with
ExpressionEvalHelper {
}
}
+ test("SPARK-58236: CalendarInterval hash folds all fields and agrees across
codegen") {
+ def lit(months: Int, days: Int, micros: Long): Literal =
+ Literal.create(new CalendarInterval(months, days, micros),
CalendarIntervalType)
+
+ // Murmur3Hash and XxHash64 treat months, days and microseconds as
independent fields, so
+ // changing any one of them changes the hash. HiveHash mirrors Hive's
day-time interval
+ // instead, dropping months and folding days into microseconds, so it
joins only the
+ // interpreted-vs-codegen check below.
+ val base = lit(1, 5, 7L)
+ Seq[Expression => Any](
+ e => Murmur3Hash(Seq(e), 42).eval(),
+ e => XxHash64(Seq(e), 42L).eval()).foreach { hash =>
+ assert(hash(base) === hash(lit(1, 5, 7L)))
+ assert(hash(base) !== hash(lit(2, 5, 7L))) // months
+ assert(hash(base) !== hash(lit(1, 6, 7L))) // days
+ assert(hash(base) !== hash(lit(1, 5, 8L))) // microseconds
+ }
+
+ // The interpreted and codegen paths must agree, above all when days is
the only non-zero
+ // field. checkEvaluation runs both and asserts they match, catching a
codegen path that
+ // omits days.
+ Seq(lit(0, 5, 0L), lit(0, -5, 0L), lit(3, 5, 0L), lit(1, 5, 7L)).foreach {
interval =>
+ checkEvaluation(Murmur3Hash(Seq(interval), 42),
Murmur3Hash(Seq(interval), 42).eval())
+ checkEvaluation(XxHash64(Seq(interval), 42L), XxHash64(Seq(interval),
42L).eval())
+ checkEvaluation(HiveHash(Seq(interval)), HiveHash(Seq(interval)).eval())
+ }
+ }
+
test("nanosecond timestamp hash matches expected golden values") {
// The expected values are composed independently of the expression under
test -- directly
// from the primitive hashers (and the separate hashTimestamp for Hive)
with an explicit
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]