Skip to content

Conversation

@szehon-ho
Copy link
Member

@szehon-ho szehon-ho commented Apr 14, 2022

#4516 fixes the schema of the partitions table in the case of changed partition specs to use Partitioning.partitionType (a union of all previous partition specs), but the data is still wrong in some cases:

This fixes the problem by :

  1. Instantiating the PartitionMap by Partitioning.partitionType, so all types are used in the hashcode generation.
  2. Transforming the PartitionDatas to fit the final schema Partioning.partitionType()

@szehon-ho
Copy link
Member Author

FYI @rdblue @aokolnychyi @RussellSpitzer @szlta if can you help review, thanks

originalPartitionIndex++;
}

PartitionData result = new PartitionData(newSchema);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

result -> normalizedPartition?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, renamed relevant fields in this method.

return partitions.all();
}

private static PartitionData normalizePartition(PartitionData partition, Types.StructType newSchema) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newSchema -> normalizedPartitionSchema?

Maybe that's too long?


PartitionData result = new PartitionData(newSchema);

int finalPartitionIndex = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalizedPartitionIndex (or normalizedIndex)

}

@Test
public void testPartitionMetadataTable() throws ParseException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As one more thing to test, can you check reordering partition transforms?
Going from data, category -> category,data

I think you have this covered but I want to make sure we have a test in there since I think this is a pretty common usecase

Copy link
Member Author

@szehon-ho szehon-ho Apr 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the test. Unfortunately it's a bit weird and does not work in V2 until #4292 is completely fixed (I believe @szlta is taking a look at the remaining point there which would fix this issue).

Copy link
Member

@RussellSpitzer RussellSpitzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One suggested additional test, and I'm not quite sure about the Precondition check but other than that looks good

for (FileScanTask task : tasks) {
partitions.get(task.file().partition()).update(task.file());
PartitionData original = (PartitionData) task.file().partition();
PartitionData normalized = normalizePartition(original, Partitioning.partitionType(table));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we reuse the result of the Partitioning.partitionType(table) invocation at line 99 rather than calling it in every iteration?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea good point

@szlta
Copy link
Contributor

szlta commented Apr 19, 2022

Thanks for catching this @szehon-ho, this change looks good to me, just added a nit comment.

}

private static PartitionData normalizePartition(PartitionData partition, Types.StructType normalizedPartitionSchema) {
Map<Integer, Object> fieldIdToValues = Maps.newHashMap();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing we may want to consider here is caching these normalization by spec rather than recomputing the mapping for every partition value. Thinking about this code being run on millions of files

Copy link
Member Author

@szehon-ho szehon-ho Apr 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still wasn't sure how to cache something per spec, I added a cache per partition for the normalized key, let me know if you have some thought?

By the way, I tried to use the un-normalized partition in the map itself as map key, but doesn't work, as it duplicates if the partition field has name change. So we still need to use normalized partition as map key.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline, added cache of positional mappings to the final partition type, for each spec-id

// Re-added partition fields currently not re-associated: https://github.com/apache/iceberg/issues/4292
// In V1, dropped partition fields show separately when field is re-added
// In V2, re-added field currently conflicts with its deleted form
if (formatVersion == 1) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting this in a new test with the "Assume(formatVersion ==1)" would be a bit cleaner.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, split this into 3 tests

Copy link
Member

@RussellSpitzer RussellSpitzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two suggestions but I think this is good as is,

  1. Spitting up the test so that we have individual tests for the different alterations
  2. Caching the field mapping by partition spec

@szehon-ho szehon-ho force-pushed the partition_key_evolving_spec branch from 8253071 to 3d2860f Compare April 20, 2022 07:10
@szehon-ho
Copy link
Member Author

@RussellSpitzer added additional cache of field positional mapping per spec-id as suggested , it's changed a bit, let me know if you can take another look


LoadingCache<Integer, Integer[]> originalPartitionFieldPositionsBySpec = Caffeine.newBuilder().build(specId ->
originalPositions(table, specId, normalizedPartitionType));
LoadingCache<Pair<PartitionData, Integer>, PartitionData> normalizedPartitions = Caffeine.newBuilder().build(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This I don't think is super important to cache since the getters and setters are pretty fast and you have the integer mapping already. So I probably would drop this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was saving the construction of the new partitionData object, at the cost of memory. but yea it might not be worth it, dropped it.

Types.StructType normalizedPartitionType = Partitioning.partitionType(table);
PartitionMap partitions = new PartitionMap();

LoadingCache<Integer, Integer[]> originalPartitionFieldPositionsBySpec = Caffeine.newBuilder().build(specId ->
Copy link
Member

@RussellSpitzer RussellSpitzer Apr 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the number of partition specs should be very small so I would probably just use a
Maps.newHashMap()

Then .computeIfAbsent

Not that I think there is something wrong with Caffeine, just seems a bit heavy weight to me in this use case

for (FileScanTask task : tasks) {
partitions.get(task.file().partition()).update(task.file());
PartitionData original = (PartitionData) task.file().partition();
int specId = task.spec().specId();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking this would look something more like

positionMapping = originalPositions.computeIfAbsent(specId, specId -> originalPositions())
make normalizedPartitionData
for (originalIndex = 0; originalIndex < original.length; originalIndex ++) {
   normalizedPartitionData.put(positionMapping[originalIndex], original.get(originalIndex))
}

This could be a separate function like we have now but I don't know if it's that bad if we just inline it here

Copy link
Member Author

@szehon-ho szehon-ho Apr 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, yea your suggestion is cleaner.

It's a bit easier to read that logic in a separate function, so kept it outside.

@szehon-ho szehon-ho force-pushed the partition_key_evolving_spec branch from 6d11188 to 5129068 Compare April 22, 2022 06:09
Partition partition = partitions.get(key);
if (partition == null) {
partition = new Partition(key);
partitions.put(StructLikeWrapper.forType(type).set(key), partition);
Copy link
Member

@RussellSpitzer RussellSpitzer Apr 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused by this change, I believe the issue here is that StructType does not have a well defined hashFunction (since implementations can do whatever they like) which is why we use the Wrapper to make sure we have a valid hash. (and equals)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to map of PartitionData (I feel, it should have been that way in the beginning)

sql("CREATE TABLE %s (id bigint NOT NULL, category string, data string) USING iceberg " +
"TBLPROPERTIES ('commit.manifest-merge.enabled' 'false')", tableName);
initTable();

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary white space change here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if we want to make this change we need to do it in all the tests and currently this is missing format changes for testFilesMetadataTable and testWithUnknownTransfer. Probably fine to just keep it as is and match in the new tests

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed whitespace


Table table = validationCatalog.loadTable(tableIdent);

table.updateSpec()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you could just do this in SparkSQL if you like and skip the refresh, but this is fine too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will just keep it for now then, matches existing tests

@szehon-ho szehon-ho force-pushed the partition_key_evolving_spec branch from 2c7d303 to 3cfb65c Compare April 22, 2022 23:29
Copy link
Member

@RussellSpitzer RussellSpitzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks for baring with my many comments. Feel free to merge whenever you feel ready.

@szehon-ho szehon-ho merged commit 4c3aac2 into apache:master Apr 25, 2022
@szehon-ho
Copy link
Member Author

No problem, thanks for all the suggestions !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants