-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-31008][SQL] Support json_array_length function #27759
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 11 commits
79cc912
243fead
7d6d225
5d782ce
5f0d010
4e06ff2
27a5f95
3bcc1cd
593dcbb
b2c4349
b0c51dc
391f33d
313151f
f44e24e
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 | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -796,3 +796,75 @@ case class SchemaOfJson( | |||||||||||||
|
|
||||||||||||||
| override def prettyName: String = "schema_of_json" | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * A function that returns the number of elements in outer JSON array. | ||||||||||||||
| */ | ||||||||||||||
| @ExpressionDescription( | ||||||||||||||
| usage = "_FUNC_(jsonArray) - Returns the number of elements in outer JSON array.", | ||||||||||||||
| arguments = """ | ||||||||||||||
| Arguments: | ||||||||||||||
| * jsonArray - A JSON array. An exception is thrown if any other valid JSON strings are passed. | ||||||||||||||
| `NULL` is returned in case of an invalid JSON. | ||||||||||||||
|
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. Shall we mention |
||||||||||||||
| """, | ||||||||||||||
| examples = """ | ||||||||||||||
| Examples: | ||||||||||||||
| > SELECT _FUNC_('[1,2,3,4]'); | ||||||||||||||
| 4 | ||||||||||||||
| > SELECT _FUNC_('[1,2,3,{"f1":1,"f2":[5,6]},4]'); | ||||||||||||||
| 5 | ||||||||||||||
| > SELECT _FUNC_('[1,2'); | ||||||||||||||
| NULL | ||||||||||||||
| """, | ||||||||||||||
| since = "3.1.0" | ||||||||||||||
| ) | ||||||||||||||
| case class LengthOfJsonArray(child: Expression) extends UnaryExpression | ||||||||||||||
| with CodegenFallback with ExpectsInputTypes { | ||||||||||||||
|
|
||||||||||||||
| override def inputTypes: Seq[AbstractDataType] = Seq(StringType) | ||||||||||||||
|
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.
|
||||||||||||||
| override def dataType: DataType = IntegerType | ||||||||||||||
| override def nullable: Boolean = true | ||||||||||||||
| override def prettyName: String = "json_array_length" | ||||||||||||||
|
|
||||||||||||||
| override def eval(input: InternalRow): Any = { | ||||||||||||||
| val json = child.eval(input).asInstanceOf[UTF8String] | ||||||||||||||
|
dongjoon-hyun marked this conversation as resolved.
|
||||||||||||||
| // return null for null input | ||||||||||||||
| if (json == null) { | ||||||||||||||
| return null | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| try { | ||||||||||||||
| Utils.tryWithResource(CreateJacksonParser.utf8String(SharedFactory.jsonFactory, json)) { | ||||||||||||||
| parser => { | ||||||||||||||
| // return null if null array is encountered. | ||||||||||||||
| if (parser.nextToken() == null) { | ||||||||||||||
| return null | ||||||||||||||
| } | ||||||||||||||
| // Parse the array to compute its length. | ||||||||||||||
| parseCounter(parser, input) | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| } catch { | ||||||||||||||
| case _: JsonProcessingException | _: IOException => null | ||||||||||||||
|
dongjoon-hyun marked this conversation as resolved.
|
||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| private def parseCounter(parser: JsonParser, input: InternalRow): Int = { | ||||||||||||||
| var length = 0; | ||||||||||||||
|
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. Ur, shall we remove |
||||||||||||||
| // Only JSON array are supported for this function. | ||||||||||||||
| if (parser.currentToken != JsonToken.START_ARRAY) { | ||||||||||||||
| throw new IllegalArgumentException(s"$prettyName can only be called on JSON array.") | ||||||||||||||
|
dongjoon-hyun marked this conversation as resolved.
Outdated
|
||||||||||||||
| } | ||||||||||||||
| // Keep traversing until the end of JSON array | ||||||||||||||
| while(parser.nextToken() != JsonToken.END_ARRAY) { | ||||||||||||||
|
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. Can nextToken return null? Looks like it can: spark/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonUtils.scala Lines 28 to 33 in 8e280ce
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. It returns null when end of input is reached. |
||||||||||||||
| // Null indicates end of input. | ||||||||||||||
| if (parser.currentToken == null) { | ||||||||||||||
| throw new IllegalArgumentException("Please provide a valid JSON array.") | ||||||||||||||
|
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'm wondering if we can have a test coverage for this code path. Otherwise, this code path can be considered as a dead code.
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. Yeah. It is unreachable code. Because if we encounter null before |
||||||||||||||
| } | ||||||||||||||
| length += 1 | ||||||||||||||
| // skip all the child of inner object or array | ||||||||||||||
| parser.skipChildren() | ||||||||||||||
|
dongjoon-hyun marked this conversation as resolved.
|
||||||||||||||
| } | ||||||||||||||
| length | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -790,4 +790,27 @@ class JsonExpressionsSuite extends SparkFunSuite with ExpressionEvalHelper with | |
| checkDecimalInfer(_, """struct<d:decimal(7,3)>""") | ||
| } | ||
| } | ||
|
|
||
| test("Length of JSON array") { | ||
| Seq( | ||
| ("""""", null), | ||
| ("""[1,2,3]""", 3), | ||
| ("""[]""", 0), | ||
| ("""[[1],[2,3],[]]""", 3), | ||
|
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 is different from the example I gave you. Do you have any reason to prefer |
||
| ("""[{"a":123},{"b":"hello"}]""", 2), | ||
| ("""[1,2,3,[33,44],{"key":[2,3,4]}]""", 5), | ||
| ("""[1,2,3,4,5""", null), | ||
| ("""Random String""", null) | ||
| ).foreach{ | ||
|
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. nit. |
||
| case(literal, expectedValue) => | ||
| checkEvaluation(LengthOfJsonArray(Literal(literal)), expectedValue) | ||
| } | ||
|
|
||
| val not_a_json_array = """{"key":"not a json array"}""" | ||
|
|
||
| checkExceptionInExpression[IllegalArgumentException]( | ||
| LengthOfJsonArray(Literal(not_a_json_array)), | ||
| expectedErrMsg = "json_array_length can only be called on JSON array" | ||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -710,4 +710,11 @@ class JsonFunctionsSuite extends QueryTest with SharedSparkSession { | |
| Seq(Row("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. Thank you for deletion, but to be complete, you need to recover to the master branch version. You may need something like the following. |
||
| test("json_array_length") { | ||
| val df = Seq(1).toDF("json") | ||
| val errMsg = intercept[AnalysisException] { | ||
| df.selectExpr("json_array_length(json)") | ||
| }.getMessage | ||
| assert(errMsg.contains("due to data type mismatch")) | ||
| } | ||
|
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. Shall we remove this because this is already covered at |
||
| } | ||
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.
Since JSON can be a nested structure, there might be multiple
innerandouter. Can we usethe outmostinstead ofouter?