-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-47563][SQL] Add map normalization on creation #45721
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
95aad6e
9231547
9649c5d
5f008da
cd4bb3d
15065a1
e9322d3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,8 @@ import scala.collection.mutable | |
|
|
||
| import org.apache.spark.SparkException | ||
| import org.apache.spark.sql.catalyst.InternalRow | ||
| import org.apache.spark.sql.catalyst.expressions.GenericInternalRow | ||
| import org.apache.spark.sql.catalyst.optimizer.NormalizeFloatingNumbers | ||
| import org.apache.spark.sql.errors.QueryExecutionErrors | ||
| import org.apache.spark.sql.internal.SQLConf | ||
| import org.apache.spark.sql.types._ | ||
|
|
@@ -52,18 +54,36 @@ class ArrayBasedMapBuilder(keyType: DataType, valueType: DataType) extends Seria | |
|
|
||
| private val mapKeyDedupPolicy = SQLConf.get.getConf(SQLConf.MAP_KEY_DEDUP_POLICY) | ||
|
|
||
| private lazy val keyNeedNormalize = NormalizeFloatingNumbers.needNormalize(keyType) | ||
|
|
||
| def normalize(value: Any, dataType: DataType): Any = dataType match { | ||
| case FloatType => NormalizeFloatingNumbers.FLOAT_NORMALIZER(value) | ||
| case DoubleType => NormalizeFloatingNumbers.DOUBLE_NORMALIZER(value) | ||
| case ArrayType(dt, _) => | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need to handle complex types, as we use |
||
| new GenericArrayData(value.asInstanceOf[GenericArrayData].array.map { element => | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if we have an array of 1 million strings we will go through each value even though we know we don't need to normalize strings what about doing the same as in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applied |
||
| normalize(element, dt) | ||
| }) | ||
| case StructType(sf) => | ||
| new GenericInternalRow( | ||
| value.asInstanceOf[GenericInternalRow].values.zipWithIndex.map { element => | ||
| normalize(element._1, sf(element._2).dataType) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you could also check if you need to do normalization here right? this way we would avoid normalization of all fields of a struct if only one actually needs it
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As noted by @cloud-fan below, complex types have been dropped. |
||
| }) | ||
| case _ => value | ||
| } | ||
|
|
||
| def put(key: Any, value: Any): Unit = { | ||
| if (key == null) { | ||
| throw QueryExecutionErrors.nullAsMapKeyNotAllowedError() | ||
| } | ||
|
|
||
| val index = keyToIndex.getOrDefault(key, -1) | ||
| val keyNormalized = if (keyNeedNormalize) normalize(key, keyType) else key | ||
| val index = keyToIndex.getOrDefault(keyNormalized, -1) | ||
| if (index == -1) { | ||
| if (size >= ByteArrayMethods.MAX_ROUNDED_ARRAY_LENGTH) { | ||
| throw QueryExecutionErrors.exceedMapSizeLimitError(size) | ||
| } | ||
| keyToIndex.put(key, values.length) | ||
| keys.append(key) | ||
| keyToIndex.put(keyNormalized, values.length) | ||
| keys.append(keyNormalized) | ||
| values.append(value) | ||
| } else { | ||
| if (mapKeyDedupPolicy == SQLConf.MapKeyDedupPolicy.EXCEPTION.toString) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,7 @@ import org.apache.spark.sql.catalyst.InternalRow | |
| import org.apache.spark.sql.catalyst.expressions.{UnsafeArrayData, UnsafeRow} | ||
| import org.apache.spark.sql.catalyst.plans.SQLHelper | ||
| import org.apache.spark.sql.internal.SQLConf | ||
| import org.apache.spark.sql.types.{ArrayType, BinaryType, IntegerType, StructType} | ||
| import org.apache.spark.sql.types.{ArrayType, BinaryType, DoubleType, IntegerType, StructType} | ||
| import org.apache.spark.unsafe.Platform | ||
|
|
||
| class ArrayBasedMapBuilderSuite extends SparkFunSuite with SQLHelper { | ||
|
|
@@ -60,6 +60,60 @@ class ArrayBasedMapBuilderSuite extends SparkFunSuite with SQLHelper { | |
| ) | ||
| } | ||
|
|
||
| test("apply key normalization when creating") { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add another test for successful normalization |
||
| val builderDouble = new ArrayBasedMapBuilder(DoubleType, IntegerType) | ||
| builderDouble.put(-0.0, 1) | ||
| checkError( | ||
| exception = intercept[SparkRuntimeException](builderDouble.put(0.0, 2)), | ||
| errorClass = "DUPLICATED_MAP_KEY", | ||
| parameters = Map( | ||
| "key" -> "0.0", | ||
| "mapKeyDedupPolicy" -> "\"spark.sql.mapKeyDedupPolicy\"") | ||
| ) | ||
|
|
||
| val builderArray = new ArrayBasedMapBuilder(ArrayType(DoubleType), IntegerType) | ||
| builderArray.put(new GenericArrayData(Seq(-0.0)), 1) | ||
| checkError( | ||
| exception = intercept[SparkRuntimeException]( | ||
| builderArray.put(new GenericArrayData(Seq(0.0)), 1)), | ||
| errorClass = "DUPLICATED_MAP_KEY", | ||
| parameters = Map( | ||
| "key" -> "[0.0]", | ||
| "mapKeyDedupPolicy" -> "\"spark.sql.mapKeyDedupPolicy\"") | ||
| ) | ||
|
|
||
| val builderStruct = new ArrayBasedMapBuilder(new StructType().add("i", "double"), IntegerType) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe add a case when array is inside of a struct |
||
| builderStruct.put(InternalRow(-0.0), 1) | ||
| // By default duplicated map key fails the query. | ||
| checkError( | ||
| exception = intercept[SparkRuntimeException](builderStruct.put(InternalRow(0.0), 3)), | ||
| errorClass = "DUPLICATED_MAP_KEY", | ||
| parameters = Map( | ||
| "key" -> "[0.0]", | ||
| "mapKeyDedupPolicy" -> "\"spark.sql.mapKeyDedupPolicy\"") | ||
| ) | ||
|
|
||
| val builderStructWithArray = new ArrayBasedMapBuilder( | ||
| new StructType().add("array", ArrayType(DoubleType) ), IntegerType) | ||
| builderStructWithArray.put(InternalRow(new GenericArrayData(Seq(-0.0))), 1) | ||
| checkError( | ||
| exception = intercept[SparkRuntimeException]( | ||
| builderStructWithArray.put(InternalRow(new GenericArrayData(Seq(0.0))), 1)), | ||
| errorClass = "DUPLICATED_MAP_KEY", | ||
| parameters = Map( | ||
| "key" -> "[[0.0]]", | ||
| "mapKeyDedupPolicy" -> "\"spark.sql.mapKeyDedupPolicy\"") | ||
| ) | ||
| } | ||
|
|
||
| test("successful map normalization on build") { | ||
| val builder = new ArrayBasedMapBuilder(DoubleType, IntegerType) | ||
| builder.put(-0.0, 1) | ||
| val map = builder.build() | ||
| assert(map.numElements() == 1) | ||
| assert(ArrayBasedMapData.toScalaMap(map) == Map(0.0 -> 1)) | ||
| } | ||
|
|
||
| test("remove duplicated keys with last wins policy") { | ||
| withSQLConf(SQLConf.MAP_KEY_DEDUP_POLICY.key -> SQLConf.MapKeyDedupPolicy.LAST_WIN.toString) { | ||
| val builder = new ArrayBasedMapBuilder(IntegerType, IntegerType) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should return a lambda function to do normalization based on the data type, instead of matching the data type per row.