Skip to content
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

Data: Add partition stats writer and reader #11216

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

ajantha-bhat
Copy link
Member

@ajantha-bhat ajantha-bhat commented Sep 26, 2024

Introduce APIs to write the partition stats into files in table default format using Iceberg generic writers and readers.

PartitionStatisticsFile partitionStatisticsFile =
        PartitionStatsHandler.computeAndWriteStatsFile(testTable, "b1");

testTable.updatePartitionStatistics().setPartitionStatistics(partitionStatisticsFile).commit();

Schema schema,
PartitionSpec spec,
int formatVersion,
Map<String, String> properties) {
Copy link
Member Author

Choose a reason for hiding this comment

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

There was no option to pass the table properties before.
Needed to pass different file format for paramterized test.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why not adding the parameter to the old create method and call the new method from the old one?

Like:

  public static TestTable create(
      File temp,
      String name,
      Schema schema,
      PartitionSpec spec,
      SortOrder sortOrder,
      int formatVersion) {
    return create(temp, name, schema, spec, SortOrder.unsorted(), formatVersion, ImmutableMap.of());
  }

  public static TestTable create(
      File temp,
      String name,
      Schema schema,
      PartitionSpec spec,
      int formatVersion,
      Map<String, String> properties) {

Copy link
Member Author

Choose a reason for hiding this comment

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

Followed a similar style when they added MetricsReporter,

Why not adding the parameter to the old create

It is public. So, need to modify all the callers.
But we can refactor a private method that can be helper to all these public method. I can do it in a follow up to keep minimal changes for this PR.

@ajantha-bhat ajantha-bhat added this to the Iceberg 1.7.0 milestone Sep 27, 2024
@ajantha-bhat
Copy link
Member Author

@aokolnychyi: This PR is ready. But as we discussed previously, this PR wraps the PartitionStats into a Record as the writers cannot work with Iceberg internal objects yet.

I will explore adding the internal writers for Parquet and Orc. Similar to #11108.
If we fail to have it ready by 1.7.0, I think it makes sense to merge this PR and introduce the optimized writer in the next version by deprecating this writer.

@ajantha-bhat ajantha-bhat marked this pull request as ready for review September 27, 2024 02:00
@ajantha-bhat ajantha-bhat mentioned this pull request Oct 16, 2024
11 tasks
@ajantha-bhat
Copy link
Member Author

@RussellSpitzer: It would be good to have this in 1.7.0.
I am waiting from a month for a review.

@aokolnychyi
Copy link
Contributor

I think we should try to use "internal" writers. @rdblue added "internal" readers recently.

Any guidance on how to add a writer, @rdblue? We can start with Avro for now. We will also need such readers/writers for Parquet.

@ajantha-bhat
Copy link
Member Author

ajantha-bhat commented Oct 24, 2024

@aokolnychyi, @rdblue:

I already tried POC for internal writers on another branch,
c209bc9

The problems:
a) I am using PartitionData instead of Record for partition value, but the PartitionData get() method wraps the byte array to the byte buffer, which is a problem for internal writers, they expect byte[]. So, I didn't felt like using a new class instead of PartitionData just for this.

b) Also, Using partitionData in StructLikeMap is not working fine. Some keys are missing in the map (looks like equals() logic), If I use Record, it is fine.

Maybe in the next version we can have optimized writer and reader (without converter using internal reader and writers).
For end user it doesn't make any difference as new readers can also read the old partition stats parquet file and old readers can read the new partition stats parquet file. So, can we merge this?

@RussellSpitzer
Copy link
Member

Moving out of 1.7.0 since we still have a bit of discussion here

@ajantha-bhat
Copy link
Member Author

ajantha-bhat commented Nov 19, 2024

@RussellSpitzer: I have added the Assertion for Partition type as you suggested and replied to #11216 (comment), do you have anymore comments for this PR?

@aokolnychyi
Copy link
Contributor

I had a conversation with @rdblue today about internal writers. Ryan should have a bit of time to help/guide.
I will check the current implementation today too.

@jbonofre
Copy link
Member

@RussellSpitzer @aokolnychyi I'm reviewing the stale PRs, and this one is open for month. Do we have a way to move forward ? I can do a new review, but at the end of the day, it won't help for the merge (as only committers can merge PR).


@SuppressWarnings("checkstyle:CyclomaticComplexity")
public static boolean isEqual(
Comparator<StructLike> partitionComparator, PartitionStats stats1, PartitionStats stats2) {
Copy link
Member Author

@ajantha-bhat ajantha-bhat Jan 15, 2025

Choose a reason for hiding this comment

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

Cannot have Equals and HashCode for PartitionStats class as StructLike need to have comparator for equals() which forces that class extends StructLike to hold some more things. Setting comparator while serializing and deserializing that class will be a mess.

Hence, added this util method. Currently used only by tests. But can be useful for developers when they integrate partition stats to engines, they can use it for their tests. So, kept as a util.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do I understand correctly that this will never been used in production code, just in tests?
Do we publish a test artifact? If so, we can put this code to the test artifact and users can depend on it.

Copy link
Member Author

Choose a reason for hiding this comment

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

ok. Can move to test. This is not a big code. Users can replicate it in their environment if required. Always safe to have the scope to minimum.

@ajantha-bhat
Copy link
Member Author

@aokolnychyi, @rdblue, @RussellSpitzer: I have reworked on the PR to use Internal writers and readers. PR is much simpler and no need to handle those conversions now. I can rebase it once the Parquet internal writer PR is merged.

@deniskuzZ : Feel free to test the latest state. It doesn't have conversion layer. So, should behave as expected now.

@deniskuzZ
Copy link
Member

deniskuzZ commented Jan 16, 2025

@deniskuzZ : Feel free to test the latest state. It doesn't have conversion layer. So, should behave as expected now.

hi @ajantha-bhat, i need to include #11919, anything else?

@ajantha-bhat
Copy link
Member Author

@aokolnychyi, @rdblue, @RussellSpitzer: I have worked on Internal writers, readers for Avro, parquet and PRs got merged.
I have rebased this PR to use the internal writers and readers.

So, this PR is very simple now (no converter logic) and it just writes stats to a file.

I think if we get a good review support it can be merged for 1.8.0 itself. Please take a look.
It was already reviewed before internal writers. So, I don't think much effort is needed. Thanks in advance.

@deniskuzZ
Copy link
Member

deniskuzZ commented Jan 25, 2025

hi @ajantha-bhat, what is the purpose of PartitionStats.totalRecordCount? it's always 0 and there is no external setter either.
Also SnapshotSummary.TOTAL_FILE_SIZE_PROP tracks all files (data + delete, see https://github.com/apache/iceberg/blob/main/core/src/main/java/org/apache/iceberg/SnapshotSummary.java#L288), whereas PartitionStats only data files totalDataFileSizeInBytes.
Could we extends the PartitionStats with totalFileSizeInBytes metric? I can open a PR with the change if that's ok.

@ajantha-bhat
Copy link
Member Author

@deniskuzZ: While designing the spec (https://iceberg.apache.org/spec/#partition-statistics-file), we have added totalRecordCount to represent the record count after applying the delete file. It is optional field and hence not computed at the moment as it requires scanning all the data files and it can be expensive operation.

Could we extends the PartitionStats with totalFileSizeInBytes metric?

Let us wait for the merge of this PR. After that we can open the discussion to add additional stats for partition stats spec. For example some folks want min max stats also #11083.

@ajantha-bhat
Copy link
Member Author

Ping.

PartitionStatisticsFile partitionStatisticsFile =
PartitionStatsHandler.computeAndWriteStatsFile(testTable, "b1");
// creates an empty stats file since the dummy snapshot exist
assertThat(partitionStatisticsFile.fileSizeInBytes()).isEqualTo(0L);
Copy link
Member

Choose a reason for hiding this comment

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

test would be broken if default format changes, for example with avro format non-zero file would be created

Copy link
Member Author

Choose a reason for hiding this comment

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

But it won't be flaky. So, we can update the test if the behavior changes. This is as per current behavior.

Copy link
Member

Choose a reason for hiding this comment

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

true, I've already added +1

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for your reviews. I hope we ship this feature soon and glad to know Hive, Trino are waiting for this feature.

Copy link
Contributor

@pvary pvary Feb 3, 2025

Choose a reason for hiding this comment

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

I think it is bad practice to check in tests for things which are not a requirement, just "coincidentally" happens.
Could we check that the file could be read and actually empty?

Copy link
Member Author

Choose a reason for hiding this comment

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

Thinking more on it, behavior should be same as empty table testcase above. So, will update to not throw exceptions in this case.

Copy link
Member

@deniskuzZ deniskuzZ left a comment

Choose a reason for hiding this comment

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

LGTM, could we please get this merged? @pvary, would you be able to help

* Computes, writes and reads the {@link PartitionStatisticsFile}. Uses generic readers and writers
* to support writing and reading of the stats in table default format.
*/
public final class PartitionStatsHandler {
Copy link
Contributor

Choose a reason for hiding this comment

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

Any reason for having this final?

Copy link
Member Author

Choose a reason for hiding this comment

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

Utility classes (with private constructor) ideally preferred to be final. I can remove it if not a requirement in this project.

* @return a schema that corresponds to the provided unified partition type.
*/
public static Schema schema(StructType partitionType) {
Preconditions.checkState(!partitionType.fields().isEmpty(), "table must be partitioned");
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Start the error message with capital letter

if (currentSnapshot == null) {
Preconditions.checkArgument(
branch == null, "Couldn't find the snapshot for the branch %s", branch);
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this for handling an empty table?
How users of this method will use the returned null value?

Copy link
Member Author

Choose a reason for hiding this comment

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

Should this be an exception? When we query empty table, it returns zero rows. Similarly, it returns null. I will update the java doc.

try (DataWriter<StructLike> writer = dataWriter(dataSchema, outputFile); ) {
records.forEachRemaining(writer::write);
} catch (IOException e) {
throw new UncheckedIOException(e);
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did we decide to convert an IOException to an unchecked exception?

Table table, long snapshotId, Schema dataSchema, Iterator<PartitionStats> records) {
OutputFile outputFile = newPartitionStatsFile(table, snapshotId);

try (DataWriter<StructLike> writer = dataWriter(dataSchema, outputFile); ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: remove ;

Comment on lines +178 to +180
private static FileFormat fileFormat(String fileLocation) {
return FileFormat.fromString(fileLocation.substring(fileLocation.lastIndexOf(".") + 1));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we sure in this?
We usually depend on metadata files to deduce the file format. Depending on the filename seems brittle to me.

Comment on lines +183 to +185
FileFormat fileFormat =
fileFormat(
table.properties().getOrDefault(DEFAULT_FILE_FORMAT, DEFAULT_FILE_FORMAT_DEFAULT));
Copy link
Contributor

Choose a reason for hiding this comment

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

The fileFormat method parameter is a fileLocation, here we provide the actual FileFormat string... this seems like an issue for me

Copy link
Member Author

Choose a reason for hiding this comment

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

For the reader code, it has to infer from the input file extension. To keep reader and writer signature similar. It has done like this.

return table
.io()
.newOutputFile(
((HasTableOperations) table)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to check that the table implements HasTableOperations?

Comment on lines +214 to +215
case ORC:
// Internal writers are not supported for ORC yet.
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we plan to support ORC Internal writers?
Or do we plan to support partition statistics file for ORC?

Copy link
Member Author

Choose a reason for hiding this comment

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

If people are intersted to contribute.
Last I discussed it with Ryan, Community is expecting the ORC users to contribute here.


@Test
public void testPartitionStatsOnEmptyTable() throws Exception {
Table testTable = TestTables.create(tempDir("empty_table"), "empty_table", SCHEMA, SPEC, 2);
Copy link
Contributor

@pvary pvary Feb 3, 2025

Choose a reason for hiding this comment

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

Are these tables cleaned up after the test methods?
If not, they leave a state for the tests which is a bad practice

Copy link
Member Author

@ajantha-bhat ajantha-bhat Feb 4, 2025

Choose a reason for hiding this comment

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

It is similar to other existing tests. @tempdir annotation should clean up the folders.

* @param partitionType unified partition schema type.
* @return a schema that corresponds to the provided unified partition type.
*/
public static Schema schema(StructType partitionType) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not schema(Table)? In this case we would not need the "Note" and make sure that it is calculated correctly.

Copy link
Member Author

Choose a reason for hiding this comment

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

To avoid computing partition type again again in computeAndWriteStatsFile. Also, It is recommended to pass only what is required for the method instead of the whole table.

@ajantha-bhat
Copy link
Member Author

thanks @pvary for the review.
I have addressed the comments except (#11216 (comment)), It is because I wanted to keep the signatures of reader and writer similar. Let me know if you have any ideas. Thanks.

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.

7 participants