Skip to content

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

Open
anoopj wants to merge 16 commits into
apache:mainfrom
anoopj:v4-tracked-file-struct-adapters
Open

Core: Add v4 TrackedFileAdapters to bridge Data/Delete Files#16100
anoopj wants to merge 16 commits into
apache: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.

Fixes #16221

@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.

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.

Steven and Russell replied on the dev list that they're good with a tracked partition tuple. I also confirmed with Dan and Amogh, so I think we are probably good to go. Maybe we should start getting the changes to the interfaces and implementations done? This would then only need to run the projection.

@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
Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java
// 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.

Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java Outdated
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

@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.

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.

Added TrackedPositionDeleteFile

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.

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 dropped the Base. @rdblue let me know if thats OK.

Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java Outdated
Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java Outdated
Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java Outdated
* <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.

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.

Added it. It's actually fairly light.

Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java Outdated
Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java Outdated
Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java Outdated
Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java Outdated
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?

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.

+1 as I feel setting all stuff by position is quite brittle

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.

Done.

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.

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.

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

Correct. Renamed.

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

v2 position delete file entries can also exist, if a table is upgraded from v2. I just added TrackedPositionDeleteFile to handle it. PTAL

// 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.

Comment thread core/src/test/java/org/apache/iceberg/TestTrackedFileAdapters.java Outdated
Comment thread core/src/test/java/org/apache/iceberg/TestTrackedFileAdapters.java Outdated
Comment thread core/src/test/java/org/apache/iceberg/TestTrackedFileAdapters.java Outdated
Comment thread core/src/test/java/org/apache/iceberg/TestTrackedFileAdapters.java Outdated
Comment thread core/src/test/java/org/apache/iceberg/TestTrackedFileAdapters.java Outdated
Comment thread core/src/test/java/org/apache/iceberg/TestTrackedFileAdapters.java
Comment thread core/src/test/java/org/apache/iceberg/TestTrackedFileAdapters.java Outdated
Comment thread core/src/test/java/org/apache/iceberg/TestTrackedFileAdapters.java Outdated
@anoopj anoopj requested a review from rdblue May 11, 2026 23:54
@anoopj anoopj force-pushed the v4-tracked-file-struct-adapters branch from e74049c to 832deb0 Compare May 11, 2026 23:58
Copy link
Copy Markdown
Contributor

@gaborkaszab gaborkaszab left a comment

Choose a reason for hiding this comment

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

I went through this for my own education. Left some comments, let me know if they make sense.


private static PartitionSpec resolveSpec(
TrackedFile file, Map<Integer, PartitionSpec> specsById) {
Integer specId = file.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.

nit: The only thing needed from TrackedFile here is the specId. Can't we pass that instead, and then there is nothing V4 specific in this function (like TrackedFile, etc.) so it might go into some PartitionSpec related util class.

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.

That is true, but I think this signature makes the call sites a bit more readable.

return file;
}

protected Tracking tracking() {
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: Isn't this redundant? Tracking is part of TrackedFile

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.

Changed to just delegate to file.tracking()


/**
* Shared base for all tracked file adapters. Holds the common fields and implements the methods
* that delegate to {@link Tracking} and {@link PartitionSpec}.
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.

Doesn't this comment mean that practically we shouldn't have a TrackedFile member on this level of abstraction?

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.

The comment was a bit stale. Fixed. Thanks for the callout!

}

@Override
public List<Integer> equalityFieldIds() {
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: the default implementation already returns null. Isn't this redundant here? Same for content() and some other functions elsewhere like firstRowId()

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.

Removed the ones that were returning null. Kept content() override to be explicit.


private static TrackingStruct createTracking(long manifestPos) {
// Uses the struct-type constructor because the builder creates with BASE_TYPE which does not
// include ROW_POSITION, so manifest position cannot be set on builder-created structs.
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 justification doesn't hold. In TrackingStruct.java, BASE_TYPE (L33-43) already includes MetadataColumns.ROW_POSITION as its 9th field, and the builder's build() calls a constructor that does super(BASE_TYPE, BASE_TYPE) (L91), so builder-created structs do support row position projection.

manifestLocation and manifestPos aren't part of the struct projection at all - they're plain instance fields (L55-56), "set by manifest readers, not written to manifests". They're assigned via setManifestLocation(...) and set(8, ...) regardless of which constructor was used.

So the helper can just be:

TrackingStruct tracking = TrackingStruct.builder()
    .status(EntryStatus.ADDED)
    .snapshotId(42L)
    .dataSequenceNumber(10L)
    .fileSequenceNumber(11L)
    .firstRowId(1000L)
    .build();
tracking.setManifestLocation("s3://bucket/manifest.avro");
tracking.set(8, manifestPos);
return tracking;

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 had fixed exactly this last week. Sounds like this was a stale comment?

}
}

/** Adapts a TrackedFile POSITION_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.

TrackedPositionDeleteFile doesn't override referencedDataFile(), contentOffset(), or contentSizeInBytes(), so all three fall through to the DeleteFile interface defaults of null. But V4Metadata.java (cases 17/18/19, L495-509) writes those slots out for POSITION_DELETES entries, so V4 manifests can carry V2-style position delete metadata — a V2 delete file with referenced_data_file and content offset/size.

The read-side gap: TrackedFile (interface) doesn't expose referencedDataFile/contentOffset/contentSizeInBytes, so this adapter has no way to surface them even if they were in the manifest. So either:

  1. V4 manifests aren't expected to round-trip V2 position delete metadata (worth a code comment here saying these are intentionally always null), or
  2. TrackedFile is missing fields for position-delete-specific metadata, and this adapter silently drops them when reading V2 position deletes from a V4 manifest.

The test testPositionDeleteFileAdapterDelegatesAllFields doesn't exercise these three fields, so the gap isn't visible there.

Side note while we're here: TrackedFile.java:37 documents content_type as "0=DATA, 2=EQUALITY_DELETES, 3=DATA_MANIFEST, 4=DELETE_MANIFEST"1=POSITION_DELETES is omitted, but this PR's asPositionDeleteFile implies it's valid. Worth updating that doc to match (separate file, but the inconsistency is exposed by this PR).

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 don't believe there is a read-side gap here for v2 positional deletes. These three fields (referencedDataFile, contentOffset, contentSizeInBytes) are v3 DV fields right? TrackedPositionDeleteFile doesn't need to surface them because it is for an actual v2-style positional delete. (which can come from a v2 table upgrade).

V3 DVs can't come through this adapter. V3 DVs live in v3 delete manifests and come out as DeleteFile objects directly from the v3 manifest reader, without any adapter. I was just having an offline conversation with @rdblue about this.

In v4, DVs are always colocated with their data file entries as DeletionVector structs, surfaced through TrackedDVDeleteFile.

Did that make sense?

Good catch on TrackedFile. Fixed.

@anoopj anoopj force-pushed the v4-tracked-file-struct-adapters branch from fc0f4be to 35fbdd8 Compare May 12, 2026 22:54
@anoopj anoopj requested a review from stevenzwu May 12, 2026 23:08
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.

Add TrackedFile adapter for v2 position delete files

5 participants