Skip to content

feat: support data evolution table mode#193

Merged
XiaoHongbo-Hope merged 13 commits into
apache:mainfrom
JingsongLi:data_evolution
Apr 2, 2026
Merged

feat: support data evolution table mode#193
XiaoHongbo-Hope merged 13 commits into
apache:mainfrom
JingsongLi:data_evolution

Conversation

@JingsongLi
Copy link
Copy Markdown
Contributor

Purpose

Supports https://paimon.apache.org/docs/master/append-table/data-evolution/

Sub task of #173

Brief change log

Tests

API and Format

Documentation

Copy link
Copy Markdown
Contributor

@QuakeWang QuakeWang left a comment

Choose a reason for hiding this comment

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

@JingsongLi Hi, I have reviewed the pr, just leave some minor comments.


Ok(try_stream! {
for split in &splits {
if split.raw_convertible() || split.data_files().len() == 1 {
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.

The raw-convertible / single-file branch yields read_single_file() directly, but this helper does not reorder columns back to projected_column_names after ProjectionMask, unlike the existing read() path.

Parquet returns file-schema order, not request order, so a projection like ["value", "id"] will come back as ["id", "value"] for single-file splits. That makes this path inconsistent with the current read() behavior, and it can also make the new projection test flaky when the plan contains single-file groups.

Should we preserve the same reorder logic here so that the data-evolution raw path matches the normal read path?

let mut column_source: HashMap<String, (usize, i64)> = HashMap::new();

for (file_idx, file_meta) in data_files.iter().enumerate() {
let file_columns: Vec<String> = if let Some(ref wc) = file_meta.write_cols {
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.

Falling back to “all columns from the current table schema” when write_cols == None is not correct here.

One of the main data-evolution scenarios is reading old files after the table has added new columns. In that case, an old file only contains fields from the schema identified by file_meta.schema_id; using the current table schema here will incorrectly mark later-added columns as if this file already provided them. That affects winning-column resolution, and it can also drop entire row groups when the projection only contains newly added columns.

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 cannot get your point. Please raise an example.

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 cannot get your point. Please raise an example.

My point is about old files after schema evolution.

If an old file was written when the table schema was (id, name), and later the table becomes (id, name, age), then write_cols == None should not mean that this old file contains age too. But the current fallback uses the current table schema, so it will treat the old file as if it also provides the newly added columns.

I think for write_cols == None, we should use the file schema from file_meta.schema_id, not the current table schema.

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.

The old file can be treaded as (id, name, age), the age is null. Actually, this is a schema evolution.

I don't want to introduce the reading of old schema files because it should address issues such as column type changes.

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.

The old file can be treaded as (id, name, age), the age is null. Actually, this is a schema evolution.

I don't want to introduce the reading of old schema files because it should address issues such as column type changes.

@JingsongLi I get your point. I agree we do not necessarily need to introduce historical schema reading here, especially if that expands the scope to type evolution.

My concern is mainly that, even under the “current schema + NULL for missing columns” semantics, the current fallback may still behave incorrectly in some cases. For example, when projecting only a newly added column, I think this path may return 0 rows instead of preserving the row count and filling NULLs.

So my point is less about requiring old-schema reads, and more about whether the current fallback already implements the expected schema-evolution behavior correctly.

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.

81665a1
You mean these three lines code? We should remove it.

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.

Yes, those three lines are part of the issue, so removing them makes sense.

I just want to make sure this is sufficient: for the add-column case, if the projected column does not physically exist in the old files, we should still preserve the row count and fill NULL, instead of dropping rows.

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 found it quite troublesome to fill in NULL, and we also need the corresponding Arrow type, which we don't have here, so the current implementation doesn't have a corresponding column. In the future, when we need to support schema evolution, we will see how to change it.

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 found it quite troublesome to fill in NULL, and we also need the corresponding Arrow type, which we don't have here, so the current implementation doesn't have a corresponding column. In the future, when we need to support schema evolution, we will see how to change it.

OK, that makes sense to me.

Comment thread crates/paimon/src/table/table_scan.rs Outdated
if let Some(files) = data_deletion_files {
builder = builder.with_data_deletion_files(files);
if data_evolution_enabled {
let file_groups = split_by_row_id(data_files);
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.

The split-generation logic here diverges from upstream DataEvolutionSplitGenerator.

Upstream does not simply group by equal first_row_id and emit one split per group. It first merges overlapping row_id_ranges, then applies ordered bin packing using target_split_size/open_file_cost, and computes rawConvertible from the packed result. The current implementation introduces two regressions:

  1. source.split.* is effectively bypassed in data-evolution mode, so splits become much more fragmented.
  2. Grouping only by first_row_id misses the overlapping row-id-range case, which no longer matches upstream grouping semantics.

I think we should align this with the Java split generator before merging the new read path.

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.

Packaging different rowids together makes the implementation very complex, which requires re grouping during read, which is not necessary for Rust.

@JingsongLi JingsongLi changed the title feat: support data evolution table mode [WIP] feat: support data evolution table mode Apr 2, 2026
@JingsongLi JingsongLi changed the title [WIP] feat: support data evolution table mode feat: support data evolution table mode Apr 2, 2026
Copy link
Copy Markdown
Contributor

@QuakeWang QuakeWang left a comment

Choose a reason for hiding this comment

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

+1

@XiaoHongbo-Hope
Copy link
Copy Markdown
Contributor

+1

@XiaoHongbo-Hope XiaoHongbo-Hope merged commit cd3670c into apache:main Apr 2, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants