Repository navigation
Conversation
gaborkaszab
left a comment
There was a problem hiding this comment.
Hey @stevenzwu ,
I went through mostly for my own understanding. Left some comments, mostly I'm a bit hesitant to expose setting status and sequence numbers directly through the builders. Let me know what you think!
c78d6b1 to
828e0c7
Compare
828e0c7 to
fa86dc1
Compare
fa46d3d to
55f5573
Compare
b5c92a0 to
d5c696b
Compare
Keep cached field stats when rebinding a file, and return null from a factory when the field has no stats struct. Look up the bound type by name. Generated-by: Cursor Grok 4.7 Co-authored-by: Cursor <cursoragent@cursor.com>
| return (FieldStats<T>) statsById.get(fieldId); | ||
| } | ||
|
|
||
| FieldStats<?> createFieldStats(int fieldId) { |
There was a problem hiding this comment.
Nit: private? Or is this exposed for testing?
| throw new UnsupportedOperationException("copy is not implemented"); | ||
| } | ||
|
|
||
| boolean containsFieldInMaps(int fieldId) { |
| new PartitionData(PartitionSpec.unpartitioned().partitionType()); | ||
|
|
||
| /** | ||
| * Stats on fields 1-4; field 5 is absent. Field 3 has no value_count entry so the absent-count |
There was a problem hiding this comment.
What is an "absent-count" getter?
There was a problem hiding this comment.
Reworded the comment. Field 3 has no value-count entry, so valueCount() throws when it unboxes a null Long.
| 4, buf(Types.StringType.get(), "zzz"))); | ||
|
|
||
| @Test | ||
| void contentStatsTypeBuiltLazilyFromMapIds() { |
There was a problem hiding this comment.
This doesn't test that the type was built lazily and probably shouldn't. Correctness is what should be tested, not laziness.
There was a problem hiding this comment.
Renamed this to typeMatchesIdsPresentInMaps. It checks the struct before wrap and the fields after wrap. It does not assert laziness.
|
|
||
| assertThat(stats.type().fields()) | ||
| .extracting(field -> StatsUtil.toFieldId(field.fieldId())) | ||
| .containsExactlyInAnyOrder(1, 2, 3, 4); |
There was a problem hiding this comment.
I think that this should validate against statsReadSchema so that all fields are tested.
There was a problem hiding this comment.
The type is now compared to statsReadSchema for the ids in the file. The comparison is order-independent because the id set is a HashSet. wrapInvalidatesType does the same for the single remaining id.
| } | ||
|
|
||
| @Test | ||
| void boundDecodingPerType() { |
There was a problem hiding this comment.
I think that this should test all data types. We have similar tests in TestFieldStatsStruct that you can copy: https://github.com/apache/iceberg/blob/main/core/src/test/java/org/apache/iceberg/TestFieldStatsStruct.java#L203-L228
There was a problem hiding this comment.
boundDecodingPerType is now a parameterized round trip over the same type and bound pairs as TestFieldStatsStruct.TYPES_AND_BOUNDS is private, so the tuples are copied here. Unknown still expects null bounds.
Geometry stays in geoBoundsDecode.
| } | ||
|
|
||
| @Test | ||
| void copyNotSupported() { |
There was a problem hiding this comment.
This is alright, but I don't think that we need to test it. We may choose to implement this later.
There was a problem hiding this comment.
Removed copyNotSupported and fieldStatsCopyNotSupported.
| } | ||
|
|
||
| @Test | ||
| void reuseRebindsBounds() { |
There was a problem hiding this comment.
The assertions here are mixed. Only the lower bound is tested for the example file, but lower bound, upper bound, value count, and another field entirely have assertions for file2. This could be "reuseRebindsFields` if you want to test them all and a negative case (id=2), but I would do either both bounds or all fields, not half-way between.
There was a problem hiding this comment.
Renamed to reuseRebindsFields. Both files now assert lower bound, upper bound, and value count for field 1. The second file still expects statsFor(2) to be null.
| } | ||
|
|
||
| @Test | ||
| void wrapReusesFieldStats() { |
There was a problem hiding this comment.
I would remove this. Correctness doesn't depend on reusing stats and I think the tests should validate correctness, not that a particular implementation choice was made.
There was a problem hiding this comment.
Removed wrapReusesFieldStats. absentIdIsRereadWhenPresentAgain no longer checks instance identity. It still checks that the id is absent, then present again with the new file's values.
| } | ||
|
|
||
| @Test | ||
| void outOfRangeFieldStatsAreNullAndCached() { |
There was a problem hiding this comment.
I don't think that this is needed, especially if it requires extra code injected to validate caching.
There was a problem hiding this comment.
Removed outOfRangeFieldStatsAreNullAndCached and CountingContentStats.
| Map<Integer, ByteBuffer> upperBounds) { | ||
| Metrics metrics = | ||
| new Metrics( | ||
| 100L, null, valueCounts, nullValueCounts, nanValueCounts, lowerBounds, upperBounds); |
There was a problem hiding this comment.
I think it would be better to pass in 100 rather than assuming it matches value counts.
There was a problem hiding this comment.
dataFile helper now takes the record count and passes it into Metrics. Each call site passes 100L.
|
|
||
| @Test | ||
| void manifestFileAdapterKeepsZeroFormatVersion() { | ||
| TrackedFile original = dummyTrackedFile(FileContent.DATA_MANIFEST, 0); |
There was a problem hiding this comment.
It is unclear what is being tested here because of the use of dummyTrackedFile. Is this a tracked file? Why would a tracked file have a format version of 0?
There was a problem hiding this comment.
Removed manifestFileAdapterKeepsZeroFormatVersion. dataManifestTrackedFileAdapter already wraps a real manifest whose format version is 0.
| PartitionData.EMPTY, | ||
| 1024L, | ||
| new Metrics(100L, null, null, null, null), | ||
| null, |
There was a problem hiding this comment.
What are these null values and why should they not be tested?
There was a problem hiding this comment.
DATA_FILE now uses new Metrics(100L), KEY_METADATA, a sort order id, and FIRST_ROW_ID, and the field helper asserts those values. firstRowId is checked on the existing-entry path. wrapAppend still suppresses it.
| } | ||
|
|
||
| @Test | ||
| void dataTrackedFileAdapterHasNoTracking() { |
There was a problem hiding this comment.
I think this is too specific should be combined with other cases. There should be a test that validates the behavior for DATA_FILE and this should be part of that test.
There was a problem hiding this comment.
Kept this as the single wrap(DataFile) test and renamed it to dataTrackedFileAdapterFromDataFile. It still expects tracking to be null.
|
|
||
| @Test | ||
| void dataTrackedFileAdapterFromAddedManifestEntry() { | ||
| TrackedFile result = TrackedFileAdapters.forDataFile(TABLE_SCHEMA).wrap(addedEntry(DATA_FILE)); |
There was a problem hiding this comment.
Entry is pretty small so it would be great to just embed the information here rather than needing to go look at what addedEntry sets for defaults.
There was a problem hiding this comment.
Removed addedEntry, existingEntry, and deletedEntry. The remaining tests call wrapExisting directly.
|
|
||
| assertThat(result.tracking().status()).isEqualTo(EntryStatus.ADDED); | ||
| assertThat(result.tracking().snapshotId()).isEqualTo(SNAPSHOT_ID); | ||
| assertThat(result.tracking().dataSequenceNumber()).isNull(); |
There was a problem hiding this comment.
Why are the sequence numbers null? Are they not mapped properly or are they set to null in Tracking? If it is that they are null in tracking, I think this should pass real values because they should be non-null when converting from a manifest entry.
There was a problem hiding this comment.
Those nulls came from wrapAppend, which stores a null data sequence number. The existing-entry test now asserts DATA_SEQUENCE_NUMBER and FILE_SEQUENCE_NUMBER.
| assertThat(result.tracking().snapshotId()).isEqualTo(SNAPSHOT_ID); | ||
| assertThat(result.tracking().dataSequenceNumber()).isNull(); | ||
| assertThat(result.tracking().fileSequenceNumber()).isNull(); | ||
| assertThat(result.tracking().firstRowId()).isNull(); |
There was a problem hiding this comment.
The existing-entry test builds the file with FIRST_ROW_ID and asserts tracking().firstRowId().
| TrackedFile result = TrackedFileAdapters.forDataFile(TABLE_SCHEMA).wrap(DATA_FILE); | ||
|
|
||
| assertThat(result.tracking()).isNull(); | ||
| assertWrappedDataFileMatchesFileFields(result, DATA_FILE); |
There was a problem hiding this comment.
Good that this is validated. That means that this test is correct and it's just the name that is wrong.
There was a problem hiding this comment.
Renamed to dataTrackedFileAdapterFromDataFile.
| assertWrappedDataFileMatchesFileFields(result, DATA_FILE); | ||
| assertThatThrownBy(result::formatVersion) | ||
| .isInstanceOf(IllegalStateException.class) | ||
| .hasMessage("Format version is assigned at write time"); |
There was a problem hiding this comment.
Hm. Should this be 0 since it is converted from v3 or earlier?
There was a problem hiding this comment.
I think this should keep throwing. A v3 DataFile has no format version to copy. On the manifest path, 0 is forwarded from ManifestFile.formatVersion when the manifest predates format tracking.
We also discussed on Monday's sync that format version should be moved to manifest_info struct that should be set at write time.
| } | ||
|
|
||
| @Test | ||
| void dataTrackedFileAdapterFromAddedManifestEntryWithNullSnapshotId() { |
There was a problem hiding this comment.
I don't think this is needed. We know from other tests that the snapshot ID is plumbed through correctly. We don't need to validate that it is plumbed correctly when it takes a specific value (null).
There was a problem hiding this comment.
Removed this. wrapAppend stores the snapshot id it is given, and the existing-entry test already checks a non-null id.
| } | ||
|
|
||
| @Test | ||
| void dataTrackedFileAdapterFromExistingManifestEntry() { |
There was a problem hiding this comment.
I think this is a better test than the added version because it checks values. But we don't need two. The point here is to check that fields are mapped or defaulted correctly in the adapter, not to check that specific cases work.
There was a problem hiding this comment.
Kept dataTrackedFileAdapterFromExistingManifestEntry and removed the added-entry test. The existing-entry test inlines wrapExisting and checks status, snapshot id, both sequence numbers, first row id, manifest position, and the file fields.
| } | ||
|
|
||
| @Test | ||
| void dataTrackedFileAdapterFromDeletedManifestEntry() { |
There was a problem hiding this comment.
Removed dataTrackedFileAdapterFromDeletedManifestEntry.
| TrackedFileAdapters.DataTrackedFile adapter = TrackedFileAdapters.forDataFile(TABLE_SCHEMA); | ||
|
|
||
| adapter.wrap(DATA_FILE); | ||
| assertThat(adapter.location()).isEqualTo(DATA_FILE_LOCATION); |
There was a problem hiding this comment.
There's a helper to validate all fields, so we should use it.
There was a problem hiding this comment.
dataTrackedFileAdapterReuse now calls assertWrappedDataFileMatchesFileFields after each wrap. The test only asserts tracking itself: null, then EXISTING and the manifest position.
| TrackedFileAdapters.asDataFile(result, specsById(PARTITIONED_SPEC)) | ||
| .partition() | ||
| .get(0, String.class)) | ||
| .isEqualTo("books"); |
There was a problem hiding this comment.
What is this assertion doing?
I would expect this to use a struct comparator to verify that PARTITION matches result.partition().
There was a problem hiding this comment.
The partition is compared with Comparators.forType on the tracked file and again after asDataFile.
|
|
||
| DataFile roundTripped = TrackedFileAdapters.asDataFile(tracked, specs); | ||
|
|
||
| assertThat(roundTripped.content()).isEqualTo(FileContent.DATA); |
There was a problem hiding this comment.
This should validate that the returned file is the same as the original. That is, it should assert that the file was unwrapped, not equal.
The reason to validate this is to ensure that we can rely on the assumption that double wrapping does not occur so you are always wrapping a real DataFile.
There was a problem hiding this comment.
Renamed this to trackedFileDoubleWrapRoundTrip. It builds a fresh TrackedFile, converts it with asDataFile, and asserts wrap returns that same TrackedFile.
asDataFile always allocates a new TrackedDataFile, so the DataFile to TrackedFile to DataFile path is not identity. The original-file check is only in wrap, when the argument is already a TrackedDataFile.
|
|
||
| private static ManifestFile writeManifestFile( | ||
| ManifestContent content, long sequenceNumber, long minSequenceNumber) { | ||
| return writeManifestFile(content, sequenceNumber, minSequenceNumber, 200L); |
There was a problem hiding this comment.
I'd prefer having constants defined at the top and to minimize these helper methods since you have to look at how they work in order to see if a test is valid or correct.
There was a problem hiding this comment.
Removed the unused 3-arg writeManifestFile and hoisted the manifest counts and sequence numbers the tests assert. The 4-arg overload stays because one rejection test passes a null added-rows count. manifestWithCounts stays for those rejection cases.
| return dummyTrackedFile(contentType, FORMAT_VERSION_V4); | ||
| } | ||
|
|
||
| private static TrackedFileStruct dummyTrackedFile(FileContent contentType, int formatVersion) { |
There was a problem hiding this comment.
Minor: Why "dummy" here but almost nowhere else? I don't think this naming convention is a good one. Dummy is not telling the reader anything significant. And in fact, it is misleading because this creates a TrackedFileStruct rather than something else.
There was a problem hiding this comment.
Renamed dummyTrackedFile to trackedFile.
Assert mapped values instead of implementation details, matching review feedback on the v4 write-direction wrappers. Generated-by: Cursor Grok 4.7 Co-authored-by: Cursor <cursoragent@cursor.com>
The helpers are only used inside MapBackedContentStats. The manifest count errors stay valid for a null count. Generated-by: Cursor Grok 4.7 Co-authored-by: Cursor <cursoragent@cursor.com>
What
Adds v4 write-direction
TrackedFileadapters: reusable forwarding wrappers that present a legacy v2/v3ContentFile(DataFile/DeleteFile) as a v4 manifest-entry row on the write path, without materializing a fresh struct per row. A single adapter is allocated per writer and re-pointed at each file; content stats are served by a map-backed view (MapBackedContentStats) over the file's stat maps, withcopy()producing stable snapshots.Benchmark summary
JMH microbenchmarks compared two strategies for presenting a legacy file as a v4 row: convert (materialize a fresh struct per row) vs wrap (a reusable wrapper allocated once per writer and re-pointed per row, with zero per-row allocation). Throughput in ops/ms (higher is better); allocation via the GC profiler in B/op (lower is better).
Net: the reusable wrap model is adopted because it wins or ties everywhere and minimizes per-row allocation — the cost that grows with table width.
See the benchmark doc for full methodology and per-column numbers.