Uh oh!
There was an error while loading. Please reload this page.
Core: Basic fields and schemas for column files - #16285
Conversation
Uh oh!
There was an error while loading. Please reload this page.
gaborkaszab
commented
May 11, 2026
First piece of the column update work: introducing the basic interface of the column updates files, aka column files |
630b00e to
ca3259eCompareUh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
ca3259e to
e6f7cf6CompareUh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
681633b to
813d5c0Comparegaborkaszab
commented
May 21, 2026
I opened a thread on dev@ to discuss the metadata structs for column files. Once that's finalized, I'll incorporate the changes here. |
596f6a4 to
6a1cbe9Comparegaborkaszab
commented
Jun 2, 2026
6a1cbe9 to
c683e72Comparec683e72 to
6222fadComparegaborkaszab
commented
Jun 8, 2026
Adjusted field IDs because |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
6222fad to
5c04f55CompareUh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
5c04f55 to
b6ae446Compare| this.status = EntryStatus.MODIFIED; | ||
| } | ||
| // Bumping 'dataSequenceNumber' to avoid having both equality deletes and column files. | ||
| this.dataSequenceNumber = null; |
There was a problem hiding this comment.
We discussed bumping the data sequence number when adding column files. We haven't mentioned file seq num, so I'm not bumping it here.
This works if the manifest owning this data file entry bumps its own seq num when adding column files. Let me know if there is any other way achieving this.
There was a problem hiding this comment.
In a previous google doc discussion, @pvary raised the question if we should just bump up the dataSequenceNumber which captures the logical age of the row. Column file should materialize the _last_updated_sequence_number for unmodified rows and leave the modified rows with null value for inheritance. From row lineage perspective, bumping up dataSequenceNumber is correct and simpler semantically.
data sequence number is only used for v2 equality and position delete matching. it seems that we might be able to forbid writing new equality deletes for v4 tables. I also remember some previous discussion on rewriting equality delete and v2 position delete files when adding a new column file. With the writer requirement, it is safe to just bump up the dataSequenceNumber here.
But the comment line is a bit confusing. I would write as following: Reset to null to inherit from the new snapshot sequence number. It is safe to bump up the dataSequenceNumber as writers are required to rewrite v2 equality and position deletes to DVs when applying column update.
There was a problem hiding this comment.
Thanks for the comment suggestion! Added
b6ae446 to
0a252e8Compare87d43a9 to
56028eeComparegaborkaszab
commented
Jul 23, 2026
Rebased with latest main to resolve git conflicts |
56028ee to
30f42acCompareThere was a problem hiding this comment.
Still need to do a pass over tests, but had some comments on the changes.
The field IDs look reasonable to me. In terms of spec stuff on the entry structure, I remember there was an open quesiton around if we needed split offsets per column file or not. My take is yes, the files would have fundamentally different schemas and there may be more appropriate boundary split points in the file depending on that, but not sure if this had settled. Apologies if this had already reached a consensus and I missed it. @gaborkaszab@anuragmantri
| ByteBuffer keyMetadata(); | ||
| /** Returns the list of recommended split locations for this column file, or null. */ | ||
| List<Long> splitOffsets(); |
There was a problem hiding this comment.
I remember there was a discussion around if we need split offsets per column file or not, what was the conclusion on that? I feel like it makes sense to have them, there may be different files with varying schemas and as a result there would be different boundaries that would be set appropriately for each file. It's additional complexity, but not much?
There was a problem hiding this comment.
I brought this up once on the sync, we didn't have a deep dive but there were no objections either. Rational might be that when we project fields in a way that we don't have to read the base file, we can use the split offsets of one of the column files.
Now, with regular column updates, the column files usually contain a column or two and they have a single row group. However, later for column families we can have wider column files with more than one row groups where we can have split offsets.
| } | ||
| /** Copy constructor. */ | ||
| @SuppressWarnings("CyclomaticComplexity") |
There was a problem hiding this comment.
Any reasonable way to tighten this, or is it just inherent complexity from all the null handling we need to do?
There was a problem hiding this comment.
Yes, this is because the null checks. I didn't want to extract any of these into separate methods as that would have harmed readability.
| class TrackingBuilder { | ||
| private final long newSnapshotId; | ||
| private final Long snapshotId; | ||
| private final Long dataSequenceNumber; |
There was a problem hiding this comment.
Does this need to move?
There was a problem hiding this comment.
It's no longer final, I moved it to the non-final section.
| case 0 -> formatVersion; | ||
| case 1 -> fieldIds(); | ||
| case 2 -> location; | ||
| case 3 -> fileFormat != null ? fileFormat.toString() : null; |
There was a problem hiding this comment.
I think I've noticed this on other PRs for TrackedFile for instance, when would fileFormat be null? It's required right?
There was a problem hiding this comment.
I took this from TrackedFileStruct where we guard against fileFormat and contentType being null, however both of them are required. Maybe makes sense when we have a projected read and we don't project those fields?
| toCopy.equalityIds != null | ||
| ? Arrays.copyOf(toCopy.equalityIds, toCopy.equalityIds.length) | ||
| : null; | ||
| this.columnFiles = |
There was a problem hiding this comment.
Minor: Could we express this without the Java stream? It's just a bit on the hot path when reading entries so all those allocations from the additional objects that streams create may add up.
870edbd to
c303670Comparegaborkaszab
commented
Jul 30, 2026
Rebased with main to resolve conflicts |
gaborkaszab
left a comment
There was a problem hiding this comment.
Thanks for taking a look, @amogh-jahagirdar !
| ByteBuffer keyMetadata(); | ||
| /** Returns the list of recommended split locations for this column file, or null. */ | ||
| List<Long> splitOffsets(); |
There was a problem hiding this comment.
I brought this up once on the sync, we didn't have a deep dive but there were no objections either. Rational might be that when we project fields in a way that we don't have to read the base file, we can use the split offsets of one of the column files.
Now, with regular column updates, the column files usually contain a column or two and they have a single row group. However, later for column families we can have wider column files with more than one row groups where we can have split offsets.
| case 0 -> formatVersion; | ||
| case 1 -> fieldIds(); | ||
| case 2 -> location; | ||
| case 3 -> fileFormat != null ? fileFormat.toString() : null; |
There was a problem hiding this comment.
I took this from TrackedFileStruct where we guard against fileFormat and contentType being null, however both of them are required. Maybe makes sense when we have a projected read and we don't project those fields?
| } | ||
| /** Copy constructor. */ | ||
| @SuppressWarnings("CyclomaticComplexity") |
There was a problem hiding this comment.
Yes, this is because the null checks. I didn't want to extract any of these into separate methods as that would have harmed readability.
| toCopy.equalityIds != null | ||
| ? Arrays.copyOf(toCopy.equalityIds, toCopy.equalityIds.length) | ||
| : null; | ||
| this.columnFiles = |
| class TrackingBuilder { | ||
| private final long newSnapshotId; | ||
| private final Long snapshotId; | ||
| private final Long dataSequenceNumber; |
There was a problem hiding this comment.
It's no longer final, I moved it to the non-final section.
Defines the column_file element struct referenced by the column_files field (158) in the v4 content entry, matching the ColumnFile schema added in apache#16285. Co-authored-by: Gabor Kaszab <gaborkaszab@gmail.com> Co-authored-by: Anurag Mantripragada <amantripragada@apple.com>
Makes the column_files field type list<159: column_file> to match the inline element-id convention used by other list fields in the content entry, matching apache#16285. Co-authored-by: Gabor Kaszab <gaborkaszab@gmail.com> Co-authored-by: Anurag Mantripragada <amantripragada@apple.com>
c303670 to
5d96c17Compare
anuragmantri
left a comment
There was a problem hiding this comment.
I did another round after adding key_metdata and split_offsets. I think this is ready to be merged.
| if (status == EntryStatus.EXISTING) { | ||
| this.status = EntryStatus.MODIFIED; | ||
| } | ||
| // Bumping 'dataSequenceNumber' to avoid having both equality deletes and column files. |
There was a problem hiding this comment.
Should this comment be?
| // Bumping 'dataSequenceNumber' to avoid having both equality deletes and column files. | |
| // Clears dataSequenceNumber so it re-inherits from the manifest at read time. |
There was a problem hiding this comment.
Thanks for the suggestion! Steven also had one, I went with that.
| assertThat(withDeletedPositions.latestColumnFileSnapshotId()).isEqualTo(999L); | ||
| assertThat(withDeletedPositions.dvSnapshotId()).isEqualTo(999L); | ||
| assertThat(withDeletedPositions.deletedPositions()).isEqualTo(deletedBytes); | ||
There was a problem hiding this comment.
Should we verify the dataSequenceNumber is null?
| assertThat(withDeletedPositions.dataSequenceNumber()).isNull(); |
Same on L314 and in manifestPositionsWithColumnFilesUpdated() test
Uh oh!
There was an error while loading. Please reload this page.
| case 5 -> this.keyMetadata = ByteBuffers.toByteArray((ByteBuffer) value); | ||
| case 6 -> this.splitOffsets = ArrayUtil.toLongArray((List<Long>) value); | ||
| default -> { | ||
| // ignore the object, it must be from a newer version of the format |
There was a problem hiding this comment.
nit: should the comment say `ignore the unknown positions, as they must come from a newer version of the format"
There was a problem hiding this comment.
This comment is inline with the same in TrackedFileStruct and TrackingStruct. I'd rather keep consistency with these.
There was a problem hiding this comment.
that's fine for consistency. I found "ignore the object" not very accurate.
| this.status = EntryStatus.MODIFIED; | ||
| } | ||
| // Bumping 'dataSequenceNumber' to avoid having both equality deletes and column files. | ||
| this.dataSequenceNumber = null; |
There was a problem hiding this comment.
In a previous google doc discussion, @pvary raised the question if we should just bump up the dataSequenceNumber which captures the logical age of the row. Column file should materialize the _last_updated_sequence_number for unmodified rows and leave the modified rows with null value for inheritance. From row lineage perspective, bumping up dataSequenceNumber is correct and simpler semantically.
data sequence number is only used for v2 equality and position delete matching. it seems that we might be able to forbid writing new equality deletes for v4 tables. I also remember some previous discussion on rewriting equality delete and v2 position delete files when adding a new column file. With the writer requirement, it is safe to just bump up the dataSequenceNumber here.
But the comment line is a bit confusing. I would write as following: Reset to null to inherit from the new snapshot sequence number. It is safe to bump up the dataSequenceNumber as writers are required to rewrite v2 equality and position deletes to DVs when applying column update.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| Tracking withDeletedPositions = | ||
| TrackingBuilder.from(manifestSourceTracking(), 999L) | ||
| .columnFilesUpdated() | ||
| .deletedPositions(deletedBytes) |
There was a problem hiding this comment.
deletedPositions bitmap is only meant for leaf manifest entry in the root manifest file? Ae we testing the scenario of column update for a leaf manifest file in this test?
There was a problem hiding this comment.
I don't think technically we want to avoid providing deleted/replaced positions together with column files. I just wanted to pin this down with a test.
Giving this some further thought, I think you're right: Such a Tracking that has these positions is an entry in the root manifest pointing to a leaf manifest. I don't think we plan to add column files for leaf manifest at this point, but it seems too strict to reject such a setting.
Could such a test remain? WDYT @stevenzwu ?
There was a problem hiding this comment.
I don't think we plan to add column files for leaf manifest at this point
We will use column files for leaf manifests in v4. we should keep this test.
I was mainly alluding to if we should cover the column update for data file cases, where deletedPositions is not applicable.
There was a problem hiding this comment.
In Tracking and TrackingBuilder we don't really know if it belongs to a data file entry or a manifest entry. There might be implications like presence of deleted positions or dv_snapshot_id but nothing decisive. We can add a separate test where we don't set deleted/replaced positions, but probably it doesn't add much to the coverage.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
5d96c17 to
2bdf7bbCompare
gaborkaszab
left a comment
There was a problem hiding this comment.
Thanks for the reviews @anuragmantri and @stevenzwu ! I believe I addressed all your comments. Would you mind taking another look?
Uh oh!
There was an error while loading. Please reload this page.
| case 5 -> this.keyMetadata = ByteBuffers.toByteArray((ByteBuffer) value); | ||
| case 6 -> this.splitOffsets = ArrayUtil.toLongArray((List<Long>) value); | ||
| default -> { | ||
| // ignore the object, it must be from a newer version of the format |
There was a problem hiding this comment.
This comment is inline with the same in TrackedFileStruct and TrackingStruct. I'd rather keep consistency with these.
| if (status == EntryStatus.EXISTING) { | ||
| this.status = EntryStatus.MODIFIED; | ||
| } | ||
| // Bumping 'dataSequenceNumber' to avoid having both equality deletes and column files. |
There was a problem hiding this comment.
Thanks for the suggestion! Steven also had one, I went with that.
| this.status = EntryStatus.MODIFIED; | ||
| } | ||
| // Bumping 'dataSequenceNumber' to avoid having both equality deletes and column files. | ||
| this.dataSequenceNumber = null; |
There was a problem hiding this comment.
Thanks for the comment suggestion! Added
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| assertThat(withDeletedPositions.latestColumnFileSnapshotId()).isEqualTo(999L); | ||
| assertThat(withDeletedPositions.dvSnapshotId()).isEqualTo(999L); | ||
| assertThat(withDeletedPositions.deletedPositions()).isEqualTo(deletedBytes); | ||
| Tracking withDeletedPositions = | ||
| TrackingBuilder.from(manifestSourceTracking(), 999L) | ||
| .columnFilesUpdated() | ||
| .deletedPositions(deletedBytes) |
There was a problem hiding this comment.
I don't think technically we want to avoid providing deleted/replaced positions together with column files. I just wanted to pin this down with a test.
Giving this some further thought, I think you're right: Such a Tracking that has these positions is an entry in the root manifest pointing to a leaf manifest. I don't think we plan to add column files for leaf manifest at this point, but it seems too strict to reject such a setting.
Could such a test remain? WDYT @stevenzwu ?
Uh oh!
There was an error while loading. Please reload this page.
2bdf7bb to
d0509a7Comparegaborkaszab
commented
Aug 24, 2026
Thanks for the approval @stevenzwu and for the reviews @anuragmantri , @amogh-jahagirdar , @RussellSpitzer! |
c26a609 to
dd2433cCompareThis change introduces the interface for column files and also integrates it to the schema for TrackedFile.
dd2433c to
b0712b8Compare
This change introduces the interface for column files and also integrates it to the schema for TrackedFile.