Uh oh!
There was an error while loading. Please reload this page.
API, Core: Add ManifestFile adapter for v4 tracked files - #17932
Conversation
Reintroduces the ManifestFile adapter closed in apache#16867, which stalled because ManifestFile had no way to represent a manifest deletion vector. The V4 scan task planner (apache#17541) plans over manifests through the ManifestFile API, so this adds ManifestFile.deletionVector() and has the adapter expose the tracked file's manifest DV.
| Preconditions.checkArgument( | ||
| tracking.dataSequenceNumber() != null, "Invalid data sequence number: null"); | ||
| Preconditions.checkArgument( |
There was a problem hiding this comment.
Note to reviewers: It is unclear whether we need to do these validations during read time here. Also, we don't validate snapshot IDs currently. I'm OK to just remove it.
There was a problem hiding this comment.
This has been unclear for me too. I'm not sure how we guard against data corruption on the read side and how we guarantee our internal invariants like this. On the read side we apparently assume that these invariants are satisfied, however, I'm not sure this is safe against some malformed data files.
Anyway, even if we want to check against these constraints, I think we should introduce those checks on the reader, not on the adapter. Reaching this point, I guess we should just assume that TrackedFile is correct and invariants are satisfied.
So probably we are fine not checking them here.
Uh oh!
There was an error while loading. Please reload this page.
| } | ||
| /** Returns the manifest deletion vector, or null if absent. */ | ||
| default ByteBuffer deletionVector() { |
There was a problem hiding this comment.
Another design decision is whether we just have a ByteBuffer or whether we introduce a class for MDVs.
There was a problem hiding this comment.
This is fine for now. We have other places that will need to be updated as well.
gaborkaszab
left a comment
There was a problem hiding this comment.
Thanks for the PR, @anoop!
| long length(); | ||
| /** Returns iD of the {@link PartitionSpec} used to write the manifest file. */ | ||
| /** Returns ID of the {@link PartitionSpec} used to write the manifest file. */ |
There was a problem hiding this comment.
Not sure about the general opinion on this, but I was asked on code reviews multiple occasions to remove all the tiny nitpicking that are unrelated to the PR itself. I know it seems an overkill to open a separate PR to these, but following that logic, this and the same below should be removed from this PR.
There was a problem hiding this comment.
Seemed a bit of al overkill. :) I will remove it if there is objection from another reviewer
| private final TrackedFile file; | ||
| private TrackedManifestFile(TrackedFile file) { | ||
| Tracking tracking = file.tracking(); |
There was a problem hiding this comment.
I see that in TrackedFileAdapter every time we reference a member in Tracking, we branch based on whether Tracking is null or not. Maybe to cover if it's not projected by the read?
Shouldn't we perform that branching in this class too?
| @Override | ||
| public long minSequenceNumber() { | ||
| return file.manifestInfo().minSequenceNumber(); |
There was a problem hiding this comment.
Same question as for Tracking: Are we sure ManifestInfo is not null (part of the projection)?
There was a problem hiding this comment.
It should be part of the projection and a reader/caller responsibility. I don't think we should be doing null guards here.
There was a problem hiding this comment.
I agree that we don't want to add a check to account for the case where manifest info wasn't projected.
However, we do need to account for the case where the manifest has not been written into a root manifest and does not yet have Tracking metadata. I think tracking should be: file.tracking() != null ? file.tracking().dataSequenceNumber() : null
There was a problem hiding this comment.
file.tracking() != null ? file.tracking().dataSequenceNumber() : null
This works for boxed accessors, but won't compile because we need to return a primitive long. ManifestFile.sequenceNumber() and minSequenceNumber() both return long. So should we just return a sentinel value? Perhaps -1?
public abstract class ManifestWriter<F extends ContentFile> implements FileAppender {
// stand-in for the current sequence number that will be assigned when the commit is successful
// this is replaced when writing a manifest list by the ManifestFile wrapper
static final long UNASSIGNED_SEQ = -1L;
Also I assume we need to handle this in sequenceNumber() also.
There was a problem hiding this comment.
That constant is always replaced and we should not return it through an API method.
We should just return the value and accept the NPE if it is null. If we can't express that by checking tracking, then it's an NPE either way and we should just return the expression without a null check.
There was a problem hiding this comment.
Sounds good. So not making any changes here.
| private static final String MANIFEST_LOCATION = "s3://bucket/table/manifest.parquet"; | ||
| private static final String DATA_FILE_LOCATION = "s3://bucket/data/file.parquet"; | ||
| private static final String DV_LOCATION = "s3://bucket/puffin/dv-file.bin"; | ||
| private static final String MANIFEST_FILE_LOCATION = "s3://bucket/table/manifest-1.parquet"; |
There was a problem hiding this comment.
Can't we use the existing MANIFEST_LOCATION field?
| value = FileContent.class, | ||
| names = {"DATA_MANIFEST", "DELETE_MANIFEST"}) | ||
| void manifestFileAdapterDelegation(FileContent contentType) { | ||
| ByteBuffer keyMetadata = ByteBuffer.wrap(new byte[] {7, 8, 9}); |
There was a problem hiding this comment.
keyMetadata is not specific to this test, can it be a private static member?
| assertThatThrownBy(manifest::partitionSpecId) | ||
| .isInstanceOf(UnsupportedOperationException.class) | ||
| .hasMessage("v4 manifests are not bound to a single partition spec"); |
There was a problem hiding this comment.
Is spec not allowed or optional for manifests? I know it they aren't bound to a single partition as the comment says, but in case they happen to, then is it still not allowed to set this?
There was a problem hiding this comment.
My rationale was that the concept doesn't apply anymore, so callers should not rely on it.
There was a problem hiding this comment.
I agree with this. We don't want to alter the method unless we have to. Throwing an exception is the right call for now.
| assertThat(copy.keyMetadata()).isEqualTo(original.keyMetadata()); | ||
| assertThat(copy.keyMetadata().array()).isNotSameAs(original.keyMetadata().array()); | ||
| assertThat(copy.deletionVector()).isEqualTo(original.deletionVector()); | ||
| assertThat(copy.deletionVector().array()).isNotSameAs(original.deletionVector().array()); |
There was a problem hiding this comment.
Shouldn't we use the mocking mechanism we do for TrackedFile tests to verify that the nested struct's copy part?
There was a problem hiding this comment.
Thank you. Converting it to mocks.
| Preconditions.checkArgument( | ||
| tracking.dataSequenceNumber() != null, "Invalid data sequence number: null"); | ||
| Preconditions.checkArgument( |
There was a problem hiding this comment.
This has been unclear for me too. I'm not sure how we guard against data corruption on the read side and how we guarantee our internal invariants like this. On the read side we apparently assume that these invariants are satisfied, however, I'm not sure this is safe against some malformed data files.
Anyway, even if we want to check against these constraints, I think we should introduce those checks on the reader, not on the adapter. Reaching this point, I guess we should just assume that TrackedFile is correct and invariants are satisfied.
So probably we are fine not checking them here.
Uh oh!
There was an error while loading. Please reload this page.
| } | ||
| @Override | ||
| public ByteBuffer deletionVector() { |
There was a problem hiding this comment.
It's a little odd that in most places we're using deletionVector that returned DeletionVector but here we're wrapping a dv() call and returning ByteBuffer. I wonder if we should use a different name to signal that it isn't the same thing. What about metadataDV or manifestDeletionVector?
There was a problem hiding this comment.
Renamed to manifestDeletionVector. I think it is a bit redundant to have ManifestFile.manifestDeletionVector - let me know if you have a suggestion here.
| /* dvSnapshotId= */ null, | ||
| FIRST_ROW_ID, | ||
| /* deletedPositions= */ null, | ||
| /* replacedPositions= */ null); |
There was a problem hiding this comment.
Most places use // replaced positions rather than using an inline comment before null.
| null, | ||
| null, | ||
| null, | ||
| null, |
There was a problem hiding this comment.
It would be helpful to label these as well.
| FORMAT_VERSION_V4, | ||
| MANIFEST_LOCATION, | ||
| FileFormat.PARQUET, | ||
| 0L, |
There was a problem hiding this comment.
We should not pass values that could easily be disallowed later with more strict validation. Let's pass a real value here instead.
| @Test | ||
| void manifestFileAdapterPartitionSpecIdUnsupported() { | ||
| TrackedFile file = |
There was a problem hiding this comment.
This test case could be combined with the previous case. That would avoid creating a nearly identical tracked file that leaves the reader wondering what changed between the two cases.
There was a problem hiding this comment.
Folded into manifestFileAdapterDelegation
| } | ||
| /** Returns the manifest deletion vector, or null if absent. */ | ||
| default ByteBuffer manifestDeletionVector() { |
There was a problem hiding this comment.
Originally, I thought that we could come back and update the return type here, but I was wrong because this is a public API interface. So I think rather than using ByteBuffer as a placeholder we should introduce a type to return.
That means we need to decide on a name and whether we want a generic interface for all bitmaps/DVs or if we want one for the manifest/embedded use case. We already have DeletionVector that tracks DV metadata, and PositionDeleteIndex for DVs that we've loaded into memory.
I don't think we want to reuse PositionDeleteIndex because it's mutable and represents a bitmap that could have positions that are in the range of longs. That's also in core so moving it to API to use it here would be a bigger change.
Instead, I think we should introduce EmbeddedBitmap or ManifestBitmap (suggested by @anoopj) and use int for positions. Using a name based on "bitmap" makes it distinct from DeletionVector. Here's a minimal read-only interface that I think we can use:
packageorg.apache.iceberg;
importjava.nio.ByteBuffer;
/** Interface for small bitmaps that are serialized in metadata files. */publicinterfaceEmbeddedBitmap {
/** Number of bits set in this bitmap. */intcardinality();
/** Return whether the bit at {@code position} is set. */booleanisSet(intposition);
/** Return the serialized bitmap as a {@link ByteBuffer}. */ByteBufferbuffer();
}We can also make that smaller if we are concerned about releasing any of those methods. All we really need to decide is the name, but I think we are definitely targeting the int position range so this makes sense to me.
There was a problem hiding this comment.
Sounds great. I went with ManifestBitmap, but open to changing.
| @Override | ||
| public ByteBuffer manifestDeletionVector() { | ||
| return file.manifestInfo().dv(); |
There was a problem hiding this comment.
For now, I think we should replace this with a placeholder implementation or null:
returnnewEmbeddedBitmap() {
@Overridepublicintcardinality() {
thrownewUnsupportedOperationException("Bitmap decoding has not been implemented");
}
@OverridepublicbooleanisSet(intposition) {
thrownewUnsupportedOperationException("Bitmap decoding has not been implemented");
}
@OverridepublicByteBufferbuffer() {
returnfile.manifestInfo().dv();
}
};I'd be fine returning null for now. Either way, once this is in we will need to get the bitmap implementation in and update the V4ManfiestReader to use it.
There was a problem hiding this comment.
Went with the placeholder implementation.
| new TrackingStruct( | ||
| EntryStatus.ADDED, | ||
| SNAPSHOT_ID, | ||
| // data and file sequence numbers must be equal |
There was a problem hiding this comment.
Minor: I don't think this is always true. They can be equal. But I think in the case when you add a new column file, the data sequence number gets updated, but the file sequence number would not.
| .addedFilesCount(3) | ||
| .existingFilesCount(5) | ||
| .deletedFilesCount(2) | ||
| .replacedFilesCount(0) |
There was a problem hiding this comment.
@stevenzwu added these methods to ManifestFile in https://github.com/apache/iceberg/pull/16936/changes. Maybe we should add those here first, since this is a simpler interface?
If we add them, then I think we should test non-zero values. I think that these should default to 0 since that is the case for pre-v4 manifest files. (See my comment: https://github.com/apache/iceberg/pull/16936/changes#r3929689004)
If we don't end up adding them here, we'll need to change these values and test them after that PR is merged.
There was a problem hiding this comment.
To reduce churn in @stevenzwu 's PR, I will leave them out for now. I can do a followup as soon as his PR is merged.
rdblue
left a comment
There was a problem hiding this comment.
This looks good to me. Tests are about ready but I made a couple minor suggestions. The only blocker is introducing an interface for the bitmap since this is returning through a public API (this comment: https://github.com/apache/iceberg/pull/17932/changes#r3938099334)
Reintroduces the ManifestFile adapter closed in #16867, which stalled because ManifestFile had no way to represent a manifest deletion vector. The V4 scan task planner (#17541) plans over manifests through the ManifestFile API, so this adds ManifestFile.deletionVector() and has the adapter expose the tracked file's manifest DV.