-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-23593][SQL] Add interpreted execution for InitializeJavaBean expression #20756
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 10 commits
978080b
0c48a9b
b8f171e
b51303a
6372f04
970ed6c
e20c207
f3fdf57
1a78334
e7640e1
0d6b3d3
2954e8d
96a8fe6
573db59
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 |
|---|---|---|
|
|
@@ -1261,8 +1261,42 @@ case class InitializeJavaBean(beanInstance: Expression, setters: Map[String, Exp | |
| override def children: Seq[Expression] = beanInstance +: setters.values.toSeq | ||
| override def dataType: DataType = beanInstance.dataType | ||
|
|
||
| override def eval(input: InternalRow): Any = | ||
| throw new UnsupportedOperationException("Only code-generated evaluation is supported.") | ||
| private lazy val resolvedSetters = { | ||
| assert(beanInstance.dataType.isInstanceOf[ObjectType]) | ||
|
|
||
| val ObjectType(beanClass) = beanInstance.dataType | ||
| setters.map { | ||
| case (name, expr) => | ||
| // Looking for known type mapping first, then using Class attached in `ObjectType`. | ||
| // Finally also looking for general `Object`-type parameter for generic methods. | ||
| val paramTypes = CallMethodViaReflection.typeMapping.getOrElse(expr.dataType, | ||
|
Member
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 #20753, we need to add other types into the mapping.
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. Ok, who is making those changes? You or @kiszk?
Member
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 may have no time to do tonight, if @kiszk do not make those changes, I will do it tomorrow.
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. I added the similar variable If we can expand this support to additional classes (e.g. WDYT? @hvanhovell and @viirya
Member
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. Is there any special reason it only supports basically primitives and string?
Member
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. If we want to expand the support, maybe we can have another PR to expand it in
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. I see. Is it better to have separate map variables, for now? cc @hvanhovell
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. Sorry for not coming back to this sooner. AFAIK And finally which PR will implement this. cc @maropu for visibility.
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. |
||
| Seq(expr.dataType.asInstanceOf[ObjectType].cls)) ++ Seq(classOf[Object]) | ||
| val methods = paramTypes.flatMap { fieldClass => | ||
| try { | ||
| Some(beanClass.getDeclaredMethod(name, fieldClass)) | ||
| } catch { | ||
| case e: NoSuchMethodException => None | ||
| } | ||
| } | ||
| if (methods.isEmpty) { | ||
| throw new NoSuchMethodException(s"""A method named "$name" is not declared """ + | ||
| "in any enclosing class nor any supertype") | ||
| } | ||
| methods.head -> expr | ||
| } | ||
| } | ||
|
|
||
| override def eval(input: InternalRow): Any = { | ||
| val instance = beanInstance.eval(input) | ||
| if (instance != null) { | ||
| val bean = instance.asInstanceOf[Object] | ||
| resolvedSetters.foreach { | ||
| case (setter, expr) => | ||
| setter.invoke(bean, expr.eval(input).asInstanceOf[AnyRef]) | ||
|
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. There is a subtle difference between code generation and interpreted mode here. A null value for an expression that maps to a java primitive will be some default value (e.g. -1) for code generation and I am not sure we should address this, because I am not 100% if this can ever happen. @cloud-fan could you shed some light on this?
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. Correct me if I'm wrong: IMO, I think the codegen version is wrong. In general we should not read the codegen value if it's marked as null. This doesn't cause any problem, because we only use these object expressions to generate encoders, which means the parameter for a primitive setter won't be null. But if we treat these expressions as a general DSL, we should be careful about this.
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. @viirya can you add a null check to both the interpreted and code generated version? Thanks!
Member
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. @hvanhovell For non-primitive setter, seems it is valid to pass a null into a setter method. A null check means we don't allow such case at all?
Member
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. ping @hvanhovell
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. @viirya it might be valid to invoke method with a
Member
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. Ok. Done. |
||
| } | ||
| } | ||
| instance | ||
| } | ||
|
|
||
| override def doGenCode(ctx: CodegenContext, ev: ExprCode): ExprCode = { | ||
| val instanceGen = beanInstance.genCode(ctx) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,7 +55,8 @@ trait ExpressionEvalHelper extends GeneratorDrivenPropertyChecks { | |
|
|
||
| protected def checkEvaluation( | ||
| expression: => Expression, expected: Any, inputRow: InternalRow = EmptyRow): Unit = { | ||
| val expr = prepareEvaluation(expression) | ||
| // Make it as method to obtain fresh expression everytime. | ||
|
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. Why this change?
Member
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. The content of bean instance will be changed after first evaluation of interpreted execution. For example, in the added unit test, the input bean of the later evaluation will become
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. Are we using a literal? Ok, makes sense. |
||
| def expr = prepareEvaluation(expression) | ||
| val catalystValue = CatalystTypeConverters.convertToCatalyst(expected) | ||
| checkEvaluationWithoutCodegen(expr, catalystValue, inputRow) | ||
| checkEvaluationWithGeneratedMutableProjection(expr, catalystValue, inputRow) | ||
|
|
@@ -111,12 +112,14 @@ trait ExpressionEvalHelper extends GeneratorDrivenPropertyChecks { | |
| val errMsg = intercept[T] { | ||
| eval | ||
| }.getMessage | ||
| if (errMsg != expectedErrMsg) { | ||
| if (!errMsg.contains(expectedErrMsg)) { | ||
|
Member
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. For codegen error, it has very verbose message like: So changes it to test if it contains the given error message. |
||
| fail(s"Expected error message is `$expectedErrMsg`, but `$errMsg` found") | ||
| } | ||
| } | ||
| } | ||
| val expr = prepareEvaluation(expression) | ||
|
|
||
| // Make it as method to obtain fresh expression everytime. | ||
| def expr = prepareEvaluation(expression) | ||
| checkException(evaluateWithoutCodegen(expr, inputRow), "non-codegen mode") | ||
| checkException(evaluateWithGeneratedMutableProjection(expr, inputRow), "codegen mode") | ||
| if (GenerateUnsafeProjection.canSupport(expr.dataType)) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,6 +68,32 @@ class ObjectExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { | |
| mapEncoder.serializer.head, mapExpected, mapInputRow) | ||
| } | ||
|
|
||
| test("SPARK-23593: InitializeJavaBean should support interpreted execution") { | ||
| val list = new java.util.LinkedList[Int]() | ||
| list.add(1) | ||
|
|
||
| val initializeBean = InitializeJavaBean(Literal.fromObject(new java.util.LinkedList[Int]), | ||
| Map("add" -> Literal(1))) | ||
| checkEvaluation(initializeBean, list, InternalRow.fromSeq(Seq())) | ||
|
|
||
| val initializeWithNonexistingMethod = InitializeJavaBean( | ||
| Literal.fromObject(new java.util.LinkedList[Int]), | ||
|
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. Can you also add a test for when the parameter types do not match up?
Member
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. Added below. Note that because for generic method, its parameter type is |
||
| Map("nonexisting" -> Literal(1))) | ||
| checkExceptionInExpression[Exception](initializeWithNonexistingMethod, | ||
| InternalRow.fromSeq(Seq()), | ||
| """A method named "nonexisting" is not declared in any enclosing class """ + | ||
| "nor any supertype") | ||
|
|
||
| val initializeWithWrongParamType = InitializeJavaBean( | ||
| Literal.fromObject(new TestBean), | ||
| Map("setX" -> Literal("1"))) | ||
| intercept[Exception] { | ||
| evaluateWithoutCodegen(initializeWithWrongParamType, InternalRow.fromSeq(Seq())) | ||
| }.getMessage.contains( | ||
|
Member
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. For codegen the compile exception is like: I'm not sure if we want to exactly match this kind of exception message from interpreted execution. Might be a little overkill to do that by looking methods with same name. So currently I only test interpreted execution. |
||
| """A method named "setX" is not declared in any enclosing class """ + | ||
| "nor any supertype") | ||
| } | ||
|
|
||
| test("SPARK-23585: UnwrapOption should support interpreted execution") { | ||
| val cls = classOf[Option[Int]] | ||
| val inputObject = BoundReference(0, ObjectType(cls), nullable = true) | ||
|
|
@@ -110,3 +136,9 @@ class ObjectExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper { | |
| "The 0th field 'c0' of input row cannot be null.") | ||
| } | ||
| } | ||
|
|
||
| class TestBean extends Serializable { | ||
| private var x: Int = 0 | ||
|
|
||
| def setX(i: Int): Unit = x = i | ||
| } | ||
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.
better to put
assert(beanInstance.dataType.isInstanceOf[ObjectType])in the constructor?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.
Ok.