Core: Basic fields and schemas for column files - #16285
Conversation
|
First piece of the column update work: introducing the basic interface of the column updates files, aka column files |
630b00e to
ca3259e
Compare
ca3259e to
e6f7cf6
Compare
681633b to
813d5c0
Compare
|
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
6a1cbe9
Compare
6a1cbe9 to
c683e72
Compare
c683e72 to
6222fad
Compare
|
Adjusted field IDs because |
6222fad to
5c04f55
Compare
5c04f55 to
b6ae446
Compare
| 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.
b6ae446 to
0a252e8
Compare
b4d6357 to
c98ea62
Compare
|
Rebased with main because there were overlapping activity. Will re-think what fields should be part of column files. Coming back with an update soon |
b207b61 to
a51c0e9
Compare
| import java.util.List; | ||
| import org.apache.iceberg.types.Types; | ||
|
|
||
| /** Information about a column file. */ |
There was a problem hiding this comment.
nit: Not a helpful javadoc, either needs more information about what a column file is or drop
There was a problem hiding this comment.
Decided to drop the javadoc
| Types.NestedField FORMAT_VERSION = | ||
| Types.NestedField.required( | ||
| 161, "format_version", Types.IntegerType.get(), "Format version of this column file"); | ||
| Types.NestedField FIELD_IDS = |
There was a problem hiding this comment.
If we keep the field id's here, does that mean we need to update all column file entities whenever we add a new column file? I think that's probably alright but we should probably add some text on that requirement?
There was a problem hiding this comment.
Yes, when we add a new column file, we have to remove its field IDs from other column files' field ID lists. I'm not sure we should document it here, this just the storage representation of such a structure. The "deduplication" of field IDs happen on a different level, maybe document this on the API where we add the new column files (non-existing as of now)?
| interface ColumnFile { | ||
| Types.NestedField FORMAT_VERSION = | ||
| Types.NestedField.required( | ||
| 161, "format_version", Types.IntegerType.get(), "Format version of this column file"); |
There was a problem hiding this comment.
What format version? The Table format version when the file was written? Do we really need this?
There was a problem hiding this comment.
I'd probably change this later, but the comment should probably be "Table Format version used to write this column file" or something
There was a problem hiding this comment.
I followed the comment for the similar field in TrackedFile that says "Format version of this file".
|
|
||
| Builder formatVersion(int newFormatVersion) { | ||
| Preconditions.checkArgument( | ||
| newFormatVersion >= 0, "Invalid format version: %s (must be >= 0)", newFormatVersion); |
There was a problem hiding this comment.
If we are going to have this we should also block format version > MAX_FORMAT_VERSION
There was a problem hiding this comment.
Not sure I get this, but isn't MAX_FORMAT_VERSION only used for tests? In real life, writers will provide their format version here, I'm not sure if we keep track what is the highest possible value we accept.
Maybe @stevenzwu can share if he has anything else in mind with this.
|
|
||
| Builder fieldIds(List<Integer> newFieldIds) { | ||
| Preconditions.checkArgument(newFieldIds != null, "Invalid field IDs: null"); | ||
| Preconditions.checkArgument(!newFieldIds.isEmpty(), "Invalid field IDs: empty"); |
There was a problem hiding this comment.
Does this mean we have to remove a column file if we override all the columns in it
There was a problem hiding this comment.
Yes, this is what I have in mind. However, that's not a concern of this class, I figure we could have some sort of a builder for the TrackedFile's column file list, where we take care of that.
| Preconditions.checkArgument(!newFieldIds.isEmpty(), "Invalid field IDs: empty"); | ||
| Preconditions.checkArgument( | ||
| Sets.newHashSet(newFieldIds).size() == newFieldIds.size(), | ||
| "Invalid field IDs: duplicateD IDs found: %s", |
There was a problem hiding this comment.
The message here makes it seem like it's about to display duplicate ids but it displays everything. I would either change the message a bit or actually list the duplicated ids.
There was a problem hiding this comment.
Changed the message a bit
| 160, | ||
| "latest_column_file_snapshot_id", | ||
| Types.LongType.get(), | ||
| "Snapshot ID where the latest column file was added; null if there are no column files"); |
There was a problem hiding this comment.
I think this should mirror the SnapshotID
"Snapshot ID where the file was added, deleted, or replaced");
"Snapshot ID where the latest column file was added; deleted or replaced; null if there are no column files"
There was a problem hiding this comment.
I'm not sure the same operations apply to column files as for the base file. A column file is added, but I don't think we want to delete or replace a column file as a separate operation (could be achieved by adding a new column file that eliminates one or more old ones). Just for additional context, this field is going to be used to return last_updated_sequence_number.
I think the same logic applies here as for the dv_snapshot_id where we say this:
"Snapshot ID where the DV was added; null if there is no DV"
| 162, | ||
| "field_ids", | ||
| Types.ListType.ofRequired(163, Types.IntegerType.get()), | ||
| "Field IDs this column file contains"); |
There was a problem hiding this comment.
"Live Field IDs in this column file"?
a51c0e9 to
6980283
Compare
gaborkaszab
left a comment
There was a problem hiding this comment.
Thank you for taking a look, @RussellSpitzer !
I made minor adjustments, mostly answered questions or explained concepts.
| import java.util.List; | ||
| import org.apache.iceberg.types.Types; | ||
|
|
||
| /** Information about a column file. */ |
There was a problem hiding this comment.
Decided to drop the javadoc
| interface ColumnFile { | ||
| Types.NestedField FORMAT_VERSION = | ||
| Types.NestedField.required( | ||
| 161, "format_version", Types.IntegerType.get(), "Format version of this column file"); |
There was a problem hiding this comment.
I followed the comment for the similar field in TrackedFile that says "Format version of this file".
| Types.NestedField FORMAT_VERSION = | ||
| Types.NestedField.required( | ||
| 161, "format_version", Types.IntegerType.get(), "Format version of this column file"); | ||
| Types.NestedField FIELD_IDS = |
There was a problem hiding this comment.
Yes, when we add a new column file, we have to remove its field IDs from other column files' field ID lists. I'm not sure we should document it here, this just the storage representation of such a structure. The "deduplication" of field IDs happen on a different level, maybe document this on the API where we add the new column files (non-existing as of now)?
| 162, | ||
| "field_ids", | ||
| Types.ListType.ofRequired(163, Types.IntegerType.get()), | ||
| "Field IDs this column file contains"); |
|
|
||
| Builder formatVersion(int newFormatVersion) { | ||
| Preconditions.checkArgument( | ||
| newFormatVersion >= 0, "Invalid format version: %s (must be >= 0)", newFormatVersion); |
There was a problem hiding this comment.
Not sure I get this, but isn't MAX_FORMAT_VERSION only used for tests? In real life, writers will provide their format version here, I'm not sure if we keep track what is the highest possible value we accept.
Maybe @stevenzwu can share if he has anything else in mind with this.
|
|
||
| Builder fieldIds(List<Integer> newFieldIds) { | ||
| Preconditions.checkArgument(newFieldIds != null, "Invalid field IDs: null"); | ||
| Preconditions.checkArgument(!newFieldIds.isEmpty(), "Invalid field IDs: empty"); |
There was a problem hiding this comment.
Yes, this is what I have in mind. However, that's not a concern of this class, I figure we could have some sort of a builder for the TrackedFile's column file list, where we take care of that.
| Preconditions.checkArgument(!newFieldIds.isEmpty(), "Invalid field IDs: empty"); | ||
| Preconditions.checkArgument( | ||
| Sets.newHashSet(newFieldIds).size() == newFieldIds.size(), | ||
| "Invalid field IDs: duplicateD IDs found: %s", |
There was a problem hiding this comment.
Changed the message a bit
| 160, | ||
| "latest_column_file_snapshot_id", | ||
| Types.LongType.get(), | ||
| "Snapshot ID where the latest column file was added; null if there are no column files"); |
There was a problem hiding this comment.
I'm not sure the same operations apply to column files as for the base file. A column file is added, but I don't think we want to delete or replace a column file as a separate operation (could be achieved by adding a new column file that eliminates one or more old ones). Just for additional context, this field is going to be used to return last_updated_sequence_number.
I think the same logic applies here as for the dv_snapshot_id where we say this:
"Snapshot ID where the DV was added; null if there is no DV"
6980283 to
87d43a9
Compare
87d43a9 to
56028ee
Compare
|
Rebased with latest main to resolve git conflicts |
56028ee to
30f42ac
Compare
There 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.
30f42ac to
870edbd
Compare
This change introduces the interface for column files and also integrates it to the schema for TrackedFile.
870edbd to
c303670
Compare
|
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>
This change introduces the interface for column files and also integrates it to the schema for TrackedFile.