-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-30127][SQL] Support case class parameter for typed Scala UDF #27937
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 5 commits
2421c72
dd90298
2b186bd
84e7a7f
d600cf7
867ad06
23ca098
842d6fa
174e017
8e82f3f
b0b298e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -94,6 +94,7 @@ private[spark] case class SparkUserDefinedFunction( | |
| f: AnyRef, | ||
| dataType: DataType, | ||
| inputSchemas: Seq[Option[ScalaReflection.Schema]], | ||
| inputEncoders: Seq[ExpressionEncoder[_]] = Nil, | ||
|
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. Shall we have a simple
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. or is it possible to get the schema and nullability from the encoder? |
||
| name: Option[String] = None, | ||
| nullable: Boolean = true, | ||
| deterministic: Boolean = true) extends UserDefinedFunction { | ||
|
|
@@ -115,6 +116,7 @@ private[spark] case class SparkUserDefinedFunction( | |
| dataType, | ||
| exprs, | ||
| inputsPrimitive, | ||
| inputEncoders, | ||
| inputTypes, | ||
| udfName = name, | ||
| nullable = nullable, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4388,7 +4388,8 @@ object functions { | |
| def udf[RT: TypeTag](f: Function0[RT]): UserDefinedFunction = { | ||
| val ScalaReflection.Schema(dataType, nullable) = ScalaReflection.schemaFor[RT] | ||
| val inputSchemas = Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas) | ||
| val inputEncoders: Seq[ExpressionEncoder[_]] = Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas, inputEncoders) | ||
| if (nullable) udf else udf.asNonNullable() | ||
| } | ||
|
|
||
|
|
@@ -4404,7 +4405,8 @@ object functions { | |
| def udf[RT: TypeTag, A1: TypeTag](f: Function1[A1, RT]): UserDefinedFunction = { | ||
| val ScalaReflection.Schema(dataType, nullable) = ScalaReflection.schemaFor[RT] | ||
| val inputSchemas = Try(ScalaReflection.schemaFor(typeTag[A1])).toOption :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas) | ||
| val inputEncoders: Seq[ExpressionEncoder[_]] = ExpressionEncoder[A1]() :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas, inputEncoders) | ||
| if (nullable) udf else udf.asNonNullable() | ||
| } | ||
|
|
||
|
|
@@ -4420,7 +4422,8 @@ object functions { | |
| def udf[RT: TypeTag, A1: TypeTag, A2: TypeTag](f: Function2[A1, A2, RT]): UserDefinedFunction = { | ||
| val ScalaReflection.Schema(dataType, nullable) = ScalaReflection.schemaFor[RT] | ||
| val inputSchemas = Try(ScalaReflection.schemaFor(typeTag[A1])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A2])).toOption :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas) | ||
| val inputEncoders: Seq[ExpressionEncoder[_]] = ExpressionEncoder[A1]() :: ExpressionEncoder[A2]() :: Nil | ||
|
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 the type is |
||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas, inputEncoders) | ||
| if (nullable) udf else udf.asNonNullable() | ||
| } | ||
|
|
||
|
|
@@ -4436,7 +4439,8 @@ object functions { | |
| def udf[RT: TypeTag, A1: TypeTag, A2: TypeTag, A3: TypeTag](f: Function3[A1, A2, A3, RT]): UserDefinedFunction = { | ||
| val ScalaReflection.Schema(dataType, nullable) = ScalaReflection.schemaFor[RT] | ||
| val inputSchemas = Try(ScalaReflection.schemaFor(typeTag[A1])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A2])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A3])).toOption :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas) | ||
| val inputEncoders: Seq[ExpressionEncoder[_]] = ExpressionEncoder[A1]() :: ExpressionEncoder[A2]() :: ExpressionEncoder[A3]() :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas, inputEncoders) | ||
| if (nullable) udf else udf.asNonNullable() | ||
| } | ||
|
|
||
|
|
@@ -4452,7 +4456,8 @@ object functions { | |
| def udf[RT: TypeTag, A1: TypeTag, A2: TypeTag, A3: TypeTag, A4: TypeTag](f: Function4[A1, A2, A3, A4, RT]): UserDefinedFunction = { | ||
| val ScalaReflection.Schema(dataType, nullable) = ScalaReflection.schemaFor[RT] | ||
| val inputSchemas = Try(ScalaReflection.schemaFor(typeTag[A1])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A2])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A3])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A4])).toOption :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas) | ||
| val inputEncoders: Seq[ExpressionEncoder[_]] = ExpressionEncoder[A1]() :: ExpressionEncoder[A2]() :: ExpressionEncoder[A3]() :: ExpressionEncoder[A4]() :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas, inputEncoders) | ||
| if (nullable) udf else udf.asNonNullable() | ||
| } | ||
|
|
||
|
|
@@ -4468,7 +4473,8 @@ object functions { | |
| def udf[RT: TypeTag, A1: TypeTag, A2: TypeTag, A3: TypeTag, A4: TypeTag, A5: TypeTag](f: Function5[A1, A2, A3, A4, A5, RT]): UserDefinedFunction = { | ||
| val ScalaReflection.Schema(dataType, nullable) = ScalaReflection.schemaFor[RT] | ||
| val inputSchemas = Try(ScalaReflection.schemaFor(typeTag[A1])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A2])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A3])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A4])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A5])).toOption :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas) | ||
| val inputEncoders: Seq[ExpressionEncoder[_]] = ExpressionEncoder[A1]() :: ExpressionEncoder[A2]() :: ExpressionEncoder[A3]() :: ExpressionEncoder[A4]() :: ExpressionEncoder[A5]() :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas, inputEncoders) | ||
| if (nullable) udf else udf.asNonNullable() | ||
| } | ||
|
|
||
|
|
@@ -4484,7 +4490,8 @@ object functions { | |
| def udf[RT: TypeTag, A1: TypeTag, A2: TypeTag, A3: TypeTag, A4: TypeTag, A5: TypeTag, A6: TypeTag](f: Function6[A1, A2, A3, A4, A5, A6, RT]): UserDefinedFunction = { | ||
| val ScalaReflection.Schema(dataType, nullable) = ScalaReflection.schemaFor[RT] | ||
| val inputSchemas = Try(ScalaReflection.schemaFor(typeTag[A1])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A2])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A3])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A4])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A5])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A6])).toOption :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas) | ||
| val inputEncoders: Seq[ExpressionEncoder[_]] = ExpressionEncoder[A1]() :: ExpressionEncoder[A2]() :: ExpressionEncoder[A3]() :: ExpressionEncoder[A4]() :: ExpressionEncoder[A5]() :: ExpressionEncoder[A6]() :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas, inputEncoders) | ||
| if (nullable) udf else udf.asNonNullable() | ||
| } | ||
|
|
||
|
|
@@ -4500,7 +4507,8 @@ object functions { | |
| def udf[RT: TypeTag, A1: TypeTag, A2: TypeTag, A3: TypeTag, A4: TypeTag, A5: TypeTag, A6: TypeTag, A7: TypeTag](f: Function7[A1, A2, A3, A4, A5, A6, A7, RT]): UserDefinedFunction = { | ||
| val ScalaReflection.Schema(dataType, nullable) = ScalaReflection.schemaFor[RT] | ||
| val inputSchemas = Try(ScalaReflection.schemaFor(typeTag[A1])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A2])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A3])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A4])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A5])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A6])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A7])).toOption :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas) | ||
| val inputEncoders: Seq[ExpressionEncoder[_]] = ExpressionEncoder[A1]() :: ExpressionEncoder[A2]() :: ExpressionEncoder[A3]() :: ExpressionEncoder[A4]() :: ExpressionEncoder[A5]() :: ExpressionEncoder[A6]() :: ExpressionEncoder[A7]() :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas, inputEncoders) | ||
| if (nullable) udf else udf.asNonNullable() | ||
| } | ||
|
|
||
|
|
@@ -4516,7 +4524,8 @@ object functions { | |
| def udf[RT: TypeTag, A1: TypeTag, A2: TypeTag, A3: TypeTag, A4: TypeTag, A5: TypeTag, A6: TypeTag, A7: TypeTag, A8: TypeTag](f: Function8[A1, A2, A3, A4, A5, A6, A7, A8, RT]): UserDefinedFunction = { | ||
| val ScalaReflection.Schema(dataType, nullable) = ScalaReflection.schemaFor[RT] | ||
| val inputSchemas = Try(ScalaReflection.schemaFor(typeTag[A1])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A2])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A3])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A4])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A5])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A6])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A7])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A8])).toOption :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas) | ||
| val inputEncoders: Seq[ExpressionEncoder[_]] = ExpressionEncoder[A1]() :: ExpressionEncoder[A2]() :: ExpressionEncoder[A3]() :: ExpressionEncoder[A4]() :: ExpressionEncoder[A5]() :: ExpressionEncoder[A6]() :: ExpressionEncoder[A7]() :: ExpressionEncoder[A8]() :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas, inputEncoders) | ||
| if (nullable) udf else udf.asNonNullable() | ||
| } | ||
|
|
||
|
|
@@ -4532,7 +4541,8 @@ object functions { | |
| def udf[RT: TypeTag, A1: TypeTag, A2: TypeTag, A3: TypeTag, A4: TypeTag, A5: TypeTag, A6: TypeTag, A7: TypeTag, A8: TypeTag, A9: TypeTag](f: Function9[A1, A2, A3, A4, A5, A6, A7, A8, A9, RT]): UserDefinedFunction = { | ||
| val ScalaReflection.Schema(dataType, nullable) = ScalaReflection.schemaFor[RT] | ||
| val inputSchemas = Try(ScalaReflection.schemaFor(typeTag[A1])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A2])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A3])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A4])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A5])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A6])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A7])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A8])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A9])).toOption :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas) | ||
| val inputEncoders: Seq[ExpressionEncoder[_]] = ExpressionEncoder[A1]() :: ExpressionEncoder[A2]() :: ExpressionEncoder[A3]() :: ExpressionEncoder[A4]() :: ExpressionEncoder[A5]() :: ExpressionEncoder[A6]() :: ExpressionEncoder[A7]() :: ExpressionEncoder[A8]() :: ExpressionEncoder[A9]() :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas, inputEncoders) | ||
| if (nullable) udf else udf.asNonNullable() | ||
| } | ||
|
|
||
|
|
@@ -4548,7 +4558,8 @@ object functions { | |
| def udf[RT: TypeTag, A1: TypeTag, A2: TypeTag, A3: TypeTag, A4: TypeTag, A5: TypeTag, A6: TypeTag, A7: TypeTag, A8: TypeTag, A9: TypeTag, A10: TypeTag](f: Function10[A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, RT]): UserDefinedFunction = { | ||
| val ScalaReflection.Schema(dataType, nullable) = ScalaReflection.schemaFor[RT] | ||
| val inputSchemas = Try(ScalaReflection.schemaFor(typeTag[A1])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A2])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A3])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A4])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A5])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A6])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A7])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A8])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A9])).toOption :: Try(ScalaReflection.schemaFor(typeTag[A10])).toOption :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas) | ||
| val inputEncoders: Seq[ExpressionEncoder[_]] = ExpressionEncoder[A1]() :: ExpressionEncoder[A2]() :: ExpressionEncoder[A3]() :: ExpressionEncoder[A4]() :: ExpressionEncoder[A5]() :: ExpressionEncoder[A6]() :: ExpressionEncoder[A7]() :: ExpressionEncoder[A8]() :: ExpressionEncoder[A9]() :: ExpressionEncoder[A10]() :: Nil | ||
| val udf = SparkUserDefinedFunction(f, dataType, inputSchemas, inputEncoders) | ||
| if (nullable) udf else udf.asNonNullable() | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,7 +33,6 @@ import org.apache.spark.sql.test.SQLTestData._ | |
| import org.apache.spark.sql.types._ | ||
| import org.apache.spark.sql.util.QueryExecutionListener | ||
|
|
||
|
|
||
| private case class FunctionResult(f1: String, f2: String) | ||
|
|
||
| class UDFSuite extends QueryTest with SharedSparkSession { | ||
|
|
@@ -551,4 +550,32 @@ class UDFSuite extends QueryTest with SharedSparkSession { | |
| } | ||
| assert(e.getMessage.contains("Invalid arguments for function cast")) | ||
| } | ||
|
|
||
| test("only one case class parameter") { | ||
| val f = (d: TestData) => d.key * d.value.toInt | ||
| val myUdf = udf(f) | ||
| val df = Seq(("data", TestData(50, "2"))).toDF("col1", "col2") | ||
| checkAnswer(df.select(myUdf(Column("col2"))), Row(100) :: Nil) | ||
| } | ||
|
|
||
| test("one case class with primitive parameter") { | ||
| val f = (i: Int, p: TestData) => p.key * i | ||
| val myUdf = udf(f) | ||
| val df = Seq((2, TestData(50, "data"))).toDF("col1", "col2") | ||
| checkAnswer(df.select(myUdf(Column("col1"), Column("col2"))), Row(100) :: Nil) | ||
| } | ||
|
|
||
| test("multiple case class parameters") { | ||
| val f = (d1: TestData, d2: TestData) => d1.key * d2.key | ||
| val myUdf = udf(f) | ||
| val df = Seq((TestData(10, "d1"), TestData(50, "d2"))).toDF("col1", "col2") | ||
| checkAnswer(df.select(myUdf(Column("col1"), Column("col2"))), Row(500) :: Nil) | ||
| } | ||
|
|
||
| test("input case class parameter and return case class ") { | ||
|
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 we test nested case calss as well? |
||
| val f = (d1: TestData) => TestData(d1.key * 2, "copy") | ||
| val myUdf = udf(f) | ||
| val df = Seq(("data", TestData(50, "d2"))).toDF("col1", "col2") | ||
| checkAnswer(df.select(myUdf(Column("col2"))), Row(Row(100, "copy")) :: Nil) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
quite a lot of outside libraries use this constructor (or to be precise the older equivalent version with DataType instead of Schema that used to be in UserDefinedFunction). see for example:
https://github.com/salesforce/TransmogrifAI/blob/master/features/src/main/scala/com/salesforce/op/features/FeatureSparkTypes.scala#L269
can you suggest a workaround? i see no easy way to go from DataType to ExpressionEncoder...
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.
Hi @koertkuipers , the example seems use
UserDefinedFunctioninstead ofSparkUserDefinedFunction. And since Spark v3.0,UserDefinedFunctionhas been changed from case class to abstract class, see SPARK-26216.cc @cloud-fan
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.
hi @Ngone51 yes changing it to use SparkUserDefinedFunction instead of UserDefinedFunction was easy. but the change from inputSchemas to inputEncoders poses a bit of a challenge.
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.
It's not go from DataType to ExpressionEncoder, which is impossible. It's from
T <: FeatureType : TypeTagtoExpressionEncoder[T]. I think you need to adjustFeatureSparkTypesand support creatingExpressionEncoder. This is the cost of relying on internal APIs.Uh oh!
There was an error while loading. Please reload this page.
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.
while its nice to see case classes supported, i was surprised to find that another feature of Encoder is not supported: Option to indicate a udf argument could be null. for example one could define a udf for
(Option[String], Int) => Stringto explicitly handle the case where the first input argument is null (or should i say its now None).a better example is
(String, Option[Int]) => Stringto work around the udf behavior that if primitivess are null the udf is not called and the output is also null.especially when designing generic systems that use udfs this becomes really important. say you write something that does transform
(X, Y) => Xfor generic types X and Y (with typetags). now say Y could be null (the udf could be called after a left join for example where Y could be joined in). the behavior would now change based on the concrete type of Y... for Strings nulls would get passed in to the udf while for Ints the udf would be skipped. i dont think anyone would want to deal with that. so instead you wants to write a udf for(X, Option[Y]) => Xin this case.Uh oh!
There was an error while loading. Please reload this page.
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.
in my testing i also found the api now to be somewhat inconsistent/confusing. basically sometimes
CatalystTypeConverters.createToScalaConverteris used and sometimesExpressionEncoder.fromRow, depending solely on if the the argument is a top level struct or not. butCatalystTypeConverters.createToScalaConverterandExpressionEncoder.fromRowbehave very differently, leading to inconsistent application.for example this (contrived) usage works:
but this does not:
and while Option works nicely in Person case class (and also in tuples) Option does not work in a simple Seq:
and Option also does not work for a function argument:
this inconsistency will be hard to understand. and this inconsistency is not limited to Options. it also applies to many other things. for example tuples inside maps will not work (still have to use Row there) but tuples inside maps will work if its inside a case class. that is hard to explain to a user...
finally let me give some background why i am a little nervous about this change...
spark udfs have been somewhat limited for a long time. no support for case class, tuples, options. so many libraries have worked around that by defining their own udfs on top on SparkUserDefinedFunction. we do this inhouse too. it is easy to do this with type classes thanks to the composability of inputSchemas.
so now you replaced inputSchemas with inputEncoders. but ExpressionEncoder and TypeTags are not composable. i do not see a way for us to build on top of this for our own inhouse udfs. so then the alternative for us is to abandon our inhouse udfs and start using spark's udfs again, which now support case classes and tuples, which is nice! but the inconsistency of the api and lack of support for option makes that currently not viable to me. i realize this is a spark internal api and this is entirely my own problem. but i thought it was worth pointing out because i suspect i am not the only one that has done this. i think this is one of the more typical workarounds people have done using spark (and i am aware of multiple implementations of this workaround).
sorry for the long posts(s)
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.
inputSchemasis composable but it contains too little information, that's why the Spark UDF was so limited before.Our goal is to make Spark UDF powerful enough so that people don't need to use internal APIs to build inhouse UDFs. But you are right that the support is not completed. @Ngone51 Can you take a closer look and see how to make it completed?
BTW, if you do need to keep your inhouse UDFs for a while, there is a way to create
ExpressionEncoderfromSeq[DataType], by callingRowEncoder.apply. It only supports standard Spark external types, i.e.Row, notcase class, which is the same as older versions of Spark.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.
Yea, I'm looking into it.
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.
@cloud-fan thanks i will take a look at RowEncoder