-
Notifications
You must be signed in to change notification settings - Fork 29.3k
[SPARK-30648][SQL] Support filters pushdown in JSON datasource #27366
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 66 commits
a4c6c93
ac7c730
b0ff6c9
a79dacd
bb12fd5
ccc0940
b7f17b1
521a685
a8486bf
b0d6939
e814986
c05b1e9
15f0390
02aca76
bd1d093
1c64b37
f83b93a
dd547ce
0ada227
f0d6a72
52e65d0
c60b332
617197a
03da0b2
a122fb7
0aa8499
ee53875
3381607
144f5a7
94a22e1
5d0ead1
bd1853c
330aae7
449a7e5
4527660
675682b
23191e9
67a74ad
4a7f0b0
d9bb50f
1230f60
6e0aa47
a455977
942b9a9
39b4487
e53171b
f4c63fa
bd5b9a9
a583247
9c76267
3279fcb
e78bacc
dc66f82
02cd63d
443992a
648c23b
1c4f281
0e6ffb5
01a7ee3
4e623b3
262e3c7
f2d0cad
db1ac35
4c37c9a
8bfd599
e08b6e0
9012456
31ad92c
50b9bb2
9a8ba45
90559de
38eb601
0d44c04
e57ebd1
36412ca
b7bdcff
eb79544
0a133ad
d4b88d4
6921415
0a1e575
3df60c1
649d187
2173343
e55bb50
60cd07a
8ecede6
77bd18e
864ba7d
0155b05
193a57a
35c056e
43f75a6
4d5fe2c
ba7db8b
0ca1417
18dea26
3bf3270
3f7d338
6938ec5
fc725bc
57524d6
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 |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
|
|
||
| package org.apache.spark.sql.catalyst | ||
|
|
||
| import scala.util.Try | ||
|
|
||
| import org.apache.spark.sql.catalyst.StructFilters._ | ||
| import org.apache.spark.sql.catalyst.expressions._ | ||
| import org.apache.spark.sql.sources | ||
| import org.apache.spark.sql.types.{BooleanType, StructType} | ||
|
|
||
| /** | ||
| * The class provides API for applying pushed down filters to partially or | ||
| * fully set internal rows that have the struct schema. | ||
| * | ||
| * @param pushedFilters The pushed down source filters. The filters should refer to | ||
| * the fields of the provided schema. | ||
| * @param schema The required schema of records from datasource files. | ||
| */ | ||
| abstract class StructFilters(pushedFilters: Seq[sources.Filter], schema: StructType) { | ||
|
|
||
| protected val filters = pushedFilters.filter(checkFilterRefs(_, schema.fieldNames.toSet)) | ||
|
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 pre-create the set?
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. Actually we can just call the |
||
|
|
||
| /** | ||
| * Applies pushed down source filters to the given row assuming that | ||
| * value at `index` has been already set. | ||
| * | ||
| * @param row The row with fully or partially set values. | ||
| * @param index The index of already set value. | ||
| * @return true if currently processed row can be skipped otherwise false. | ||
| */ | ||
| def skipRow(row: InternalRow, index: Int): Boolean | ||
|
|
||
| /** | ||
| * Resets states of pushed down filters. The method must be called before | ||
| * precessing any new row otherwise skipRow() may return wrong result. | ||
| */ | ||
| def reset(): Unit | ||
|
|
||
| /** | ||
| * Compiles source filters to a predicate. | ||
| */ | ||
| def toPredicate(filters: Seq[sources.Filter]): BasePredicate = { | ||
| val reducedExpr = filters | ||
| .sortBy(_.references.length) | ||
| .flatMap(filterToExpression(_, toRef)) | ||
| .reduce(And) | ||
| Predicate.create(reducedExpr) | ||
| } | ||
|
|
||
| // Finds a filter attribute in the schema and converts it to a `BoundReference` | ||
| def toRef(attr: String): Option[BoundReference] = { | ||
|
MaxGekk marked this conversation as resolved.
Outdated
|
||
| schema.getFieldIndex(attr).map { index => | ||
|
dongjoon-hyun marked this conversation as resolved.
cloud-fan marked this conversation as resolved.
|
||
| val field = schema(index) | ||
| BoundReference(schema.fieldIndex(attr), field.dataType, field.nullable) | ||
|
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. Isn't
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. Right, I will replace it by |
||
| } | ||
| } | ||
| } | ||
|
|
||
| object StructFilters { | ||
| private def checkFilterRefs(filter: sources.Filter, fieldNames: Set[String]): Boolean = { | ||
| filter.references.forall(fieldNames.contains) | ||
|
MaxGekk marked this conversation as resolved.
|
||
| } | ||
|
|
||
| /** | ||
| * Returns the filters currently supported by the datasource. | ||
| * @param filters The filters pushed down to the datasource. | ||
| * @param schema data schema of datasource files. | ||
| * @return a sub-set of `filters` that can be handled by the datasource. | ||
| */ | ||
| def pushedFilters(filters: Array[sources.Filter], schema: StructType): Array[sources.Filter] = { | ||
| val fieldNames = schema.fieldNames.toSet | ||
| filters.filter(checkFilterRefs(_, fieldNames)) | ||
| } | ||
|
|
||
| private def zip[A, B](a: Option[A], b: Option[B]): Option[(A, B)] = { | ||
|
dongjoon-hyun marked this conversation as resolved.
|
||
| a.zip(b).headOption | ||
| } | ||
|
|
||
| private def toLiteral(value: Any): Option[Literal] = { | ||
| Try(Literal(value)).toOption | ||
| } | ||
|
|
||
| /** | ||
| * Converts a filter to an expression and binds it to row positions. | ||
| * | ||
| * @param filter The filter to convert. | ||
| * @param toRef The function converts a filter attribute to a bound reference. | ||
| * @return some expression with resolved attributes or None if the conversion | ||
| * of the given filter to an expression is impossible. | ||
| */ | ||
| def filterToExpression( | ||
| filter: sources.Filter, | ||
| toRef: String => Option[BoundReference]): Option[Expression] = { | ||
| def zipAttributeAndValue(name: String, value: Any): Option[(BoundReference, Literal)] = { | ||
| zip(toRef(name), toLiteral(value)) | ||
| } | ||
| def translate(filter: sources.Filter): Option[Expression] = filter match { | ||
| case sources.And(left, right) => | ||
| zip(translate(left), translate(right)).map(And.tupled) | ||
| case sources.Or(left, right) => | ||
| zip(translate(left), translate(right)).map(Or.tupled) | ||
| case sources.Not(child) => | ||
| translate(child).map(Not) | ||
| case sources.EqualTo(attribute, value) => | ||
| zipAttributeAndValue(attribute, value).map(EqualTo.tupled) | ||
| case sources.EqualNullSafe(attribute, value) => | ||
| zipAttributeAndValue(attribute, value).map(EqualNullSafe.tupled) | ||
| case sources.IsNull(attribute) => | ||
| toRef(attribute).map(IsNull) | ||
| case sources.IsNotNull(attribute) => | ||
| toRef(attribute).map(IsNotNull) | ||
| case sources.In(attribute, values) => | ||
| val literals = values.toSeq.flatMap(toLiteral) | ||
| if (literals.length == values.length) { | ||
| toRef(attribute).map(In(_, literals)) | ||
| } else { | ||
| None | ||
| } | ||
| case sources.GreaterThan(attribute, value) => | ||
| zipAttributeAndValue(attribute, value).map(GreaterThan.tupled) | ||
| case sources.GreaterThanOrEqual(attribute, value) => | ||
| zipAttributeAndValue(attribute, value).map(GreaterThanOrEqual.tupled) | ||
| case sources.LessThan(attribute, value) => | ||
| zipAttributeAndValue(attribute, value).map(LessThan.tupled) | ||
| case sources.LessThanOrEqual(attribute, value) => | ||
| zipAttributeAndValue(attribute, value).map(LessThanOrEqual.tupled) | ||
| case sources.StringContains(attribute, value) => | ||
| zipAttributeAndValue(attribute, value).map(Contains.tupled) | ||
| case sources.StringStartsWith(attribute, value) => | ||
| zipAttributeAndValue(attribute, value).map(StartsWith.tupled) | ||
| case sources.StringEndsWith(attribute, value) => | ||
| zipAttributeAndValue(attribute, value).map(EndsWith.tupled) | ||
| case sources.AlwaysTrue() => | ||
| Some(Literal(true, BooleanType)) | ||
| case sources.AlwaysFalse() => | ||
| Some(Literal(false, BooleanType)) | ||
| } | ||
| translate(filter) | ||
| } | ||
| } | ||
|
|
||
| class NoopFilters extends StructFilters(Seq.empty, new StructType()) { | ||
| override def skipRow(row: InternalRow, index: Int): Boolean = false | ||
| override def reset(): Unit = {} | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,11 +27,12 @@ import com.fasterxml.jackson.core._ | |
|
|
||
| import org.apache.spark.SparkUpgradeException | ||
| import org.apache.spark.internal.Logging | ||
| import org.apache.spark.sql.catalyst.InternalRow | ||
| import org.apache.spark.sql.catalyst.{InternalRow, NoopFilters, StructFilters} | ||
| import org.apache.spark.sql.catalyst.expressions._ | ||
| import org.apache.spark.sql.catalyst.util._ | ||
| import org.apache.spark.sql.catalyst.util.LegacyDateFormats.FAST_DATE_FORMAT | ||
| import org.apache.spark.sql.internal.SQLConf | ||
| import org.apache.spark.sql.sources.Filter | ||
| import org.apache.spark.sql.types._ | ||
| import org.apache.spark.unsafe.types.{CalendarInterval, UTF8String} | ||
| import org.apache.spark.util.Utils | ||
|
|
@@ -42,7 +43,8 @@ import org.apache.spark.util.Utils | |
| class JacksonParser( | ||
| schema: DataType, | ||
| val options: JSONOptions, | ||
| allowArrayAsStructs: Boolean) extends Logging { | ||
| allowArrayAsStructs: Boolean, | ||
| filters: Seq[Filter] = Seq.empty) extends Logging { | ||
|
|
||
| import JacksonUtils._ | ||
| import com.fasterxml.jackson.core.JsonToken._ | ||
|
|
@@ -85,8 +87,9 @@ class JacksonParser( | |
| private def makeStructRootConverter(st: StructType): JsonParser => Iterable[InternalRow] = { | ||
| val elementConverter = makeConverter(st) | ||
| val fieldConverters = st.map(_.dataType).map(makeConverter).toArray | ||
| val jsonFilters = new JsonFilters(filters, st) | ||
|
MaxGekk marked this conversation as resolved.
|
||
| (parser: JsonParser) => parseJsonToken[Iterable[InternalRow]](parser, st) { | ||
| case START_OBJECT => Some(convertObject(parser, st, fieldConverters)) | ||
| case START_OBJECT => convertObject(parser, st, fieldConverters, jsonFilters) | ||
| // SPARK-3308: support reading top level JSON arrays and take every element | ||
| // in such an array as a row | ||
| // | ||
|
|
@@ -146,7 +149,7 @@ class JacksonParser( | |
| // | ||
| val st = at.elementType.asInstanceOf[StructType] | ||
| val fieldConverters = st.map(_.dataType).map(makeConverter).toArray | ||
| Some(InternalRow(new GenericArrayData(Seq(convertObject(parser, st, fieldConverters))))) | ||
| Some(InternalRow(new GenericArrayData(convertObject(parser, st, fieldConverters).toArray))) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -291,7 +294,7 @@ class JacksonParser( | |
| case st: StructType => | ||
| val fieldConverters = st.map(_.dataType).map(makeConverter).toArray | ||
| (parser: JsonParser) => parseJsonToken[InternalRow](parser, dataType) { | ||
| case START_OBJECT => convertObject(parser, st, fieldConverters) | ||
| case START_OBJECT => convertObject(parser, st, fieldConverters).get | ||
|
dongjoon-hyun marked this conversation as resolved.
|
||
| } | ||
|
|
||
| case at: ArrayType => | ||
|
|
@@ -375,15 +378,19 @@ class JacksonParser( | |
| private def convertObject( | ||
| parser: JsonParser, | ||
| schema: StructType, | ||
| fieldConverters: Array[ValueConverter]): InternalRow = { | ||
| fieldConverters: Array[ValueConverter], | ||
| structFilters: StructFilters = new NoopFilters()): Option[InternalRow] = { | ||
| val row = new GenericInternalRow(schema.length) | ||
| var badRecordException: Option[Throwable] = None | ||
| var skipRow = false | ||
|
|
||
| while (nextUntil(parser, JsonToken.END_OBJECT)) { | ||
| structFilters.reset() | ||
| while (!skipRow && nextUntil(parser, JsonToken.END_OBJECT)) { | ||
| schema.getFieldIndex(parser.getCurrentName) match { | ||
| case Some(index) => | ||
| try { | ||
| row.update(index, fieldConverters(index).apply(parser)) | ||
| skipRow = structFilters.skipRow(row, index) | ||
|
HyukjinKwon marked this conversation as resolved.
|
||
| } catch { | ||
| case e: SparkUpgradeException => throw e | ||
| case NonFatal(e) => | ||
|
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. If any badRecordException is happened,
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 far as I know yes, we do. |
||
|
|
@@ -395,10 +402,14 @@ class JacksonParser( | |
| } | ||
| } | ||
|
|
||
| if (badRecordException.isEmpty) { | ||
| row | ||
| if (skipRow) { | ||
| None | ||
| } else { | ||
| throw PartialResultException(row, badRecordException.get) | ||
| if (badRecordException.isEmpty) { | ||
|
MaxGekk marked this conversation as resolved.
Outdated
|
||
| Some(row) | ||
| } else { | ||
| throw PartialResultException(row, badRecordException.get) | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.