Skip to content

Core: Project tracked-file partitions onto the resolved spec - #18108

Open
anoopj wants to merge 12 commits into
apache:mainfrom
anoopj:v4-adapter-partition-projection
Open

anoopj wants to merge 12 commits into
apache:mainfrom
anoopj:v4-adapter-partition-projection

Conversation

@anoopj

@anoopj anoopj commented Sep 14, 2026

Copy link
Copy Markdown
Member

TrackedFileAdapters exposed the manifest's union-type partition tuple unchanged while reporting the file's own spec ID. Residual evaluation and partition-constant injection read the tuple by the spec's field ordinals, so on partition-evolved tables a later spec's field (at a different position in the union than in its own spec) was read from the wrong slot, silently dropping rows or injecting null partition constants.

Project each file's partition onto its resolved spec's partition type before exposing it, so partition() is always in the file's own spec order.

TrackedFileAdapters exposed the manifest's union-type partition tuple
unchanged while reporting the file's own spec ID. Residual evaluation and
partition-constant injection read the tuple by the spec's field ordinals, so
on partition-evolved tables a later spec's field (at a different position in
the union than in its own spec) was read from the wrong slot, silently
dropping rows or injecting null partition constants.

Project each file's partition onto its resolved spec's partition type before
exposing it, so partition() is always in the file's own spec order.
@github-actions github-actions Bot added the core label Sep 14, 2026
Comment thread core/src/main/java/org/apache/iceberg/TrackedFileAdapters.java Outdated
Per review, the union->spec partition projection is TrackedFile's
responsibility, not the adapter's or the reader's. TrackedFileStruct.partition()
now projects the stored union tuple onto the file's own spec (lazily; copies
materialize it into a spec-ordered PartitionData), and V4ManifestReader
evaluates partition filters against partition() directly instead of projecting
itself. Reverts the adapter-level projection.
… is read,

  not only on the filter path, so partition() stays spec-projected for
  select()/project() callers (not just filtered scans).
- Cache the per-spec project-or-passthrough decision in TrackedFileStruct so the
  filter hot path is a map lookup instead of a per-row StructType comparison.
- Add a round-trip test that a materialized spec-ordered partition survives Java
  and Kryo serialization.
Cover the no-filter path where the partition is projected: the reader must
still project spec_id so partition() returns the file's own spec order, and a
narrow nested partition selection must still read the whole union tuple so the
projection onto the file's spec does not fail on an omitted field.
// tuple (including data) so partition() can project it onto the file's data spec
try (V4ManifestReader reader =
V4ManifestReader.builder(manifest, io, specsById, TABLE_LOCATION)
.select("partition.id")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

selecting id is intentional, btw. hopefully the comment on line 708 is clear.

@yangshangqing95 yangshangqing95 left a comment

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.

Overall LGTM, just a few non-blocking questions.

}

@Override
public StructLike partition() {

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, non-blocking: could we make the TrackedFile.partition() contract explicit that the returned struct is shaped/ordered according to the file's resolved partition spec? This PR makes that invariant important for adapters and downstream evaluators, while the current method Javadoc only says it returns the partition.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I can clarify this, but to me it was a basic expectation from this field.

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 agree that we should update the Javadoc to reflect this expectation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Updated the javadocs

assertThat(actual.partition().get(0, CharSequence.class)).hasToString("x");
}
}

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, non-blocking: would it be worth adding a regression test that goes through TrackedFileAdapters as well?

The current tests cover the projection behavior in TrackedFileStruct and V4ManifestReader well, but the original failure shows up downstream when the partition tuple is interpreted using the file's spec, e.g. during residual evaluation or partition-constant injection.

A test along the lines of V4ManifestReader -> TrackedFileAdapters -> partition/residual evaluation would help lock in the user-visible behavior and guard against the adapter boundary regressing in the future.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We do have coverage in the scan planner here: #17541 You can see that there is a test failure (which will be red until this PR is merged)

return fileSizeInBytes;
}

void setSpecsById(Map<Integer, PartitionSpec> newSpecsById) {

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'm not a fan of needing to set specs on the class like this. I think maybe it would be cleaner to just do the work to remove partition().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agree. Leaving it as is for now, and we can remove it as part of the change to recover partition tuple from stats.

return projection != null ? projection.wrap(partitionData) : partitionData;
}

private StructProjection partitionProjection(int id) {

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: which ID? Generally better to be specific and use specId.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Renamed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Actually there is a Checkstyle's HiddenField rule: the param specId collides with the specId field. I renamed it to partitionSpecId to avoid collision.

id,
specType == null || partitionData.getPartitionType().equals(specType)
? null
: StructProjection.create(partitionData.getPartitionType(), specType));

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 put is too complicated and it forces the caller to handle null. The projection should be created and stored in a variable so put can be separate from the control flow for creating the projection.

I'd rather use a Function<PartitionData, StructLike> to project. That way this can return projection::wrap or Functions.identity and the caller is simpler.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That looks a lot nicer. Making that change.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

FYI, I did this change, but this implementation had a thread safety issue, so I changed this.

return partitionProjections.get(id);
}

private PartitionData materializedPartition() {

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 this class should use StructUtil.copy(partition()). There's no need to ensure that the result is a PartitionData. Won't any StructLike work?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, I didn't know about StructlikeUtil.copy(). Thanks.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Actually that didn't work because StructLikeUtil.copy(...) returns a StructCopy which isn't Serializable. So it has to stay a serializable PartitionData and we need to keep PartitionData.copy()/copyFor(...).

Or should we make StructLikeUtil.copy serializable? (felt out of scope for this PR)

Reconcile the partition projection with main's V4ManifestReader rework
(apache#18109 content stats, apache#18147 stats filtering, apache#18138 ManifestBitmap MDV)
@anoopj
anoopj requested a review from rdblue September 17, 2026 22:59
Comment thread core/src/main/java/org/apache/iceberg/TrackedFileStruct.java

This branch has not been deployed

No deployments
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