-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Push down partition filter to Spark when Importing File Based Tables #3745
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 6 commits
3ef0170
46602b8
f72721e
79828e2
51ee198
19cb5c7
7d95086
ffc5c6e
3b4ba27
f9b2f40
f8822e4
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 |
|---|---|---|
|
|
@@ -27,6 +27,7 @@ | |
| import java.util.Objects; | ||
| import java.util.Set; | ||
| import java.util.stream.Collectors; | ||
| import org.apache.hadoop.fs.FileStatus; | ||
| import org.apache.hadoop.fs.Path; | ||
| import org.apache.iceberg.MetadataTableType; | ||
| import org.apache.iceberg.MetadataTableUtils; | ||
|
|
@@ -48,6 +49,7 @@ | |
| import org.apache.iceberg.relocated.com.google.common.collect.ImmutableList; | ||
| import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap; | ||
| import org.apache.iceberg.relocated.com.google.common.collect.ImmutableSet; | ||
| import org.apache.iceberg.relocated.com.google.common.collect.Lists; | ||
| import org.apache.iceberg.relocated.com.google.common.collect.Maps; | ||
| import org.apache.iceberg.spark.SparkTableUtil.SparkPartition; | ||
| import org.apache.iceberg.spark.source.SparkTable; | ||
|
|
@@ -57,6 +59,7 @@ | |
| import org.apache.iceberg.types.TypeUtil; | ||
| import org.apache.iceberg.types.Types; | ||
| import org.apache.iceberg.util.Pair; | ||
| import org.apache.spark.sql.AnalysisException; | ||
| import org.apache.spark.sql.Dataset; | ||
| import org.apache.spark.sql.Row; | ||
| import org.apache.spark.sql.SparkSession; | ||
|
|
@@ -77,6 +80,8 @@ | |
| import org.apache.spark.sql.connector.expressions.Transform; | ||
| import org.apache.spark.sql.execution.datasources.FileStatusCache; | ||
| import org.apache.spark.sql.execution.datasources.InMemoryFileIndex; | ||
| import org.apache.spark.sql.execution.datasources.PartitionDirectory; | ||
| import org.apache.spark.sql.execution.datasources.SparkExpressionConverter; | ||
| import org.apache.spark.sql.execution.datasources.v2.DataSourceV2Relation; | ||
| import org.apache.spark.sql.types.IntegerType; | ||
| import org.apache.spark.sql.types.LongType; | ||
|
|
@@ -745,9 +750,11 @@ public static TableIdentifier identifierToTableIdentifier(Identifier identifier) | |
| * @param spark a Spark session | ||
| * @param rootPath a table identifier | ||
| * @param format format of the file | ||
| * @param partitionFilter partitionFilter of the file | ||
| * @return all table's partitions | ||
| */ | ||
| public static List<SparkPartition> getPartitions(SparkSession spark, Path rootPath, String format) { | ||
| public static List<SparkPartition> getPartitions(SparkSession spark, String tableName, Path rootPath, String format, | ||
| Map<String, String> partitionFilter) { | ||
| FileStatusCache fileStatusCache = FileStatusCache.getOrCreate(spark); | ||
| Map<String, String> emptyMap = Collections.emptyMap(); | ||
|
|
||
|
|
@@ -768,9 +775,22 @@ public static List<SparkPartition> getPartitions(SparkSession spark, Path rootPa | |
|
|
||
| org.apache.spark.sql.execution.datasources.PartitionSpec spec = fileIndex.partitionSpec(); | ||
| StructType schema = spec.partitionColumns(); | ||
| if (schema.isEmpty()) { | ||
| return Lists.newArrayList(); | ||
| } | ||
|
|
||
| List<org.apache.spark.sql.catalyst.expressions.Expression> filterExpressions = | ||
| getPartitionFilterExpressions(spark, tableName, partitionFilter); | ||
|
|
||
| List<org.apache.spark.sql.catalyst.expressions.Expression> dataFilters = Lists.newArrayList(); | ||
| Seq<org.apache.spark.sql.catalyst.expressions.Expression> scalaPartitionFilters = | ||
| JavaConverters.asScalaBufferConverter(filterExpressions).asScala().toSeq(); | ||
| Seq<org.apache.spark.sql.catalyst.expressions.Expression> scalaDataFilters = | ||
| JavaConverters.asScalaBufferConverter(dataFilters).asScala().toSeq(); | ||
|
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. Is there an easier way to construct an empty sequence? Also, since this is always empty, can you put the |
||
| Seq<PartitionDirectory> filteredPartitions = fileIndex.listFiles(scalaPartitionFilters, scalaDataFilters); | ||
|
|
||
| return JavaConverters | ||
| .seqAsJavaListConverter(spec.partitions()) | ||
| .seqAsJavaListConverter(filteredPartitions) | ||
| .asJava() | ||
| .stream() | ||
| .map(partition -> { | ||
|
|
@@ -781,10 +801,29 @@ public static List<SparkPartition> getPartitions(SparkSession spark, Path rootPa | |
| Object value = CatalystTypeConverters.convertToScala(catalystValue, field.dataType()); | ||
| values.put(field.name(), String.valueOf(value)); | ||
| }); | ||
| return new SparkPartition(values, partition.path().toString(), format); | ||
| FileStatus fileStatus = | ||
|
rdblue marked this conversation as resolved.
|
||
| scala.collection.JavaConverters.seqAsJavaListConverter(partition.files()).asJava().get(0); | ||
|
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.
|
||
|
|
||
| return new SparkPartition(values, fileStatus.getPath().getParent().toString(), format); | ||
| }).collect(Collectors.toList()); | ||
| } | ||
|
|
||
| private static List<org.apache.spark.sql.catalyst.expressions.Expression> getPartitionFilterExpressions( | ||
|
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. Is it possible to move this to a separate util class to avoid the conflict with the connector |
||
| SparkSession spark, String tableName, Map<String, String> partitionFilter) { | ||
| List<org.apache.spark.sql.catalyst.expressions.Expression> filterExpressions = Lists.newArrayList(); | ||
| for (Map.Entry<String, String> entry : partitionFilter.entrySet()) { | ||
| String filter = entry.getKey() + " = '" + entry.getValue() + "'"; | ||
|
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. why do we have quotes ('') for the value? have we tested both string and non string partition columns ? I was also thinking instead of map, can we expose the
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.
But it will become breaking API changes I guess. Let's see what others think on this also.
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. If the filter is on String column, e.g. For non-String columns, such as
|
||
| try { | ||
| org.apache.spark.sql.catalyst.expressions.Expression expression = | ||
| SparkExpressionConverter.collectResolvedSparkExpression(spark, tableName, filter); | ||
|
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't this produce
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. @rdblue Thank you very much for reviewing my PR on the weekend! Do you mean constructing a filter There are some concerns from the reviewers because we need to test each of the data types. Then I changed the code to call I will address all the other comments after I find out what to do for this one, so I can fix all the problems in one commit.
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. Yes, I think you should construct the filter expression directly rather than calling
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. @rdblue I changed the code to construct filter expression directly. Could you please take one more look? Thank you very much! I have manually checked to make sure the filter expression can be created correctly for all the numeric types, Date and Timestamp. There are tests for String and int partition filters in The build failure doesn't seem to be related to my changes. |
||
| filterExpressions.add(expression); | ||
| } catch (AnalysisException e) { | ||
| throw new IllegalArgumentException("filter " + filter + " cannot be converted to Spark expression"); | ||
|
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. Minor: The exception message should follow the conventions for errors:
This should be "throw new IllegalArgumentException("Cannot convert filter to Spark: " + filter, e)` |
||
| } | ||
| } | ||
| return filterExpressions; | ||
|
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. Nit: missing whitespace between the control flow block and the following statement. |
||
| } | ||
|
|
||
| public static org.apache.spark.sql.catalyst.TableIdentifier toV1TableIdentifier(Identifier identifier) { | ||
| String[] namespace = identifier.namespace(); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -159,7 +159,8 @@ private static void ensureNameMappingPresent(Table table) { | |
| private void importFileTable(Table table, Path tableLocation, String format, Map<String, String> partitionFilter, | ||
| boolean checkDuplicateFiles) { | ||
| // List Partitions via Spark InMemory file search interface | ||
| List<SparkPartition> partitions = Spark3Util.getPartitions(spark(), tableLocation, format); | ||
| List<SparkPartition> partitions = | ||
| Spark3Util.getPartitions(spark(), table.name(), tableLocation, format, partitionFilter); | ||
|
|
||
| if (table.spec().isUnpartitioned()) { | ||
| Preconditions.checkArgument(partitions.isEmpty(), "Cannot add partitioned files to an unpartitioned table"); | ||
|
|
@@ -172,11 +173,7 @@ private void importFileTable(Table table, Path tableLocation, String format, Map | |
| } else { | ||
| Preconditions.checkArgument(!partitions.isEmpty(), | ||
| "Cannot find any partitions in table %s", partitions); | ||
|
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. Nit / corner case: Should we update the precondition message to indicate that it's possible that the filter didn't match any partitions? The current error message now might be kind of confusing to users if the file based table is partitioned. Maybe just
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. Fixed. Thanks! |
||
| List<SparkPartition> filteredPartitions = SparkTableUtil.filterPartitions(partitions, partitionFilter); | ||
| Preconditions.checkArgument(!filteredPartitions.isEmpty(), | ||
| "Cannot find any partitions which match the given filter. Partition filter is %s", | ||
| MAP_JOINER.join(partitionFilter)); | ||
| importPartitions(table, filteredPartitions, checkDuplicateFiles); | ||
| importPartitions(table, partitions, checkDuplicateFiles); | ||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.