Skip to content

Core: Basic fields and schemas for column files - #16285

Open
gaborkaszab wants to merge 1 commit into
apache:mainfrom
gaborkaszab:main_column_file_interface
Open

Core: Basic fields and schemas for column files#16285
gaborkaszab wants to merge 1 commit into
apache:mainfrom
gaborkaszab:main_column_file_interface

Conversation

@gaborkaszab

Copy link
Copy Markdown
Contributor

This change introduces the interface for column files and also integrates it to the schema for TrackedFile.

@github-actions github-actions Bot added the core label May 11, 2026
Comment thread core/src/main/java/org/apache/iceberg/ColumnFileInfo.java Outdated
@gaborkaszab

Copy link
Copy Markdown
Contributor Author

First piece of the column update work: introducing the basic interface of the column updates files, aka column files
cc @anuragmantri @rdblue @pvary @RussellSpitzer @amogh-jahagirdar @anoopj @nastra

Comment thread core/src/main/java/org/apache/iceberg/ColumnFileInfo.java Outdated
Comment thread core/src/main/java/org/apache/iceberg/ColumnFileInfo.java Outdated
Comment thread core/src/main/java/org/apache/iceberg/TrackedFileStruct.java
Comment thread core/src/main/java/org/apache/iceberg/ColumnFileInfo.java Outdated
@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch from ca3259e to e6f7cf6 Compare May 12, 2026 09:35
Comment thread core/src/main/java/org/apache/iceberg/ColumnFileInfo.java Outdated
Comment thread core/src/main/java/org/apache/iceberg/ColumnFileInfo.java Outdated
@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch 3 times, most recently from 681633b to 813d5c0 Compare May 13, 2026 12:52
@gaborkaszab

Copy link
Copy Markdown
Contributor Author

I opened a thread on dev@ to discuss the metadata structs for column files. Once that's finalized, I'll incorporate the changes here.

@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch 3 times, most recently from 596f6a4 to 6a1cbe9 Compare June 2, 2026 13:12
@gaborkaszab

Copy link
Copy Markdown
Contributor Author

cc @amogh-jahagirdar @rdblue @anoopj

@gaborkaszab gaborkaszab changed the title Core: Introduce interface for column files Core: Basic fields and schemas for column files Jun 3, 2026
@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch from 6a1cbe9 to c683e72 Compare June 4, 2026 06:55
@pvary pvary moved this to In review in V4: metadata tree Jun 8, 2026
@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch from c683e72 to 6222fad Compare June 8, 2026 12:32
@gaborkaszab

Copy link
Copy Markdown
Contributor Author

Adjusted field IDs because 157 is going to be allocated for writer_format_version in this PR.

Comment thread core/src/main/java/org/apache/iceberg/TrackedFileStruct.java
Comment thread core/src/main/java/org/apache/iceberg/TrackingStruct.java
@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch from 6222fad to 5c04f55 Compare June 11, 2026 09:27
Comment thread core/src/main/java/org/apache/iceberg/TrackedFileStruct.java Outdated
Comment thread core/src/main/java/org/apache/iceberg/TrackingBuilder.java
@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch from 5c04f55 to b6ae446 Compare June 12, 2026 14:37
this.status = EntryStatus.MODIFIED;
}
// Bumping 'dataSequenceNumber' to avoid having both equality deletes and column files.
this.dataSequenceNumber = null;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch from b6ae446 to 0a252e8 Compare June 12, 2026 15:09
@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch from b4d6357 to c98ea62 Compare July 10, 2026 12:38
@gaborkaszab

Copy link
Copy Markdown
Contributor Author

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

@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch 2 times, most recently from b207b61 to a51c0e9 Compare July 14, 2026 12:46
import java.util.List;
import org.apache.iceberg.types.Types;

/** Information about a column file. */

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: Not a helpful javadoc, either needs more information about what a column file is or drop

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What format version? The Table format version when the file was written? Do we really need this?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd probably change this later, but the comment should probably be "Table Format version used to write this column file" or something

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we are going to have this we should also block format version > MAX_FORMAT_VERSION

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this mean we have to remove a column file if we override all the columns in it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"Live Field IDs in this column file"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure, adjusted

@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch from a51c0e9 to 6980283 Compare July 15, 2026 13:47

@gaborkaszab gaborkaszab left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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. */

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure, adjusted


Builder formatVersion(int newFormatVersion) {
Preconditions.checkArgument(
newFormatVersion >= 0, "Invalid format version: %s (must be >= 0)", newFormatVersion);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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"

@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch from 6980283 to 87d43a9 Compare July 15, 2026 14:11
@RussellSpitzer RussellSpitzer added the Iceberg V4 Iceberg Table Format Version 4 label Jul 16, 2026
@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch from 87d43a9 to 56028ee Compare July 23, 2026 13:47
@gaborkaszab

Copy link
Copy Markdown
Contributor Author

Rebased with latest main to resolve git conflicts

@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch from 56028ee to 30f42ac Compare July 23, 2026 14:40

@amogh-jahagirdar amogh-jahagirdar 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.

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();

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 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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")

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.

Any reasonable way to tighten this, or is it just inherent complexity from all the null handling we need to do?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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;

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.

Does this need to move?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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;

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 I've noticed this on other PRs for TrackedFile for instance, when would fileFormat be null? It's required right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 =

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.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure, done.

This change introduces the interface for column files and also
integrates it to the schema for TrackedFile.
@gaborkaszab
gaborkaszab force-pushed the main_column_file_interface branch from 870edbd to c303670 Compare July 30, 2026 19:54
@gaborkaszab

Copy link
Copy Markdown
Contributor Author

Rebased with main to resolve conflicts

@gaborkaszab gaborkaszab left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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 =

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure, done.

class TrackingBuilder {
private final long newSnapshotId;
private final Long snapshotId;
private final Long dataSequenceNumber;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's no longer final, I moved it to the non-final section.

amogh-jahagirdar added a commit to amogh-jahagirdar/iceberg that referenced this pull request Aug 3, 2026
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>
amogh-jahagirdar added a commit to amogh-jahagirdar/iceberg that referenced this pull request Aug 3, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Iceberg V4 Iceberg Table Format Version 4

Projects

Status: In progress
Status: In review

Development

Successfully merging this pull request may close these issues.

7 participants