This is an automated email from the ASF dual-hosted git repository.
dongjoon-hyun pushed a commit to branch branch-4.3
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.3 by this push:
new b46334bbcd5a [SPARK-59054][CORE][SQL] Fix `KeyGroupedPartitioner` to
compare partition keys by value in storage-partitioned join shuffles
b46334bbcd5a is described below
commit b46334bbcd5ad4f49d3d27efbf10f3a5b4c86637
Author: Dongjoon Hyun <[email protected]>
AuthorDate: Thu Aug 27 18:49:09 2026 -0700
[SPARK-59054][CORE][SQL] Fix `KeyGroupedPartitioner` to compare partition
keys by value in storage-partitioned join shuffles
### What changes were proposed in this pull request?
This PR fixes a correctness bug in the `KeyedPartitioning` shuffle path
(storage-partitioned join with
`spark.sql.sources.v2.bucketing.shuffle.enabled=true`) by making the
partition-key lookup in `KeyGroupedPartitioner` use the exact same equivalence
that grouped the partition keys.
- In the `val part` match inside
`ShuffleExchangeExec.prepareShuffleDependency`, the driver-side `valueMap` now
uses the partitioning's own `InternalRowComparableWrapper`s as keys
(`k.partitionKeys.zipWithIndex.toMap`), instead of `Seq[Any]` from
`InternalRow.toSeq`.
- In the local `getPartitionKeyExtractor` of that same method, the
executor-side lookup key is built by evaluating the bound partition expressions
into a reused `GenericInternalRow` and wrapping it with
`InternalRowComparableWrapper.getInternalRowComparableWrapperFactory`, instead
of `Seq[Any]` from per-row `eval`.
- `InternalRowComparableWrapper` is now `Serializable`: `structType` and
`ordering` cannot cross the wire (the ordering may be generated code), so they
are `transient` and re-derived from the shared caches on first use after
deserialization. This is what allows the wrappers to be shipped to executors
inside the partitioner.
- `KeyGroupedPartitioner` (core) takes `Map[Any, Int]` and looks keys up
with `getOrElse`, removing the `ArraySeq` normalization and the
`getOrElseUpdate` map mutation. Since lookup keys are no longer retained by the
map, the executor side can reuse a single row per task without per-record
copies.
Because both the map keys and the lookup keys are wrappers over the same
data types, lookups share the `RowOrdering` equivalence (binary keys by
content, `-0.0 == 0.0`, NaNs equal, collation-aware strings) that grouped and
de-duplicated `partitionKeys` on the driver -- by construction, not by
approximation.
### Why are the changes needed?
The `valueMap` keys and the executor-side lookup keys were `Seq[Any]`
compared with Scala `==` element equality, while partition-key
grouping/de-duplication uses `InternalRowComparableWrapper` (`RowOrdering`)
semantics. The two disagree for `BinaryType`: `Array[Byte]` elements are
compared by reference, so every lookup misses and falls back to
`nonNegativeMod(hashCode, numPartitions)` -- effectively a random partition per
row, since `Array` hash codes are identity-based.
As a result, when the non-keyed side of a storage-partitioned join is
shuffled into a table partitioned by e.g. `identity(binary_col)`, rows land in
partitions that do not match the keyed side, and the join silently drops
matches (wrong results, no error).
An earlier revision of this PR used `UnsafeRow`s produced by identical
`UnsafeProjection`s as the shared key representation. As pointed out in review,
byte equality is strictly finer than the `RowOrdering` equality that grouped
`partitionKeys`: a transform whose `resultType()` is floating point can produce
`-0.0` and `0.0` partition keys, which the driver collapses into one partition
but byte comparison splits, reintroducing the same lost-match failure (and
regressing a case the `Seq[ [...]
Note that this bug also exists in the released 4.0, 4.1 and 4.2 lines,
which carry the same `KeyGroupedPartitioner` lookup.
### Does this PR introduce _any_ user-facing change?
Yes, it is a bug fix. Previously, a storage-partitioned join with a
`BinaryType` partition key and
`spark.sql.sources.v2.bucketing.shuffle.enabled=true` could silently return
fewer rows than expected. Now it returns correct results.
### How was this patch tested?
Two new regression tests in `KeyGroupedPartitioningSuite`, each exercising
both `V2_BUCKETING_SHUFFLE_ENABLED` states (1 shuffle when on, 2 shuffles when
off, same results either way):
- `SPARK-59054: shuffle one side: partition keys with binary type`: joins a
v2 table partitioned by `identity` on a binary column with an unpartitioned
table. Confirmed to fail before the fix (missing join rows) and pass after it.
- `SPARK-59054: shuffle one side: partition transform collapsing -0.0 and
0.0`: uses a new test connector function `signed_zeros` whose `resultType()`
(`DoubleType`) differs from its `inputTypes()` (`LongType`), mapping ids 1 and
2 to `-0.0` and `0.0`. Confirmed to fail against the earlier `UnsafeRow`-based
revision (one match lost) and pass with the wrapper-based fix.
`InMemoryBaseTable` gained the matching transform whitelist entry and `getKey`
case.
The full `KeyGroupedPartitioningSuite` (106 tests) passes.
A NaN-key reproduction through ordinary float/double join keys is not
reachable, because `NormalizeFloatingNumbers` wraps such join keys in
`KnownFloatingPointNormalized(NormalizeNaNAndZero(...))`, which prevents SPJ
from triggering; the wrapper-based comparison handles NaN keys correctly
regardless.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Fable 5
Closes #58345 from dongjoon-hyun/SPARK-59054.
Authored-by: Dongjoon Hyun <[email protected]>
Signed-off-by: Dongjoon Hyun <[email protected]>
(cherry picked from commit 74b5db8651d5cc13b6e75a96044c0186f16570f7)
Signed-off-by: Dongjoon Hyun <[email protected]>
---
.../main/scala/org/apache/spark/Partitioner.scala | 13 +--
.../util/InternalRowComparableWrapper.scala | 20 +++-
.../sql/connector/catalog/InMemoryBaseTable.scala | 10 ++
.../execution/exchange/ShuffleExchangeExec.scala | 41 +++++--
.../connector/KeyGroupedPartitioningSuite.scala | 128 +++++++++++++++++++++
.../catalog/functions/transformFunctions.scala | 26 +++++
6 files changed, 220 insertions(+), 18 deletions(-)
diff --git a/core/src/main/scala/org/apache/spark/Partitioner.scala
b/core/src/main/scala/org/apache/spark/Partitioner.scala
index 7dd6015b2f87..388db12956a2 100644
--- a/core/src/main/scala/org/apache/spark/Partitioner.scala
+++ b/core/src/main/scala/org/apache/spark/Partitioner.scala
@@ -19,7 +19,6 @@ package org.apache.spark
import java.io.{IOException, ObjectInputStream, ObjectOutputStream}
-import scala.collection.immutable.ArraySeq
import scala.collection.mutable
import scala.collection.mutable.ArrayBuffer
import scala.math.log10
@@ -144,16 +143,16 @@ private[spark] class PartitionIdPassthrough(override val
numPartitions: Int) ext
* The `valueMap` is a map that contains tuples of (partition value, partition
id). It is generated
* by [[org.apache.spark.sql.catalyst.plans.physical.KeyedPartitioning]], used
to partition
* the other side of a join to make sure records with same partition value are
in the same
- * partition.
+ * partition. Keys are looked up with `equals`/`hashCode`, so the caller must
supply the map keys
+ * and the per-record lookup keys in a single representation that compares
partition values
+ * consistently (the caller uses the partitioning's own
`InternalRowComparableWrapper`s). Keys
+ * absent from the map fall back to a partition derived from the key's hash
code.
*/
private[spark] class KeyGroupedPartitioner(
- valueMap: mutable.Map[Seq[Any], Int],
+ valueMap: Map[Any, Int],
override val numPartitions: Int) extends Partitioner {
override def getPartition(key: Any): Int = {
- val keys = key.asInstanceOf[Seq[Any]]
- val normalizedKeys = ArraySeq.from(keys)
- valueMap.getOrElseUpdate(normalizedKeys,
- Utils.nonNegativeMod(normalizedKeys.hashCode, numPartitions))
+ valueMap.getOrElse(key, Utils.nonNegativeMod(key.hashCode, numPartitions))
}
}
diff --git
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/InternalRowComparableWrapper.scala
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/InternalRowComparableWrapper.scala
index 217d12710a6a..3566319c39ad 100644
---
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/InternalRowComparableWrapper.scala
+++
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/InternalRowComparableWrapper.scala
@@ -34,8 +34,8 @@ import org.apache.spark.util.NonFateSharingCache
class InternalRowComparableWrapper private (
val row: InternalRow,
val dataTypes: Seq[DataType],
- val structType: StructType,
- val ordering: BaseOrdering) {
+ @transient private var _structType: StructType,
+ @transient private var _ordering: BaseOrdering) extends Serializable {
/**
* Previous constructor for binary compatibility. Prefer using
@@ -49,6 +49,22 @@ class InternalRowComparableWrapper private (
InternalRowComparableWrapper.structTypeCache.get(dataTypes),
InternalRowComparableWrapper.orderingCache.get(dataTypes))
+ // `structType` and `ordering` cannot cross the wire (the ordering may be
generated code), so
+ // they are transient and re-derived from the shared caches on first use
after deserialization.
+ def structType: StructType = {
+ if (_structType == null) {
+ _structType = InternalRowComparableWrapper.structTypeCache.get(dataTypes)
+ }
+ _structType
+ }
+
+ def ordering: BaseOrdering = {
+ if (_ordering == null) {
+ _ordering = InternalRowComparableWrapper.orderingCache.get(dataTypes)
+ }
+ _ordering
+ }
+
override def hashCode(): Int = Murmur3HashFunction.hash(
row,
structType,
diff --git
a/sql/catalyst/src/test/scala/org/apache/spark/sql/connector/catalog/InMemoryBaseTable.scala
b/sql/catalyst/src/test/scala/org/apache/spark/sql/connector/catalog/InMemoryBaseTable.scala
index 1b5d7bb75fe6..0b666ab473fc 100644
---
a/sql/catalyst/src/test/scala/org/apache/spark/sql/connector/catalog/InMemoryBaseTable.scala
+++
b/sql/catalyst/src/test/scala/org/apache/spark/sql/connector/catalog/InMemoryBaseTable.scala
@@ -208,6 +208,7 @@ abstract class InMemoryBaseTable(
case _: SortedBucketTransform =>
case _: ClusterByTransform =>
case NamedTransform("truncate", Seq(_: NamedReference, _: V2Literal[_])) =>
+ case NamedTransform("signed_zeros", Seq(_: NamedReference)) =>
case t if !allowUnsupportedTransforms =>
throw new IllegalArgumentException(s"Transform $t is not a supported
transform")
}
@@ -323,6 +324,15 @@ abstract class InMemoryBaseTable(
case (v, t) =>
throw new IllegalArgumentException(s"Match: unsupported
argument(s) type - ($v, $t)")
}
+ // the result should be consistent with SignedZerosFunction defined at
+ // transformFunctions.scala
+ case NamedTransform("signed_zeros", Seq(ref: NamedReference)) =>
+ extractor(ref.fieldNames, cleanedSchema, row) match {
+ case (value: Long, LongType) =>
+ if (value == 1L) -0.0d else if (value == 2L) 0.0d else
value.toDouble
+ case (v, t) =>
+ throw new IllegalArgumentException(s"Match: unsupported
argument(s) type - ($v, $t)")
+ }
case ClusterByTransform(columnNames) =>
columnNames.map { colName =>
extractor(colName.fieldNames, cleanedSchema, row)._1
diff --git
a/sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/ShuffleExchangeExec.scala
b/sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/ShuffleExchangeExec.scala
index 0f3bb973000c..4054f25c6e01 100644
---
a/sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/ShuffleExchangeExec.scala
+++
b/sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/ShuffleExchangeExec.scala
@@ -20,7 +20,6 @@ package org.apache.spark.sql.execution.exchange
import java.util.concurrent.atomic.AtomicReference
import java.util.function.Supplier
-import scala.collection.mutable
import scala.concurrent.{ExecutionContext, Future, Promise}
import org.apache.spark._
@@ -31,13 +30,14 @@ import
org.apache.spark.shuffle.{ShuffleWriteMetricsReporter, ShuffleWriteProces
import org.apache.spark.shuffle.sort.SortShuffleManager
import org.apache.spark.sql.catalyst.InternalRow
import org.apache.spark.sql.catalyst.expressions.{
- Attribute, BoundReference, CollationAwareMurmur3Hash, Literal, Pmod,
UnsafeProjection,
- UnsafeRow, UnsafeRowChecksum}
+ Attribute, BoundReference, CollationAwareMurmur3Hash, GenericInternalRow,
Literal, Pmod,
+ UnsafeProjection, UnsafeRow, UnsafeRowChecksum}
import org.apache.spark.sql.catalyst.expressions.BindReferences.bindReferences
import
org.apache.spark.sql.catalyst.expressions.codegen.LazilyGeneratedOrdering
import org.apache.spark.sql.catalyst.plans.logical.Statistics
import org.apache.spark.sql.catalyst.plans.physical._
import org.apache.spark.sql.catalyst.types.DataTypeUtils
+import org.apache.spark.sql.catalyst.util.InternalRowComparableWrapper
import org.apache.spark.sql.execution._
import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics,
SQLShuffleReadMetricsReporter, SQLShuffleWriteMetricsReporter}
import org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}
@@ -407,10 +407,19 @@ object ShuffleExchangeExec {
assert(k.isGrouped,
s"Expected a grouped KeyedPartitioning on ${k.expressions}, but got
${k.numPartitions} " +
"partition keys with duplicates among them")
- val valueMap = k.partitionKeys.zipWithIndex.map {
- case (key, index) => (key.row.toSeq(k.expressionDataTypes), index)
- }.toMap
- new KeyGroupedPartitioner(mutable.Map.from(valueMap), k.numPartitions)
+ // The map keys and the lookup keys produced by
`getPartitionKeyExtractor` below are
+ // wrapped through the same `InternalRowComparableWrapper` factory
over this
+ // partitioning's expression data types, so map lookups share the
exact equivalence
+ // (`RowOrdering`, e.g. binary keys by content, -0.0 == 0.0, NaNs
equal) that grouped and
+ // de-duplicated `partitionKeys`. Re-wrapping the stored keys matters:
`KeyedShuffleSpec
+ // .createPartitioning` substitutes this side's expressions but
retains the other side's
+ // `partitionKeys`, whose wrappers may carry a schema that differs in
struct field names,
+ // which wrapper equality would reject.
+ val wrapperFactory = InternalRowComparableWrapper
+ .getInternalRowComparableWrapperFactory(k.expressionDataTypes)
+ val valueMap = k.partitionKeys.map(key =>
wrapperFactory(key.row)).zipWithIndex
+ .toMap[Any, Int]
+ new KeyGroupedPartitioner(valueMap, k.numPartitions)
case _ => throw SparkException.internalError(s"Exchange not implemented
for $newPartitioning")
// TODO: Handle BroadcastPartitioning.
}
@@ -463,8 +472,22 @@ object ShuffleExchangeExec {
val projection =
UnsafeProjection.create(sortingExpressions.map(_.child), outputAttributes)
row => projection(row)
case SinglePartition => identity
- case KeyedPartitioning(expressions, _, _, _) =>
- row => bindReferences(expressions, outputAttributes).map(_.eval(row))
+ case k: KeyedPartitioning =>
+ val expressions = bindReferences(k.expressions,
outputAttributes).toArray
+ // Wrap the evaluated partition key so it compares equal to the
KeyGroupedPartitioner's
+ // map keys (the partitioning's own wrappers) under `RowOrdering`
semantics. The wrapped
+ // row is reused across records; KeyGroupedPartitioner does not retain
lookup keys.
+ val wrapperFactory = InternalRowComparableWrapper
+ .getInternalRowComparableWrapperFactory(k.expressionDataTypes)
+ val partitionKeyRow = new GenericInternalRow(expressions.length)
+ row => {
+ var i = 0
+ while (i < expressions.length) {
+ partitionKeyRow.update(i, expressions(i).eval(row))
+ i += 1
+ }
+ wrapperFactory(partitionKeyRow)
+ }
case s: ShufflePartitionIdPassThrough =>
// For ShufflePartitionIdPassThrough, the expression directly
evaluates to the partition ID
// If the value is null, `InternalRow#getInt` returns 0.
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala
b/sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala
index f0c676d2d74f..c406f6e0ad02 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala
@@ -1690,6 +1690,134 @@ class KeyGroupedPartitioningSuite extends
DistributionAndOrderingSuiteBase with
}
}
+ test("SPARK-59054: shuffle one side: partition keys with binary type") {
+ val items_partitions = Array(identity("id"))
+ createTable(items, Array(
+ Column.create("id", BinaryType),
+ Column.create("name", StringType),
+ Column.create("price", DoubleType)), items_partitions)
+
+ sql(s"INSERT INTO testcat.ns.$items VALUES " +
+ "(X'0101', 'aa', 40.0), " +
+ "(X'0202', 'bb', 10.0), " +
+ "(X'0303', 'cc', 15.5), " +
+ "(X'0404', 'dd', 20.0)")
+
+ createTable(purchases, Array(
+ Column.create("item_id", BinaryType),
+ Column.create("price", DoubleType)), Array.empty)
+ sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
+ "(X'0101', 42.0), (X'0101', 44.0), (X'0202', 11.0), (X'0202', 19.5), " +
+ "(X'0303', 26.0), (X'0303', 30.0), (X'0404', 50.0), (X'0404', 60.0)")
+
+ Seq(true, false).foreach { shuffle =>
+ withSQLConf(SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key ->
shuffle.toString) {
+ val df = createJoinTestDF(Seq("id" -> "item_id"))
+ val shuffles = collectShuffles(df.queryExecution.executedPlan)
+ if (shuffle) {
+ assert(shuffles.size == 1, "only shuffle one side not report
partitioning")
+ } else {
+ assert(shuffles.size == 2, "should add two side shuffle when
bucketing shuffle one " +
+ "side is not enabled")
+ }
+
+ checkAnswer(df, Seq(
+ Row(Array[Byte](1, 1), "aa", 40.0, 42.0),
+ Row(Array[Byte](1, 1), "aa", 40.0, 44.0),
+ Row(Array[Byte](2, 2), "bb", 10.0, 11.0),
+ Row(Array[Byte](2, 2), "bb", 10.0, 19.5),
+ Row(Array[Byte](3, 3), "cc", 15.5, 26.0),
+ Row(Array[Byte](3, 3), "cc", 15.5, 30.0),
+ Row(Array[Byte](4, 4), "dd", 20.0, 50.0),
+ Row(Array[Byte](4, 4), "dd", 20.0, 60.0)))
+ }
+ }
+ }
+
+ test("SPARK-59054: shuffle one side: struct partition keys with different
field names") {
+ // Struct equality ignores field names, so joining STRUCT<a:INT> with
STRUCT<b:INT> is legal
+ // and SPJ stays eligible. The shuffled side's lookup keys carry its own
schema while the
+ // partitioner's map keys come from the keyed side, so key comparison must
not depend on
+ // the field names.
+ val items_partitions = Array(identity("id"))
+ createTable(items, Array(
+ Column.create("id", new StructType().add("a", IntegerType)),
+ Column.create("name", StringType),
+ Column.create("price", DoubleType)), items_partitions)
+
+ sql(s"INSERT INTO testcat.ns.$items VALUES " +
+ "(named_struct('a', 1), 'aa', 40.0), " +
+ "(named_struct('a', 2), 'bb', 10.0), " +
+ "(named_struct('a', 3), 'cc', 15.5), " +
+ "(named_struct('a', 4), 'dd', 20.0)")
+
+ createTable(purchases, Array(
+ Column.create("item_id", new StructType().add("b", IntegerType)),
+ Column.create("price", DoubleType)), Array.empty)
+ sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
+ "(named_struct('b', 1), 42.0), (named_struct('b', 2), 19.5), " +
+ "(named_struct('b', 3), 26.0), (named_struct('b', 4), 50.0)")
+
+ Seq(true, false).foreach { shuffle =>
+ withSQLConf(SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key ->
shuffle.toString) {
+ val df = createJoinTestDF(Seq("id" -> "item_id"))
+ val shuffles = collectShuffles(df.queryExecution.executedPlan)
+ if (shuffle) {
+ assert(shuffles.size == 1, "only shuffle one side not report
partitioning")
+ } else {
+ assert(shuffles.size == 2, "should add two side shuffle when
bucketing shuffle one " +
+ "side is not enabled")
+ }
+
+ checkAnswer(df, Seq(
+ Row(Row(1), "aa", 40.0, 42.0),
+ Row(Row(2), "bb", 10.0, 19.5),
+ Row(Row(3), "cc", 15.5, 26.0),
+ Row(Row(4), "dd", 20.0, 50.0)))
+ }
+ }
+ }
+
+ test("SPARK-59054: shuffle one side: partition transform collapsing -0.0 and
0.0") {
+ withFunction(UnboundSignedZerosFunction) {
+ // `signed_zeros` maps id 1 to -0.0 and id 2 to 0.0: two partition keys
that are equal
+ // under SQL semantics but distinct bitwise, which the grouped side
collapses into one
+ // partition. Rows of both forms on the shuffled side must land in that
partition.
+ val items_partitions = Array(
+ Expressions.apply("signed_zeros", Expressions.column("id")))
+ createTable(items, itemsColumns, items_partitions)
+
+ sql(s"INSERT INTO testcat.ns.$items VALUES " +
+ "(1, 'aa', 40.0, cast('2020-01-01' as timestamp)), " +
+ "(2, 'bb', 10.0, cast('2020-01-01' as timestamp)), " +
+ "(3, 'cc', 15.5, cast('2020-02-01' as timestamp))")
+
+ createTable(purchases, purchasesColumns, Array.empty)
+ sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
+ "(1, 42.0, cast('2020-01-01' as timestamp)), " +
+ "(2, 19.5, cast('2020-02-01' as timestamp)), " +
+ "(3, 26.0, cast('2023-01-01' as timestamp))")
+
+ Seq(true, false).foreach { shuffle =>
+ withSQLConf(SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key ->
shuffle.toString) {
+ val df = createJoinTestDF(Seq("id" -> "item_id"))
+ val shuffles = collectShuffles(df.queryExecution.executedPlan)
+ if (shuffle) {
+ assert(shuffles.size == 1, "only shuffle one side not report
partitioning")
+ } else {
+ assert(shuffles.size == 2, "should add two side shuffle when
bucketing shuffle one " +
+ "side is not enabled")
+ }
+
+ checkAnswer(df, Seq(
+ Row(1, "aa", 40.0, 42.0),
+ Row(2, "bb", 10.0, 19.5),
+ Row(3, "cc", 15.5, 26.0)))
+ }
+ }
+ }
+ }
+
test("SPARK-44641: duplicated records when SPJ is not triggered") {
val items_partitions = Array(bucket(8, "id"))
createTable(items, itemsColumns, items_partitions)
diff --git
a/sql/core/src/test/scala/org/apache/spark/sql/connector/catalog/functions/transformFunctions.scala
b/sql/core/src/test/scala/org/apache/spark/sql/connector/catalog/functions/transformFunctions.scala
index 35102c6893d3..62adb9bed606 100644
---
a/sql/core/src/test/scala/org/apache/spark/sql/connector/catalog/functions/transformFunctions.scala
+++
b/sql/core/src/test/scala/org/apache/spark/sql/connector/catalog/functions/transformFunctions.scala
@@ -252,6 +252,32 @@ object StringSelfFunction extends
ScalarFunction[UTF8String] {
}
}
+object UnboundSignedZerosFunction extends UnboundFunction {
+ override def bind(inputType: StructType): BoundFunction = {
+ if (inputType.size == 1 && inputType.head.dataType == LongType)
SignedZerosFunction
+ else throw new UnsupportedOperationException("'signed_zeros' only takes a
long as input type")
+ }
+ override def description(): String = name()
+ override def name(): String = "signed_zeros"
+}
+
+// A transform whose result type (DoubleType) differs from its input type
(LongType), mapping
+// 1 to -0.0 and 2 to 0.0 so that two distinct inputs produce partition keys
that are equal
+// under SQL semantics but distinct bitwise. The result should be consistent
with the
+// "signed_zeros" NamedTransform defined at InMemoryBaseTable.scala.
+object SignedZerosFunction extends ScalarFunction[Double] {
+ override def inputTypes(): Array[DataType] = Array(LongType)
+ override def resultType(): DataType = DoubleType
+ override def name(): String = "signed_zeros"
+ override def canonicalName(): String = name()
+ override def toString: String = name()
+ override def produceResult(input: InternalRow): Double = input.getLong(0)
match {
+ case 1L => -0.0d
+ case 2L => 0.0d
+ case v => v.toDouble
+ }
+}
+
object UnboundTruncateFunction extends UnboundFunction {
override def bind(inputType: StructType): BoundFunction = TruncateFunction
override def description(): String = name()
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]