Skip to content

Core: Add v4 TrackedFileAdapters to bridge Data/Delete Files#16100

Open
anoopj wants to merge 7 commits intoapache:mainfrom
anoopj:v4-tracked-file-struct-adapters
Open

Core: Add v4 TrackedFileAdapters to bridge Data/Delete Files#16100
anoopj wants to merge 7 commits intoapache:mainfrom
anoopj:v4-tracked-file-struct-adapters

Conversation

@anoopj
Copy link
Copy Markdown
Contributor

@anoopj anoopj commented Apr 24, 2026

The adapter bridges TrackedFile to existing DataFile/DeleteFile APIs and would allow to minimize the v4 related code changes during scan planning and commits.

…leteFile APIs

This adapter would allow to minimize the v4 related code changes during
scan planning and commits.
@github-actions github-actions Bot added the core label Apr 24, 2026
@anoopj anoopj moved this to In review in V4: metadata tree Apr 24, 2026
return new TrackedDeleteFile(file, spec);
}

// TODO: TrackedFile will likely get an explicit partition tuple field (using a union partition
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This will change after the approach to store partition tuple is settled.

@anoopj anoopj changed the title [core] v4: Add TrackedFileAdapters to bridge Data/Delete Files Core: Add v4 TrackedFileAdapters to bridge Data/Delete Files Apr 24, 2026
return result.isEmpty() ? null : result;
}

static Map<Integer, Long> nullValueCounts(ContentStats stats) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

An open question is whether it's worth caching the stats (lazy/eager). I don't see a lot of repeated reads, so may not be worth it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's probably fine. I have a comment about this below.

Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java
@anoopj anoopj requested a review from stevenzwu April 30, 2026 15:10

private TrackedFileAdapters() {}

static DataFile asDataFile(TrackedFile file, PartitionSpec spec) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What was your reason not to make this a method of TrackedFile?

Also, PartitionSpec is tracked by the file itself, so is very strange to pass it in here. At a minimum, I would expect this to have a validation that the spec's ID matches the file's spec ID. But it would also be better to have some way to look up spec by ID instead of forcing the caller to do it and then validate that the caller did it correctly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I intentionally kept the adapters outside of TrackedFile to avoid coupling it with Data/DeleteFile. It seemed like an adapter concern.

The tracked file keeps track of the spec ID, but not the spec itself. I saw some v3 code paths that followed a similar pattern of passing around specsById. Pretty much open to changing it if you have suggestions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I changed the methods to take in specsById map instead.

Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java Outdated

@Override
public Integer sortOrderId() {
return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good context from the spec for a comment:

Position deletes are required to be sorted by file and position, not a table order, and should set sort order id to null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Maybe note that this is from the spec.

// each reported the full file size).
@Override
public long fileSizeInBytes() {
return dv.sizeInBytes();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I appreciate the comment. The decision here seems reasonable to me since we don't know the total Puffin file size.

We should also consider whether we want to have a field in the tracked_file struct for this. We originally wanted to use file_size_in_bytes for the Puffin file size so that we could determine when DVs should be compacted. But variance in the footer was a problem that prevented it from being used as intended.

@aokolnychyi should we revisit this?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

even if we know the total Puffin file size, dv.sizeInBytes still seems the correct value here. A puffin file may contain thousands of DVs. A logical DeleteFile should only contain one DV. The DeleteFile size should be the DV size.


@Override
public Long firstRowId() {
return tracking != null ? tracking.firstRowId() : null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From the spec:

The value of first_row_id for delete manifests is always null.

The value of first_row_id for delete files is always null.

This should just be null.

I also wonder if there are some implementations that can be refactored out, to avoid duplication between this and the other adapters? It seems like many of these should be the same as for data file and for v2 delete files that haven't been compacted into DVs yet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This should just be null.

Great callout. Fixed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll add an abstract based class to reduce the duplication.

Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java
Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java Outdated
Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java Outdated

@Override
public Map<Integer, ByteBuffer> lowerBounds() {
return TrackedFileAdapters.lowerBounds(file.contentStats());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is okay, but a little concerning because the helper method here creates a new map every time it is called. Creating the map is an expensive operation because it allocates buffers to hold each column bound and serializes into that buffer. Then the map is thrown away rather than reused.

The evaluators themselves (for example, InclusiveMetricsEvaluator) only call these methods once to evaluate for the data file, but if multiple evaluators are used then we should expect a performance degredation.

On the other hand, we want to have evaluators that work directly with ContentStats instead of going through these methods. I think I'm fine with leaving this as-is for now, but we need to make sure that we use the right evaluators everywhere.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agree. Will build a new evaluator that operates on content stats, as a followup. cc @nastra

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Filed #16218 to track this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's hold off on doing it right now. I think the ContentStats API is going to change.

Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java Outdated
return new TrackedDataFile(file, spec);
}

static DeleteFile asDVDeleteFile(TrackedFile file, PartitionSpec spec) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We will also need a way to wrap TrackedFile as a v2 DeleteFile for position deletes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ack. I will create a followup PR for this so that the size is manageable.

@anoopj anoopj requested a review from rdblue May 5, 2026 18:23
private final Tracking tracking;
private final PartitionSpec spec;

private AbstractTrackedContentFile(TrackedFile file, PartitionSpec spec) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rather than Abstract, we typically use Base for the prefix. For example, BaseAction or BaseContentStats.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can even drop the Base here with just TrackedContentFile, because ContentFile is a base class. This is also consistent with other classes like TrackedDataFile, TrackedDVDeleteFile.

Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java

@Override
public int specId() {
return spec.specId();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If the spec ID is set on file then it should be returned.

The last version was this:

    return file.specId() != null ? file.specId() : 0;

The problem wasn't that it was returning file.specId(). When that spec ID is set, it is canonical and is the ID that was used to look up spec. The problem was that it was guessing ID 0 for the unpartitioned spec, which is not correct. The updated version should be this:

    return file.specId() != null ? file.specId() : spec.specId();

Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java
* <p>Subclasses provide {@code content()}, {@code firstRowId()}, {@code equalityFieldIds()}, and
* the copy methods.
*/
private abstract static class AbstractTrackedContentFile<F extends ContentFile<F>>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It would be helpful to implement the version for v2 position deletes here as well. That would make it easier to evaluate the implementations here, although I suspect it will be fine.


@Override
public Long pos() {
return tracking != null ? tracking.manifestPos() : null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I wonder if the methods that delegate to Tracking could be shared through another base class.


@Override
public int specId() {
return spec.specId();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Needs to delegate to file.specId() as well.

private TrackedFileAdapters() {}

static DataFile asDataFile(TrackedFile file, Map<Integer, PartitionSpec> specsById) {
Preconditions.checkState(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: checkState is for invariants on internal state; checkArgument is the right call here since file.contentType() is essentially validating the input. Same applies to asDVDeleteFile (the two checkState calls on L54/L58) and asEqualityDeleteFile (L64).


@Override
public DeleteFile copy() {
return new TrackedDVDeleteFile(file.copy(), spec);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The DV adapter never exposes the underlying TrackedFile's content stats — valueCounts() / lowerBounds() / etc. all return null directly. So file.copy() here retains content stats that will never be read through this adapter.

Using file.copyWithoutStats() would let copy callers drop them at the source and avoid retaining the dead weight when many DVs are held in memory. Same applies to the other copy variants below.

tracking.set(3, 11L);
tracking.set(5, 1000L);
tracking.setManifestLocation("s3://bucket/manifest.avro");
tracking.set(8, 7L);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

switch to the builder added recently?

private final Tracking tracking;
private final PartitionSpec spec;

private AbstractTrackedContentFile(TrackedFile file, PartitionSpec spec) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can even drop the Base here with just TrackedContentFile, because ContentFile is a base class. This is also consistent with other classes like TrackedDataFile, TrackedDVDeleteFile.

}
}

/** Adapts a TrackedFile EQUALITY_DELETES entry to the {@link DeleteFile} interface. */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this class only for EQUALITY_DELETES, as the content() method override indicates? if yes, it can probably names as TrackedEqualityDeleteFile

Just to confirm my understanding. This is for new V4 manifest files, where v2 position delete file entries won't exist.

// each reported the full file size).
@Override
public long fileSizeInBytes() {
return dv.sizeInBytes();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

even if we know the total Puffin file size, dv.sizeInBytes still seems the correct value here. A puffin file may contain thousands of DVs. A logical DeleteFile should only contain one DV. The DeleteFile size should be the DV size.

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

Labels

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

3 participants