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.

Comment threadcore/src/main/java/org/apache/iceberg/ColumnFileInfo.java Outdated
@gaborkaszab

Copy link
Copy Markdown
ContributorAuthor

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 threadcore/src/main/java/org/apache/iceberg/ColumnFileInfo.java Outdated
Comment threadcore/src/main/java/org/apache/iceberg/ColumnFileInfo.java Outdated
Comment threadcore/src/main/java/org/apache/iceberg/TrackedFileStruct.java
Comment threadcore/src/main/java/org/apache/iceberg/ColumnFileInfo.java Outdated
@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch from ca3259e to e6f7cf6CompareMay 12, 2026 09:35
Comment threadcore/src/main/java/org/apache/iceberg/ColumnFileInfo.java Outdated
Comment threadcore/src/main/java/org/apache/iceberg/ColumnFileInfo.java Outdated
@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch 3 times, most recently from 681633b to 813d5c0CompareMay 13, 2026 12:52
@gaborkaszab

Copy link
Copy Markdown
ContributorAuthor

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
gaborkaszabforce-pushed the main_column_file_interface branch 3 times, most recently from 596f6a4 to 6a1cbe9CompareJune 2, 2026 13:12
@gaborkaszab

Copy link
Copy Markdown
ContributorAuthor

@gaborkaszabgaborkaszab changed the title Core: Introduce interface for column filesCore: Basic fields and schemas for column filesJun 3, 2026
@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch from 6a1cbe9 to c683e72CompareJune 4, 2026 06:55
@pvarypvary moved this to In review in V4: metadata treeJun 8, 2026
@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch from c683e72 to 6222fadCompareJune 8, 2026 12:32
@gaborkaszab

Copy link
Copy Markdown
ContributorAuthor

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

Comment threadcore/src/main/java/org/apache/iceberg/TrackedFileStruct.java
Comment threadcore/src/main/java/org/apache/iceberg/TrackingStruct.java
@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch from 6222fad to 5c04f55CompareJune 11, 2026 09:27
Comment threadcore/src/main/java/org/apache/iceberg/TrackedFileStruct.java Outdated
Comment threadcore/src/main/java/org/apache/iceberg/TrackingBuilder.java
@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch from 5c04f55 to b6ae446CompareJune 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
ContributorAuthor

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.

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.

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.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks for the comment suggestion! Added

@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch from b6ae446 to 0a252e8CompareJune 12, 2026 15:09
@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch from 87d43a9 to 56028eeCompareJuly 23, 2026 13:47
@gaborkaszab

Copy link
Copy Markdown
ContributorAuthor

Rebased with latest main to resolve git conflicts

@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch from 56028ee to 30f42acCompareJuly 23, 2026 14:40

@amogh-jahagirdaramogh-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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

Choose a reason for hiding this comment

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

Sure, done.

@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch 2 times, most recently from 870edbd to c303670CompareJuly 30, 2026 19:54
@gaborkaszab

Copy link
Copy Markdown
ContributorAuthor

Rebased with main to resolve conflicts

@gaborkaszabgaborkaszab left a comment

Copy link
Copy Markdown
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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
ContributorAuthor

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>
@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch from c303670 to 5d96c17CompareAugust 6, 2026 07:56

@anuragmantrianuragmantri left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should this comment be?

Suggested change
// Bumping 'dataSequenceNumber' to avoid having both equality deletes and column files.
// Clears dataSequenceNumber so it re-inherits from the manifest at read time.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we verify the dataSequenceNumber is null?

Suggested change
assertThat(withDeletedPositions.dataSequenceNumber()).isNull();

Same on L314 and in manifestPositionsWithColumnFilesUpdated() test

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done

Comment threadcore/src/main/java/org/apache/iceberg/ColumnFileStruct.java
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

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.

nit: should the comment say `ignore the unknown positions, as they must come from a newer version of the format"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

This comment is inline with the same in TrackedFileStruct and TrackingStruct. I'd rather keep consistency with these.

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.

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;

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.

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.

Comment threadcore/src/main/java/org/apache/iceberg/TrackingStruct.java
Comment threadcore/src/test/java/org/apache/iceberg/TestColumnFileStruct.java Outdated
Comment threadcore/src/test/java/org/apache/iceberg/TestColumnFileStruct.java Outdated
Tracking withDeletedPositions =
TrackingBuilder.from(manifestSourceTracking(), 999L)
.columnFilesUpdated()
.deletedPositions(deletedBytes)

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.

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?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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 ?

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

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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.

Comment threadcore/src/test/java/org/apache/iceberg/TestTrackingBuilder.java Outdated
Comment threadcore/src/test/java/org/apache/iceberg/TestTrackingStruct.java Outdated
@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch from 5d96c17 to 2bdf7bbCompareAugust 8, 2026 12:11

@gaborkaszabgaborkaszab left a comment

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks for the reviews @anuragmantri and @stevenzwu ! I believe I addressed all your comments. Would you mind taking another look?

Comment threadcore/src/main/java/org/apache/iceberg/ColumnFileStruct.java
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

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Thanks for the comment suggestion! Added

Comment threadcore/src/main/java/org/apache/iceberg/TrackingStruct.java
Comment threadcore/src/test/java/org/apache/iceberg/TestColumnFileStruct.java Outdated
Comment threadcore/src/test/java/org/apache/iceberg/TestTrackingBuilder.java Outdated
assertThat(withDeletedPositions.latestColumnFileSnapshotId()).isEqualTo(999L);
assertThat(withDeletedPositions.dvSnapshotId()).isEqualTo(999L);
assertThat(withDeletedPositions.deletedPositions()).isEqualTo(deletedBytes);

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Done

Tracking withDeletedPositions =
TrackingBuilder.from(manifestSourceTracking(), 999L)
.columnFilesUpdated()
.deletedPositions(deletedBytes)

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

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 ?

Comment threadcore/src/test/java/org/apache/iceberg/TestTrackingStruct.java Outdated

@stevenzwustevenzwu 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.

LGTM overall

@gaborkaszab

Copy link
Copy Markdown
ContributorAuthor

Thanks for the approval @stevenzwu and for the reviews @anuragmantri , @amogh-jahagirdar , @RussellSpitzer!
Are there anything else before we merge this? @rdblue Would you like to take a look yourself too?

@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch 4 times, most recently from c26a609 to dd2433cCompareAugust 26, 2026 16:11
This change introduces the interface for column files and also
integrates it to the schema for TrackedFile.
@gaborkaszab
gaborkaszabforce-pushed the main_column_file_interface branch from dd2433c to b0712b8CompareSeptember 7, 2026 11:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coreIceberg V4Iceberg Table Format Version 4

Projects

Status: In progress
Status: In review

Development

Successfully merging this pull request may close these issues.

7 participants

@gaborkaszab@anoopj@RussellSpitzer@stevenzwu@anuragmantri@amogh-jahagirdar@pvary