-
Notifications
You must be signed in to change notification settings - Fork 3k
Core: Fix Partitions table filtering for evolved partition specs #4637
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 all commits
94bff1d
2e24cbc
0b926e9
87f50c6
6ff381a
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 |
|---|---|---|
|
|
@@ -550,6 +550,126 @@ public void testDeleteFilesTableSelection() throws IOException { | |
| Assert.assertEquals(expected, scan.schema().asStruct()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testPartitionSpecEvolutionAdditive() { | ||
| preparePartitionedTable(); | ||
|
|
||
| // Change spec and add two data files | ||
| table.updateSpec() | ||
| .addField("id") | ||
| .commit(); | ||
| PartitionSpec newSpec = table.spec(); | ||
|
|
||
| // Add two data files with new spec | ||
| PartitionKey data10Key = new PartitionKey(newSpec, table.schema()); | ||
| data10Key.set(0, 0); // data=0 | ||
| data10Key.set(1, 10); // id=10 | ||
| DataFile data10 = DataFiles.builder(newSpec) | ||
| .withPath("/path/to/data-10.parquet") | ||
| .withRecordCount(10) | ||
| .withFileSizeInBytes(10) | ||
| .withPartition(data10Key) | ||
| .build(); | ||
| PartitionKey data11Key = new PartitionKey(newSpec, table.schema()); | ||
| data11Key.set(0, 1); // data=0 | ||
| data10Key.set(1, 11); // id=11 | ||
| DataFile data11 = DataFiles.builder(newSpec) | ||
| .withPath("/path/to/data-11.parquet") | ||
| .withRecordCount(10) | ||
| .withFileSizeInBytes(10) | ||
| .withPartition(data11Key) | ||
| .build(); | ||
|
|
||
| table.newFastAppend().appendFile(data10).commit(); | ||
| table.newFastAppend().appendFile(data11).commit(); | ||
|
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. Do you need two commits here?
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. Yea , it messes up the test a little bit as it combines into one manifest (as the test is a bit low level and depends on how many manifestReadTask get spawned) |
||
|
|
||
| Table metadataTable = new PartitionsTable(table.ops(), table); | ||
| Expression filter = Expressions.and( | ||
| Expressions.equal("partition.id", 10), | ||
| Expressions.greaterThan("record_count", 0)); | ||
| TableScan scan = metadataTable.newScan().filter(filter); | ||
| CloseableIterable<FileScanTask> tasks = PartitionsTable.planFiles((StaticTableScan) scan); | ||
|
|
||
| // Four data files of old spec, one new data file of new spec | ||
| Assert.assertEquals(5, Iterables.size(tasks)); | ||
|
|
||
| filter = Expressions.and( | ||
| Expressions.equal("partition.data_bucket", 0), | ||
| Expressions.greaterThan("record_count", 0)); | ||
| scan = metadataTable.newScan().filter(filter); | ||
| tasks = PartitionsTable.planFiles((StaticTableScan) scan); | ||
|
|
||
| // 1 original data file written by old spec, plus 1 new data file written by new spec | ||
| Assert.assertEquals(2, Iterables.size(tasks)); | ||
| } | ||
|
|
||
| @Test | ||
| public void testPartitionSpecEvolutionRemoval() { | ||
| preparePartitionedTable(); | ||
|
|
||
| // Remove partition field | ||
| table.updateSpec() | ||
| .removeField(Expressions.bucket("data", 16)) | ||
| .addField("id") | ||
| .commit(); | ||
| PartitionSpec newSpec = table.spec(); | ||
|
|
||
| // Add two data files with new spec | ||
| // Partition Fields are replaced in V1 with void and actually removed in V2 | ||
| int partIndex = (formatVersion == 1) ? 1 : 0; | ||
| PartitionKey data10Key = new PartitionKey(newSpec, table.schema()); | ||
| data10Key.set(partIndex, 10); | ||
| DataFile data10 = DataFiles.builder(newSpec) | ||
| .withPath("/path/to/data-10.parquet") | ||
| .withRecordCount(10) | ||
| .withFileSizeInBytes(10) | ||
| .withPartition(data10Key) | ||
| .build(); | ||
| PartitionKey data11Key = new PartitionKey(newSpec, table.schema()); | ||
| data11Key.set(partIndex, 11); | ||
| DataFile data11 = DataFiles.builder(newSpec) | ||
| .withPath("/path/to/data-11.parquet") | ||
| .withRecordCount(10) | ||
| .withFileSizeInBytes(10) | ||
| .withPartition(data11Key) | ||
| .build(); | ||
|
|
||
| table.newFastAppend().appendFile(data10).commit(); | ||
| table.newFastAppend().appendFile(data11).commit(); | ||
|
|
||
| Table metadataTable = new PartitionsTable(table.ops(), table); | ||
| Expression filter = Expressions.and( | ||
| Expressions.equal("partition.id", 10), | ||
| Expressions.greaterThan("record_count", 0)); | ||
| TableScan scan = metadataTable.newScan().filter(filter); | ||
| CloseableIterable<FileScanTask> tasks = PartitionsTable.planFiles((StaticTableScan) scan); | ||
|
|
||
| // Four original files of original spec, one data file written by new spec | ||
| Assert.assertEquals(5, Iterables.size(tasks)); | ||
|
|
||
| // Filter for a dropped partition spec field. Correct behavior is that only old partitions are returned. | ||
| filter = Expressions.and( | ||
| Expressions.equal("partition.data_bucket", 0), | ||
| Expressions.greaterThan("record_count", 0)); | ||
| scan = metadataTable.newScan().filter(filter); | ||
| tasks = PartitionsTable.planFiles((StaticTableScan) scan); | ||
|
|
||
| if (formatVersion == 1) { | ||
| // 1 original data file written by old spec | ||
| Assert.assertEquals(1, Iterables.size(tasks)); | ||
| } else { | ||
| // 1 original data/delete files written by old spec, plus both of new data file/delete file written by new spec | ||
| // | ||
| // Unlike in V1, V2 does not write (data=null) on newer files' partition data, so these cannot be filtered out | ||
| // early in scan planning here. | ||
| // | ||
| // However, these partition rows are filtered out later in Spark data filtering, as the newer partitions | ||
| // will have 'data=null' field added as part of normalization to the Partitions table final schema. | ||
| // The Partitions table final schema is a union of fields of all specs, including dropped fields. | ||
| Assert.assertEquals(3, Iterables.size(tasks)); | ||
|
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 not sure I understand this, I would have thought the filter would be used with the correct column and give us the same result as the v1 table?
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 tried to clarify the comment, can you see if it makes sense? It's a bit confusing, but the background is, this occurs when trying to query with filter on a dropped partition field (data). The correct behavior, because this is a partition table, is that only partitions of old spec are returned and partitions of new spec without data should not be returned. In V1, new files are written with void transform for the dropped field (data=null), so the predicate pushdown can filter them out early. In V2 new files do not write any values for data, so predicate pushdown cannot filter them out early. However, they are filtered out later by Spark data filtering, because the partition values are normalized to the Partioning,partitionType (union of all specs), and old field "data" is filled in as 'null' when returning to Spark. (That was done in #4560). This is shown in the new test added added in TestMetadataTablesWithPartitionEvolution
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 was just wondering if we need some kind of special filter, if you have a predicate on a column not present in the spec just return cannot match
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. Yea i was thinking about it, but as we rely on existing ManifestEvaluator , it seems a bit heavy to implement a ManifestEvaluator only for this case (improving the perf for querying dropped partition fields in a metadata table), and also its a bit risky ( if we cannot definitively say a partition value matches or not, I feel safer not filtering), as there's bugs in the past : #4520 |
||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void testPartitionColumnNamedPartition() throws Exception { | ||
| TestTables.clearTables(); | ||
|
|
||
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.
Not sure we need the full LoadingCache here, but I'm ok with it if you like, we could probably just proactively build the full set of evaluators for all specs in the metadata. I probably should have suggested this before on the other PR as well. Since we know we will need every single evaluator
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.
Discussed, at this point we keep all partition specs so this will save a cycle if we have some spec without any manifests.