-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-31937][SQL] Support processing ArrayType/MapType/StructType data using no-serde mode script transform #30957
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 14 commits
adc9ded
6a7438b
d3b9cec
fdd5225
aa16c8f
092c927
9761c0e
28ad7fa
9ac75fc
33d8b5b
63f07eb
b631b70
b7e7f92
8dec5a1
529d54d
4f0e78f
ed8c54c
520f4b8
97f9d58
b5a4268
76a746e
6aa05fc
9e3f808
3f51d27
adf8a66
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 |
|---|---|---|
|
|
@@ -30,7 +30,7 @@ import org.apache.spark.{SparkException, SparkFiles, TaskContext} | |
| import org.apache.spark.internal.Logging | ||
| import org.apache.spark.rdd.RDD | ||
| import org.apache.spark.sql.catalyst.{CatalystTypeConverters, InternalRow} | ||
| import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeSet, Cast, Expression, GenericInternalRow, UnsafeProjection} | ||
| import org.apache.spark.sql.catalyst.expressions.{Attribute, AttributeSet, Cast, Expression, GenericInternalRow, JsonToStructs, Literal, StructsToJson, UnsafeProjection} | ||
| import org.apache.spark.sql.catalyst.plans.logical.ScriptInputOutputSchema | ||
| import org.apache.spark.sql.catalyst.plans.physical.Partitioning | ||
| import org.apache.spark.sql.catalyst.util.{DateTimeUtils, IntervalUtils} | ||
|
|
@@ -47,7 +47,13 @@ trait BaseScriptTransformationExec extends UnaryExecNode { | |
| def ioschema: ScriptTransformationIOSchema | ||
|
|
||
| protected lazy val inputExpressionsWithoutSerde: Seq[Expression] = { | ||
| input.map(Cast(_, StringType).withTimeZone(conf.sessionLocalTimeZone)) | ||
| input.map { in => | ||
| in.dataType match { | ||
| case _: ArrayType | _: MapType | _: StructType => | ||
| new StructsToJson(in).withTimeZone(conf.sessionLocalTimeZone) | ||
|
Member
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. Is it okay to follow the default behaviour w/o
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.
I am not so familiar about this part's properties. (I am checking these properties)
Member
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.
Ah, it seems fine. Could you try 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.
Updated.
Member
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. How do users know we serialize complex type data using json format? This requires the external process know to parse json back to complext data, isn't? Is it well known to use json for similar feature in Hive?
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.
In the first I implement it in same way as hive, then change to use json serde, you can see discussion in this part of comment #30957 (comment)
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. Hmm, it's a new feature, so we can have a migration guide or release note make user know the new feature and difference.
Member
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. hm. Actually, this feature is not relevant to users using pre-built spark (w/
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.
That's why I implement this in hive's way first and support make json as a serde. If we implement this in json, then some origin hive SQL still can't run when directly upgrade to spark sql (w/ -Phive)。 How about add a configuration such as |
||
| case _ => Cast(in, StringType).withTimeZone(conf.sessionLocalTimeZone) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override def producedAttributes: AttributeSet = outputSet -- inputSet | ||
|
|
@@ -220,6 +226,9 @@ trait BaseScriptTransformationExec extends UnaryExecNode { | |
| case CalendarIntervalType => wrapperConvertException( | ||
| data => IntervalUtils.stringToInterval(UTF8String.fromString(data)), | ||
| converter) | ||
| case _: ArrayType | _: MapType | _: StructType => | ||
| wrapperConvertException(data => JsonToStructs(attr.dataType, Map.empty[String, String], | ||
|
Member
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. This can cause much overhead cuz this make a new object (
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.
This problem also happen in input side's
Member
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.
Really? I think the input side does not have the issue. Why do you think so?
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.
Hmmm, I got your point. I misunderstood you point. Updated, how about current? Test with df.repartition(1) then transform. It only create JsonToStruct once for one partition. |
||
| Literal(data), Some(conf.sessionLocalTimeZone)).eval(), any => any) | ||
|
maropu marked this conversation as resolved.
Outdated
|
||
| case udt: UserDefinedType[_] => | ||
| wrapperConvertException(data => udt.deserialize(data), converter) | ||
| case dt => | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -302,14 +302,16 @@ abstract class BaseScriptTransformationSuite extends SparkPlanTest with SQLTestU | |
| script = "cat", | ||
| output = Seq( | ||
| AttributeReference("a", CalendarIntervalType)(), | ||
| AttributeReference("b", StringType)(), | ||
| AttributeReference("c", StringType)(), | ||
| AttributeReference("d", StringType)(), | ||
| AttributeReference("b", ArrayType(IntegerType))(), | ||
| AttributeReference("c", MapType(StringType, IntegerType))(), | ||
| AttributeReference("d", StructType( | ||
| Array(StructField("_1", IntegerType), | ||
| StructField("_2", IntegerType))))(), | ||
| AttributeReference("e", new SimpleTupleUDT)()), | ||
| child = child, | ||
| ioschema = defaultIOSchema | ||
| ), | ||
| df.select('a, 'b.cast("string"), 'c.cast("string"), 'd.cast("string"), 'e).collect()) | ||
| df.select('a, 'b, 'c, 'd, 'e).collect()) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -471,6 +473,126 @@ abstract class BaseScriptTransformationSuite extends SparkPlanTest with SQLTestU | |
| } | ||
| } | ||
|
|
||
| test("SPARK-31936: Script transform support ArrayType/MapType/StructType (no serde)") { | ||
| assume(TestUtils.testCommandAvailable("python")) | ||
| withTempView("v") { | ||
| val df = Seq( | ||
| (Array(0, 1, 2), Array(Array(0, 1), Array(2)), | ||
| Map("a" -> 1), Map("b" -> Array("a", "b"))), | ||
| (Array(3, 4, 5), Array(Array(3, 4), Array(5)), | ||
| Map("b" -> 2), Map("c" -> Array("c", "d"))), | ||
| (Array(6, 7, 8), Array(Array(6, 7), Array(8)), | ||
| Map("c" -> 3), Map("d" -> Array("e", "f"))) | ||
| ).toDF("a", "b", "c", "d") | ||
| .select('a, 'b, 'c, 'd, | ||
| struct('a, 'b).as("e"), | ||
| struct('a, 'd).as("f"), | ||
| struct(struct('a, 'b), struct('a, 'd)).as("g") | ||
| ) | ||
|
|
||
| checkAnswer( | ||
| df, | ||
| (child: SparkPlan) => createScriptTransformationExec( | ||
| input = Seq( | ||
| df.col("a").expr, | ||
| df.col("b").expr, | ||
| df.col("c").expr, | ||
| df.col("d").expr, | ||
| df.col("e").expr, | ||
| df.col("f").expr, | ||
| df.col("g").expr), | ||
| script = "cat", | ||
| output = Seq( | ||
| AttributeReference("a", ArrayType(IntegerType))(), | ||
| AttributeReference("b", ArrayType(ArrayType(IntegerType)))(), | ||
| AttributeReference("c", MapType(StringType, IntegerType))(), | ||
| AttributeReference("d", MapType(StringType, ArrayType(StringType)))(), | ||
| AttributeReference("e", StructType( | ||
| Array(StructField("a", ArrayType(IntegerType)), | ||
| StructField("b", ArrayType(ArrayType(IntegerType))))))(), | ||
| AttributeReference("f", StructType( | ||
| Array(StructField("a", ArrayType(IntegerType)), | ||
| StructField("d", MapType(StringType, ArrayType(StringType))))))(), | ||
| AttributeReference("g", StructType( | ||
| Array(StructField("col1", StructType( | ||
| Array(StructField("a", ArrayType(IntegerType)), | ||
| StructField("b", ArrayType(ArrayType(IntegerType)))))), | ||
| StructField("col2", StructType( | ||
| Array(StructField("a", ArrayType(IntegerType)), | ||
| StructField("d", MapType(StringType, ArrayType(StringType)))))))))()), | ||
| child = child, | ||
| ioschema = defaultIOSchema | ||
| ), | ||
| df.select('a, 'b, 'c, 'd, 'e, 'f, 'g).collect()) | ||
| } | ||
| } | ||
|
|
||
| test("SPARK-31936: Script transform support nested complex type (no serde)") { | ||
|
Member
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. We still need this test now? On second thought, only the test
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.
Emmm, add a more complex test is more better, how about combine this two test?
Member
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. What's the purpose of this tests? It seems this test does not improve test coverage.
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.
Want to show we can support more complex case. Ok, I will remove this. |
||
| assume(TestUtils.testCommandAvailable("python")) | ||
| withTempView("v") { | ||
| val df = Seq( | ||
| (Array(Array(Array(Array(Array(Array(1, 2, 3)))))), | ||
| Array(Array(Array(Array(Array(Array(1, 2, 3))), Array(Array(Array(1, 2, 3)))))), | ||
| Map("a" -> Map("c" -> Map("d" -> Array(1, 2, 3))), | ||
| "b" -> Map("c" -> Map("d" -> Array(1, 2, 3)))) | ||
| ) | ||
| ).toDF("a", "b", "c").select('a, 'b, 'c, | ||
| struct('a, 'b, 'c).as("d") | ||
| ).select('a, 'b, 'c, 'd, | ||
| struct('c, 'd).as("e") | ||
| ) | ||
|
|
||
| checkAnswer( | ||
| df, | ||
| (child: SparkPlan) => createScriptTransformationExec( | ||
| input = Seq( | ||
| df.col("a").expr, | ||
| df.col("b").expr, | ||
| df.col("c").expr, | ||
| df.col("d").expr, | ||
| df.col("e").expr), | ||
| script = "cat", | ||
| output = Seq( | ||
| AttributeReference("a", | ||
| ArrayType(ArrayType(ArrayType(ArrayType(ArrayType(ArrayType(IntegerType)))))))(), | ||
| AttributeReference("b", | ||
| ArrayType(ArrayType(ArrayType(ArrayType(ArrayType(ArrayType(IntegerType)))))))(), | ||
| AttributeReference("c", | ||
| MapType(StringType, MapType(StringType, | ||
| MapType(StringType, ArrayType(IntegerType)))))(), | ||
| AttributeReference("d", | ||
| StructType(Array( | ||
| StructField("a", | ||
| ArrayType(ArrayType(ArrayType(ArrayType(ArrayType(ArrayType(IntegerType))))))), | ||
| StructField("b", | ||
| ArrayType(ArrayType(ArrayType(ArrayType(ArrayType(ArrayType(IntegerType))))))), | ||
| StructField("c", | ||
| MapType(StringType, MapType(StringType, | ||
| MapType(StringType, ArrayType(IntegerType))))))))(), | ||
| AttributeReference("e", | ||
| StructType(Array( | ||
| StructField("c", | ||
| MapType(StringType, MapType(StringType, | ||
| MapType(StringType, ArrayType(IntegerType))))), | ||
| StructField("d", | ||
| StructType(Array( | ||
| StructField("a", | ||
| ArrayType(ArrayType(ArrayType(ArrayType(ArrayType( | ||
| ArrayType(IntegerType))))))), | ||
| StructField("b", | ||
| ArrayType(ArrayType(ArrayType(ArrayType(ArrayType( | ||
| ArrayType(IntegerType))))))), | ||
| StructField("c", | ||
| MapType(StringType, MapType(StringType, | ||
| MapType(StringType, ArrayType(IntegerType)))))))))))() | ||
| ), | ||
| child = child, | ||
| ioschema = defaultIOSchema | ||
| ), | ||
| df.select('a, 'b, 'c, 'd, 'e).collect()) | ||
| } | ||
| } | ||
|
|
||
| test("SPARK-33934: Add SparkFile's root dir to env property PATH") { | ||
| assume(TestUtils.testCommandAvailable("python")) | ||
| val scriptFilePath = copyAndGetResourceFile("test_script.py", ".py").getAbsoluteFile | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.