Skip to content

Commit 8c2dfe6

Browse files
dongjoon-hyunpeter-toth
authored andcommitted
[SPARK-59054][CORE][SQL] Fix KeyGroupedPartitioner to compare partition keys by value in storage-partitioned join shuffles
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. 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[Any]` code handled). Using the wrappers themselves eliminates the mismatch by construction. Note that this bug also exists in the released 4.0, 4.1 and 4.2 lines, which carry the same `KeyGroupedPartitioner` lookup. 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. 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. Generated-by: Claude Fable 5 Closes apache#58345 from dongjoon-hyun/SPARK-59054. Authored-by: Dongjoon Hyun <dongjoon@apache.org> Signed-off-by: Dongjoon Hyun <dongjoon@apache.org> (cherry picked from commit 74b5db8) Signed-off-by: Dongjoon Hyun <dongjoon@apache.org> (cherry picked from commit b46334b) (cherry picked from commit 6686830)
1 parent 3d70c2c commit 8c2dfe6

6 files changed

Lines changed: 215 additions & 20 deletions

File tree

core/src/main/scala/org/apache/spark/Partitioner.scala

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package org.apache.spark
1919

2020
import java.io.{IOException, ObjectInputStream, ObjectOutputStream}
2121

22-
import scala.collection.immutable.ArraySeq
2322
import scala.collection.mutable
2423
import scala.collection.mutable.ArrayBuffer
2524
import scala.math.log10
@@ -144,16 +143,16 @@ private[spark] class PartitionIdPassthrough(override val numPartitions: Int) ext
144143
* The `valueMap` is a map that contains tuples of (partition value, partition id). It is generated
145144
* by [[org.apache.spark.sql.catalyst.plans.physical.KeyGroupedPartitioning]], used to partition
146145
* the other side of a join to make sure records with same partition value are in the same
147-
* partition.
146+
* partition. Keys are looked up with `equals`/`hashCode`, so the caller must supply the map keys
147+
* and the per-record lookup keys in a single representation that compares partition values
148+
* consistently (the caller uses the partitioning's own `InternalRowComparableWrapper`s). Keys
149+
* absent from the map fall back to a partition derived from the key's hash code.
148150
*/
149151
private[spark] class KeyGroupedPartitioner(
150-
valueMap: mutable.Map[Seq[Any], Int],
152+
valueMap: Map[Any, Int],
151153
override val numPartitions: Int) extends Partitioner {
152154
override def getPartition(key: Any): Int = {
153-
val keys = key.asInstanceOf[Seq[Any]]
154-
val normalizedKeys = ArraySeq.from(keys)
155-
valueMap.getOrElseUpdate(normalizedKeys,
156-
Utils.nonNegativeMod(normalizedKeys.hashCode, numPartitions))
155+
valueMap.getOrElse(key, Utils.nonNegativeMod(key.hashCode, numPartitions))
157156
}
158157
}
159158

sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/util/InternalRowComparableWrapper.scala

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ package org.apache.spark.sql.catalyst.util
2020
import scala.collection.mutable
2121

2222
import org.apache.spark.sql.catalyst.InternalRow
23-
import org.apache.spark.sql.catalyst.expressions.{Expression, Murmur3HashFunction, RowOrdering}
23+
import org.apache.spark.sql.catalyst.expressions.{BaseOrdering, Expression, Murmur3HashFunction, RowOrdering}
2424
import org.apache.spark.sql.connector.read.{HasPartitionKey, InputPartition}
2525
import org.apache.spark.sql.types.{DataType, StructField, StructType}
2626
import org.apache.spark.util.NonFateSharingCache
@@ -33,11 +33,27 @@ import org.apache.spark.util.NonFateSharingCache
3333
*
3434
* @param dataTypes the data types for the row
3535
*/
36-
class InternalRowComparableWrapper(val row: InternalRow, val dataTypes: Seq[DataType]) {
37-
import InternalRowComparableWrapper._
36+
class InternalRowComparableWrapper(val row: InternalRow, val dataTypes: Seq[DataType])
37+
extends Serializable {
3838

39-
private val structType = structTypeCache.get(dataTypes)
40-
private val ordering = orderingCache.get(dataTypes)
39+
// `structType` and `ordering` cannot cross the wire (the ordering may be generated code), so
40+
// they are transient and re-derived from the shared caches on first use after deserialization.
41+
@transient private var _structType: StructType = _
42+
@transient private var _ordering: BaseOrdering = _
43+
44+
def structType: StructType = {
45+
if (_structType == null) {
46+
_structType = InternalRowComparableWrapper.structTypeCache.get(dataTypes)
47+
}
48+
_structType
49+
}
50+
51+
def ordering: BaseOrdering = {
52+
if (_ordering == null) {
53+
_ordering = InternalRowComparableWrapper.orderingCache.get(dataTypes)
54+
}
55+
_ordering
56+
}
4157

4258
override def hashCode(): Int = Murmur3HashFunction.hash(
4359
row,

sql/catalyst/src/test/scala/org/apache/spark/sql/connector/catalog/InMemoryBaseTable.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ abstract class InMemoryBaseTable(
109109
case _: SortedBucketTransform =>
110110
case _: ClusterByTransform =>
111111
case NamedTransform("truncate", Seq(_: NamedReference, _: Literal[_])) =>
112+
case NamedTransform("signed_zeros", Seq(_: NamedReference)) =>
112113
case t if !allowUnsupportedTransforms =>
113114
throw new IllegalArgumentException(s"Transform $t is not a supported transform")
114115
}
@@ -221,6 +222,15 @@ abstract class InMemoryBaseTable(
221222
case (v, t) =>
222223
throw new IllegalArgumentException(s"Match: unsupported argument(s) type - ($v, $t)")
223224
}
225+
// the result should be consistent with SignedZerosFunction defined at
226+
// transformFunctions.scala
227+
case NamedTransform("signed_zeros", Seq(ref: NamedReference)) =>
228+
extractor(ref.fieldNames, cleanedSchema, row) match {
229+
case (value: Long, LongType) =>
230+
if (value == 1L) -0.0d else if (value == 2L) 0.0d else value.toDouble
231+
case (v, t) =>
232+
throw new IllegalArgumentException(s"Match: unsupported argument(s) type - ($v, $t)")
233+
}
224234
case ClusterByTransform(columnNames) =>
225235
columnNames.map { colName =>
226236
extractor(colName.fieldNames, cleanedSchema, row)._1

sql/core/src/main/scala/org/apache/spark/sql/execution/exchange/ShuffleExchangeExec.scala

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ package org.apache.spark.sql.execution.exchange
2020
import java.util.concurrent.atomic.AtomicReference
2121
import java.util.function.Supplier
2222

23-
import scala.collection.mutable
2423
import scala.concurrent.{ExecutionContext, Future, Promise}
2524

2625
import org.apache.spark._
@@ -30,12 +29,13 @@ import org.apache.spark.serializer.Serializer
3029
import org.apache.spark.shuffle.{ShuffleWriteMetricsReporter, ShuffleWriteProcessor}
3130
import org.apache.spark.shuffle.sort.SortShuffleManager
3231
import org.apache.spark.sql.catalyst.InternalRow
33-
import org.apache.spark.sql.catalyst.expressions.{Attribute, BoundReference, UnsafeProjection, UnsafeRow}
32+
import org.apache.spark.sql.catalyst.expressions.{Attribute, BoundReference, GenericInternalRow, UnsafeProjection, UnsafeRow}
3433
import org.apache.spark.sql.catalyst.expressions.BindReferences.bindReferences
3534
import org.apache.spark.sql.catalyst.expressions.codegen.LazilyGeneratedOrdering
3635
import org.apache.spark.sql.catalyst.plans.logical.Statistics
3736
import org.apache.spark.sql.catalyst.plans.physical._
3837
import org.apache.spark.sql.catalyst.types.DataTypeUtils
38+
import org.apache.spark.sql.catalyst.util.InternalRowComparableWrapper
3939
import org.apache.spark.sql.execution._
4040
import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics, SQLShuffleReadMetricsReporter, SQLShuffleWriteMetricsReporter}
4141
import org.apache.spark.sql.internal.{SQLConf, StaticSQLConf}
@@ -367,10 +367,14 @@ object ShuffleExchangeExec {
367367
samplePointsPerPartitionHint = SQLConf.get.rangeExchangeSampleSizePerPartition)
368368
case SinglePartition => new ConstantPartitioner
369369
case k @ KeyGroupedPartitioning(expressions, n, _, _, _, _) =>
370-
val valueMap = k.uniquePartitionValues.zipWithIndex.map {
371-
case (partition, index) => (partition.toSeq(expressions.map(_.dataType)), index)
372-
}.toMap
373-
new KeyGroupedPartitioner(mutable.Map(valueMap.toSeq: _*), n)
370+
// The map keys and the lookup keys produced by `getPartitionKeyExtractor` below are
371+
// wrapped over this partitioning's expression data types, so map lookups share the exact
372+
// equivalence (`RowOrdering`, e.g. binary keys by content, -0.0 == 0.0, NaNs equal) that
373+
// grouped and de-duplicated the partition values.
374+
val dataTypes = expressions.map(_.dataType)
375+
val valueMap = k.uniquePartitionValues
376+
.map(new InternalRowComparableWrapper(_, dataTypes)).zipWithIndex.toMap[Any, Int]
377+
new KeyGroupedPartitioner(valueMap, n)
374378
case _ => throw SparkException.internalError(s"Exchange not implemented for $newPartitioning")
375379
// TODO: Handle BroadcastPartitioning.
376380
}
@@ -398,7 +402,20 @@ object ShuffleExchangeExec {
398402
row => projection(row)
399403
case SinglePartition => identity
400404
case KeyGroupedPartitioning(expressions, _, _, _, _, _) =>
401-
row => bindReferences(expressions, outputAttributes).map(_.eval(row))
405+
// Wrap the evaluated partition key so it compares equal to the KeyGroupedPartitioner's
406+
// map keys (wrappers over the same data types) under `RowOrdering` semantics. The wrapped
407+
// row is reused across records; KeyGroupedPartitioner does not retain lookup keys.
408+
val boundExpressions = bindReferences(expressions, outputAttributes).toArray
409+
val dataTypes = expressions.map(_.dataType)
410+
val partitionKeyRow = new GenericInternalRow(boundExpressions.length)
411+
row => {
412+
var i = 0
413+
while (i < boundExpressions.length) {
414+
partitionKeyRow.update(i, boundExpressions(i).eval(row))
415+
i += 1
416+
}
417+
new InternalRowComparableWrapper(partitionKeyRow, dataTypes)
418+
}
402419
case _ => throw SparkException.internalError(s"Exchange not implemented for $newPartitioning")
403420
}
404421

sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ class KeyGroupedPartitioningSuite extends DistributionAndOrderingSuiteBase {
4444
UnboundYearsFunction,
4545
UnboundDaysFunction,
4646
UnboundBucketFunction,
47-
UnboundTruncateFunction)
47+
UnboundTruncateFunction,
48+
UnboundSignedZerosFunction)
4849

4950
override def sparkConf: SparkConf = super.sparkConf
5051
.set(V2_BUCKETING_ENABLED, true)
@@ -1405,6 +1406,132 @@ class KeyGroupedPartitioningSuite extends DistributionAndOrderingSuiteBase {
14051406
}
14061407
}
14071408

1409+
test("SPARK-59054: shuffle one side: partition keys with binary type") {
1410+
val items_partitions = Array(identity("id"))
1411+
createTable(items, Array(
1412+
Column.create("id", BinaryType),
1413+
Column.create("name", StringType),
1414+
Column.create("price", DoubleType)), items_partitions)
1415+
1416+
sql(s"INSERT INTO testcat.ns.$items VALUES " +
1417+
"(X'0101', 'aa', 40.0), " +
1418+
"(X'0202', 'bb', 10.0), " +
1419+
"(X'0303', 'cc', 15.5), " +
1420+
"(X'0404', 'dd', 20.0)")
1421+
1422+
createTable(purchases, Array(
1423+
Column.create("item_id", BinaryType),
1424+
Column.create("price", DoubleType)), Array.empty)
1425+
sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
1426+
"(X'0101', 42.0), (X'0101', 44.0), (X'0202', 11.0), (X'0202', 19.5), " +
1427+
"(X'0303', 26.0), (X'0303', 30.0), (X'0404', 50.0), (X'0404', 60.0)")
1428+
1429+
Seq(true, false).foreach { shuffle =>
1430+
withSQLConf(SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> shuffle.toString) {
1431+
val df = createJoinTestDF(Seq("id" -> "item_id"))
1432+
val shuffles = collectShuffles(df.queryExecution.executedPlan)
1433+
if (shuffle) {
1434+
assert(shuffles.size == 1, "only shuffle one side not report partitioning")
1435+
} else {
1436+
assert(shuffles.size == 2, "should add two side shuffle when bucketing shuffle one " +
1437+
"side is not enabled")
1438+
}
1439+
1440+
checkAnswer(df, Seq(
1441+
Row(Array[Byte](1, 1), "aa", 40.0, 42.0),
1442+
Row(Array[Byte](1, 1), "aa", 40.0, 44.0),
1443+
Row(Array[Byte](2, 2), "bb", 10.0, 11.0),
1444+
Row(Array[Byte](2, 2), "bb", 10.0, 19.5),
1445+
Row(Array[Byte](3, 3), "cc", 15.5, 26.0),
1446+
Row(Array[Byte](3, 3), "cc", 15.5, 30.0),
1447+
Row(Array[Byte](4, 4), "dd", 20.0, 50.0),
1448+
Row(Array[Byte](4, 4), "dd", 20.0, 60.0)))
1449+
}
1450+
}
1451+
}
1452+
1453+
test("SPARK-59054: shuffle one side: struct partition keys with different field names") {
1454+
// Struct equality ignores field names, so joining STRUCT<a:INT> with STRUCT<b:INT> is legal
1455+
// and SPJ stays eligible. The shuffled side's lookup keys carry its own schema while the
1456+
// partitioner's map keys come from the keyed side, so key comparison must not depend on
1457+
// the field names.
1458+
val items_partitions = Array(identity("id"))
1459+
createTable(items, Array(
1460+
Column.create("id", new StructType().add("a", IntegerType)),
1461+
Column.create("name", StringType),
1462+
Column.create("price", DoubleType)), items_partitions)
1463+
1464+
sql(s"INSERT INTO testcat.ns.$items VALUES " +
1465+
"(named_struct('a', 1), 'aa', 40.0), " +
1466+
"(named_struct('a', 2), 'bb', 10.0), " +
1467+
"(named_struct('a', 3), 'cc', 15.5), " +
1468+
"(named_struct('a', 4), 'dd', 20.0)")
1469+
1470+
createTable(purchases, Array(
1471+
Column.create("item_id", new StructType().add("b", IntegerType)),
1472+
Column.create("price", DoubleType)), Array.empty)
1473+
sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
1474+
"(named_struct('b', 1), 42.0), (named_struct('b', 2), 19.5), " +
1475+
"(named_struct('b', 3), 26.0), (named_struct('b', 4), 50.0)")
1476+
1477+
Seq(true, false).foreach { shuffle =>
1478+
withSQLConf(SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> shuffle.toString) {
1479+
val df = createJoinTestDF(Seq("id" -> "item_id"))
1480+
val shuffles = collectShuffles(df.queryExecution.executedPlan)
1481+
if (shuffle) {
1482+
assert(shuffles.size == 1, "only shuffle one side not report partitioning")
1483+
} else {
1484+
assert(shuffles.size == 2, "should add two side shuffle when bucketing shuffle one " +
1485+
"side is not enabled")
1486+
}
1487+
1488+
checkAnswer(df, Seq(
1489+
Row(Row(1), "aa", 40.0, 42.0),
1490+
Row(Row(2), "bb", 10.0, 19.5),
1491+
Row(Row(3), "cc", 15.5, 26.0),
1492+
Row(Row(4), "dd", 20.0, 50.0)))
1493+
}
1494+
}
1495+
}
1496+
1497+
test("SPARK-59054: shuffle one side: partition transform collapsing -0.0 and 0.0") {
1498+
// `signed_zeros` maps id 1 to -0.0 and id 2 to 0.0: two partition keys that are equal
1499+
// under SQL semantics but distinct bitwise, which the grouped side collapses into one
1500+
// partition. Rows of both forms on the shuffled side must land in that partition.
1501+
val items_partitions = Array(
1502+
Expressions.apply("signed_zeros", Expressions.column("id")))
1503+
createTable(items, itemsColumns, items_partitions)
1504+
1505+
sql(s"INSERT INTO testcat.ns.$items VALUES " +
1506+
"(1, 'aa', 40.0, cast('2020-01-01' as timestamp)), " +
1507+
"(2, 'bb', 10.0, cast('2020-01-01' as timestamp)), " +
1508+
"(3, 'cc', 15.5, cast('2020-02-01' as timestamp))")
1509+
1510+
createTable(purchases, purchasesColumns, Array.empty)
1511+
sql(s"INSERT INTO testcat.ns.$purchases VALUES " +
1512+
"(1, 42.0, cast('2020-01-01' as timestamp)), " +
1513+
"(2, 19.5, cast('2020-02-01' as timestamp)), " +
1514+
"(3, 26.0, cast('2023-01-01' as timestamp))")
1515+
1516+
Seq(true, false).foreach { shuffle =>
1517+
withSQLConf(SQLConf.V2_BUCKETING_SHUFFLE_ENABLED.key -> shuffle.toString) {
1518+
val df = createJoinTestDF(Seq("id" -> "item_id"))
1519+
val shuffles = collectShuffles(df.queryExecution.executedPlan)
1520+
if (shuffle) {
1521+
assert(shuffles.size == 1, "only shuffle one side not report partitioning")
1522+
} else {
1523+
assert(shuffles.size == 2, "should add two side shuffle when bucketing shuffle one " +
1524+
"side is not enabled")
1525+
}
1526+
1527+
checkAnswer(df, Seq(
1528+
Row(1, "aa", 40.0, 42.0),
1529+
Row(2, "bb", 10.0, 19.5),
1530+
Row(3, "cc", 15.5, 26.0)))
1531+
}
1532+
}
1533+
}
1534+
14081535
test("SPARK-44641: duplicated records when SPJ is not triggered") {
14091536
val items_partitions = Array(bucket(8, "id"))
14101537
createTable(items, itemsColumns, items_partitions)

sql/core/src/test/scala/org/apache/spark/sql/connector/catalog/functions/transformFunctions.scala

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,32 @@ object StringSelfFunction extends ScalarFunction[UTF8String] {
133133
}
134134
}
135135

136+
object UnboundSignedZerosFunction extends UnboundFunction {
137+
override def bind(inputType: StructType): BoundFunction = {
138+
if (inputType.size == 1 && inputType.head.dataType == LongType) SignedZerosFunction
139+
else throw new UnsupportedOperationException("'signed_zeros' only takes a long as input type")
140+
}
141+
override def description(): String = name()
142+
override def name(): String = "signed_zeros"
143+
}
144+
145+
// A transform whose result type (DoubleType) differs from its input type (LongType), mapping
146+
// 1 to -0.0 and 2 to 0.0 so that two distinct inputs produce partition keys that are equal
147+
// under SQL semantics but distinct bitwise. The result should be consistent with the
148+
// "signed_zeros" NamedTransform defined at InMemoryBaseTable.scala.
149+
object SignedZerosFunction extends ScalarFunction[Double] {
150+
override def inputTypes(): Array[DataType] = Array(LongType)
151+
override def resultType(): DataType = DoubleType
152+
override def name(): String = "signed_zeros"
153+
override def canonicalName(): String = name()
154+
override def toString: String = name()
155+
override def produceResult(input: InternalRow): Double = input.getLong(0) match {
156+
case 1L => -0.0d
157+
case 2L => 0.0d
158+
case v => v.toDouble
159+
}
160+
}
161+
136162
object UnboundTruncateFunction extends UnboundFunction {
137163
override def bind(inputType: StructType): BoundFunction = TruncateFunction
138164
override def description(): String = name()

0 commit comments

Comments
 (0)