[fix](be) Correct FileScannerV2 Iceberg delete handling#65502
[fix](be) Correct FileScannerV2 Iceberg delete handling#65502Gabriel39 wants to merge 7 commits into
Conversation
### What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary: FileScannerV2 kept rows matched by delete predicates because it applied delete expressions with ordinary predicate polarity. Iceberg equality deletes also rejected fields absent from older data files instead of treating them as NULL, and delete files shared by multiple splits were parsed repeatedly. Invert delete predicate results into a keep filter, resolve equality-delete keys through field ids or name mappings with typed NULL literals for absent fields, honor position bounds, and cache parsed equality and position delete data through the split cache.
### Release note
Fix Iceberg V2 row-level delete filtering across schema evolution and reuse parsed delete files across splits.
### Check List (For Author)
- Test: Unit Test
- 26 focused EqualityDeletePredicate, JSON materialized-reader, and Iceberg V2 delete tests passed
- Iceberg schema-evolution regression coverage was updated; execution was skipped because the validation environment disables external Iceberg tests
- Behavior changed: Yes, FileScannerV2 now applies Iceberg equality and position deletes with correct semantics
- Does this need documentation: No
|
Thank you for your contribution to Apache Doris. Please clearly describe your PR:
|
|
/review |
|
run buildall |
There was a problem hiding this comment.
Automated review completed after two rounds: main risk scan, normal coverage subagents, risk-focused subagents, and a convergence round all ended with the final M1-M5 set and NO_NEW_VALUABLE_FINDINGS after that set stabilized. I found five issues that should be fixed before merge.
Checkpoint conclusions:
- Scope/user focus: Reviewed the authoritative diff for the changed Iceberg V2 delete, reader, and test paths. No additional user-provided focus was supplied.
- Correctness/schema compatibility: Issues found in equality-delete row sizing and BY_NAME schema-evolution binding.
- Lifecycle/cache: Issue found in shared delete-file cache scoping across split filesystem identities.
- Tests: Added tests cover useful positive paths but miss the accepted edge cases, and one regression leaks session state on failure.
- Validation: Static review only; per the review prompt I did not build, run tests, or modify source.
.worktree_initialized,thirdparty/installed, andthirdparty/installed/bin/protocare absent in this checkout.
There was a problem hiding this comment.
Automated review completed after two rounds: main risk scan, normal coverage subagents, risk-focused subagents, and a convergence round all ended with the final M1-M5 set and NO_NEW_VALUABLE_FINDINGS after that set stabilized. I found five issues that should be fixed before merge.
Checkpoint conclusions:
- Scope/user focus: Reviewed the authoritative diff for the changed Iceberg V2 delete, reader, and test paths. No additional user-provided focus was supplied.
- Correctness/schema compatibility: Issues found in equality-delete row sizing and BY_NAME schema-evolution binding.
- Lifecycle/cache: Issue found in shared delete-file cache scoping across split filesystem identities.
- Tests: Added tests cover useful positive paths but miss the accepted edge cases, and one regression leaks session state on failure.
- Validation: Static review only; per the review prompt I did not build, run tests, or modify source.
.worktree_initialized,thirdparty/installed, andthirdparty/installed/bin/protocare absent in this checkout.
| // Migrated Hive tables may have no field ids in their physical files. Once the mapper has | ||
| // selected BY_NAME mode, bind the delete key by its delete-file name or by the table column's | ||
| // historical name mapping, exactly as normal column materialization does for a renamed field. | ||
| const auto table_field_it = std::ranges::find_if( |
There was a problem hiding this comment.
This lookup makes equality-delete binding depend on the query projection. _projected_columns comes from the scan's required slots, so a query that reads only data or count(*) does not necessarily include the equality key's table metadata. For an old field-id-less file with physical legacy_id, a current table field current_id with name_mapping = legacy_id, and an equality delete file for field id 0 named current_id, the test where current_id is projected works, but SELECT data ... reaches this code with no table field for id 0. The fallback then treats the key as a missing column and uses a NULL literal, so a non-null delete key like current_id = 2 will not delete the row. Equality-delete key binding needs to use table schema/history metadata independent of the selected columns, or add the delete key as a hidden mapping before resolving the data file field.
Superseded by review 4680092049 after repairing inline anchors.
### What problem does this PR solve? Issue Number: None Related PR: apache#65502 Problem Summary: Follow-up review found that equality-delete evaluation could derive a zero batch size from an unread first block column, BY_NAME equality-key binding depended on query projection and did not share the mapper's complete name semantics, and parsed delete-file caches were not isolated by filesystem identity. It also found that the Iceberg regression suite could leak its FileScannerV2 session override after a failure. Derive predicate row count from populated block columns, resolve hidden equality keys from the complete current table schema through the common BY_NAME matcher, include fs_name in both delete cache keys, and restore the session variable in a finally block. ### Release note Fix Iceberg V2 equality and position delete handling for lazy reads, schema evolution, and multi-filesystem scans. ### Check List (For Author) - Test: Unit Test / Regression test - `EqualityDeletePredicateTest.*:IcebergV2ReaderTest.Iceberg*Delete*` (29 tests) - Iceberg schema-evolution regression suite loaded successfully; external Iceberg data execution was disabled by the validation environment - Behavior changed: Yes, V2 delete filtering now uses reliable batch sizing, complete schema name mapping, and filesystem-scoped caches - Does this need documentation: No
|
run buildall |
|
/review |
There was a problem hiding this comment.
Automated review completed after three rounds, including main-agent risk scan, full-review subagents, risk-focused subagents, candidate verification, duplicate suppression against existing threads, and final convergence.
I found three additional blocking issues in the current head:
- Missing equality-delete keys are always compared as NULL, which does not preserve Iceberg initial-default/default semantics for old files.
- Equality-delete cache keys are still structurally ambiguous when raw paths are concatenated with colon-prefixed field ids.
IcebergTableReader::prepare_split()now readsSplitReadOptions::cacheunconditionally even though the pointer has no default initializer.
Checkpoint conclusions: data correctness is still at risk for equality deletes in the missing/defaulted-column and cache-collision cases; lifecycle/test determinism is at risk for the uninitialized cache pointer; concurrency did not show a distinct new issue; position-delete grouping/bounds and delete-conjunct polarity were rechecked without additional non-duplicate findings; no configuration, persistence, FE-BE protocol, or observability change requires a separate comment here.
User focus: no additional user-provided focus was supplied.
Validation: static review only. I did not build or run tests, per the review instructions for this GitHub Actions review environment.
### What problem does this PR solve? Issue Number: None Related PR: apache#65502 Problem Summary: Iceberg V2 equality delete handling could treat fields missing from old data files as NULL even when the field has a non-null initial default. Delete-file cache keys also allowed structural collisions between raw paths and appended equality field ids, and split cache pointers could be read before initialization. Carry Iceberg initial defaults through FE and BE schema metadata, use them for missing equality keys, length-encode cache key components, and initialize the split cache pointer. The existing lazy-batch row-count fix prevents the attached equality-delete filter-size coredump. ### Release note Fix Iceberg V2 equality and position delete correctness across schema evolution and cached splits. ### Check List (For Author) - Test: Regression test / Unit Test - ./build.sh --be --fe - EqualityDeletePredicateTest.*:IcebergV2ReaderTest.Iceberg*Delete* (30 passed) - ExternalUtilTest, IcebergUtilsTest (25 passed) - Behavior changed: Yes, missing Iceberg fields use their initial defaults and delete cache keys are structurally isolated - Does this need documentation: No
|
run buildall |
|
/review |
|
run buildall |
|
Codex automated review failed and did not complete. Error: You've hit your usage limit. Visit https://chatgpt.com/codex/settings/usage to purchase more credits or try again at 5:19 PM. Please inspect the workflow logs and rerun the review after the underlying issue is resolved. |
TPC-H: Total hot run time: 29931 ms |
TPC-DS: Total hot run time: 180574 ms |
ClickBench: Total hot run time: 24.98 s |
BE Regression && UT Coverage ReportIncrement line coverage Increment coverage report
|
FE Regression Coverage ReportIncrement line coverage |
### What problem does this PR solve? Issue Number: N/A Related PR: apache#65502 Problem Summary: The Iceberg equality-delete schema-change regression test indexed the SQL string literal before invoking the sql helper. Groovy therefore attempted to take index 1 from a one-character string and failed before the test queries ran. Store the SQL result rows first, validate that the variable exists, and then extract the session value for restoration. ### Release note None ### Check List (For Author) - Test: Regression test - Remote minimal regression test covering the same SQL-result extraction passed - Behavior changed: No - Does this need documentation: No
|
run buildall |
|
/review |
There was a problem hiding this comment.
Static review of PR 65502 at head daf0c5006176be580f7599e606fcc4eaaf1d6283. I found one remaining blocker, left inline.
Checkpoint conclusions: goal/scope is clear and the remaining issue is patch-scoped to the new Iceberg initial-default carrier. Concurrency/lifecycle/cache behavior was rechecked against the existing threads and current patch; I found no additional non-duplicated cache or split-lifetime issue. Compatibility and parallel paths were checked for scanner V2 versus fallback, Parquet/ORC delete handling, thrift optionality, and generated outputs; no additional issue found beyond the default-carrier problem. Condition checks and delete-predicate polarity/row sizing were rechecked across BE consumers; no additional issue found. Tests cover integer defaults and several BE delete paths, but miss the timestamp and VARBINARY-mapped default cases called out inline. Observability is not materially changed. Transaction/persistence/data-write boundaries are not directly changed; FE-BE schema/default transport is the relevant boundary and is where the blocker sits. Performance risk appears bounded to the new delete/default handling paths; no extra performance finding. Static review only per prompt; I did not build or run tests.
TPC-H: Total hot run time: 29535 ms |
TPC-DS: Total hot run time: 180849 ms |
ClickBench: Total hot run time: 25.15 s |
### What problem does this PR solve? Issue Number: None Related PR: apache#65502 Problem Summary: Iceberg equality-delete keys can be absent from older data files after schema evolution. Both scanner implementations treated such keys as NULL even when Iceberg assigned a non-null initial default, and the default carrier was not type-safe for timestamp and VARBINARY-mapped values. This change sends complete current schema metadata, normalizes initial defaults by type, materializes missing keys in FileScanner V1, and builds typed missing-key expressions in FileScanner V2. It preserves Iceberg type promotion when the table and delete-file key types differ. ### Release note Fix Iceberg equality deletes for evolved columns with initial defaults in FileScanner V1 and V2. ### Check List (For Author) - Test: Unit Test - Remote BE ASAN build - 3 focused Iceberg BE unit tests - IcebergUtilsTest#testParseSchemaPreservesInitialDefault - C++ format check - Behavior changed: Yes, both file scanners now apply equality deletes using the logical Iceberg initial value for columns absent from older files - Does this need documentation: No
|
run buildall |
|
/review |
### What problem does this PR solve? Issue Number: close #0 Related PR: apache#65502 Problem Summary: Format the missing Iceberg equality-delete key helper with the project clang-format 16 style. ### Release note None ### Check List (For Author) - Test: No need to test (format-only change); clang-format 16 verification passed - Behavior changed: No - Does this need documentation: No
|
/review |
|
run buildall |
There was a problem hiding this comment.
Automated static review for PR 65502 at live head 7c234e2ae69228b430977711dbcac3e6a1d3e448.
I found four remaining blocking issues and left inline comments.
Checkpoint conclusions:
- Scope/user focus: reviewed the authoritative Iceberg delete/default/schema-change diff; no additional user-provided focus was supplied.
- Correctness/schema compatibility: remaining issues affect BY_NAME equality-delete binding, binary-like initial defaults, V1 ORC missing-key materialization, and TIMESTAMPTZ equality-delete semantics.
- Lifecycle/cache/row-count: rechecked split cache keys, failed-load behavior, position bounds, delete polarity, and row-count carriers; no additional non-duplicate finding beyond existing threads.
- Tests: current tests miss the accepted edge cases called out inline.
- Validation: static review only; per review prompt I did not build, run tests, or modify source. Live head differs from the local checkout only by a formatting-only change in
build_missing_equality_delete_key_expr, which does not affect these findings.
| return field.has_identifier_field_id() && | ||
| field.get_identifier_field_id() == field_id; | ||
| }); | ||
| if (field_it != _data_reader.file_schema.end() || |
There was a problem hiding this comment.
This still lets a partial-id migrated file bypass BY_NAME binding. mapping_mode() intentionally switches the split to BY_NAME when any data column lacks an Iceberg field id, so normal scan columns are resolved by ColumnMapper's name/identifier/alias rules. This helper, however, first accepts any file column whose field id equals the equality key id and returns before it checks the mode. If an imported file has the real key column matched only by name_mapping and some other physical column carrying the stale key id, the delete predicate compares against that unrelated column and misses or misapplies equality deletes. Please skip the id fast path when mapping_mode() == BY_NAME and resolve hidden equality keys through the same BY_NAME matcher as normal materialization.
| bytes.putLong(uuid.getLeastSignificantBits()); | ||
| return Base64.getEncoder().encodeToString(bytes.array()); | ||
| } | ||
| return humanValue; |
There was a problem hiding this comment.
This still sends the wrong representation for binary-like defaults when varbinary mapping is disabled. In that mode UUID/BINARY become Doris STRING and FIXED becomes CHAR, so the BE branch that Base64-decodes TYPE_VARBINARY never runs; the STRING/CHAR serde keeps this humanValue text as-is. Equality delete files, however, read BYTE_ARRAY/FIXED_LEN_BYTE_ARRAY keys as raw bytes into string columns. For example an added BINARY default 00 01 02 ff is carried as "AAEC/w==", but the delete key column contains raw bytes, so the missing-key literal does not match the delete row. Please normalize UUID/BINARY/FIXED defaults to the same raw-byte representation for the STRING/CHAR mapping as well, or carry enough type metadata for BE to decode them before building the literal.
| if (i < _expand_columns.size()) { | ||
| _expand_columns[i].name = table_col_name; | ||
| } | ||
| if (it == field_id_to_file_col_name.end()) { |
There was a problem hiding this comment.
This missing-key branch registers a constant for the equality key, but the ORC path still treats the same synthetic column as physically present. After this branch runs, the loop still appends table_col_name to ctx->column_names and add_children() marks it with exists=true. ORC therefore does not put it in _fill_missing_cols; _init_file_column_mapping() adds it to _read_file_cols, and the batch read later returns Wrong read column because the old ORC file has no such physical column. Please leave the hidden equality key in the block/default-materialization path without marking it as an existing ORC child, and add an end-to-end V1 ORC missing-key equality-delete test.
| EqualityDeleteFilter* result) { | ||
| DORIS_CHECK(result != nullptr); | ||
| std::unique_ptr<format::FileReader> reader; | ||
| RETURN_IF_ERROR(_create_delete_file_reader(delete_file, scan_params, delete_io_ctx, &reader)); |
There was a problem hiding this comment.
The reader created here needs the same TIMESTAMPTZ mapping as the data reader. Normal TableReader construction passes enable_mapping_timestamp_tz into Parquet/ORC readers, but _create_delete_file_reader() uses those constructors' default false, so adjusted-UTC timestamp keys in equality-delete files are loaded as DATETIMEV2. The data side may still be TIMESTAMPTZ and then gets cast to the delete key type through the session timezone before _equal() compares raw column values. Around a DST fold, two different instants can collapse to the same local DATETIMEV2 and an equality delete for one instant can delete rows for the other. Please pass the effective timestamp-tz mapping flag into V2 delete-file readers and add equality-delete coverage with enable_mapping_timestamp_tz=true in a non-UTC timezone.
|
Codex automated review failed and did not complete. Error: Codex completed, but no new pull request review was submitted for the current head SHA. Please inspect the workflow logs and rerun the review after the underlying issue is resolved. |
There was a problem hiding this comment.
Automated static review for PR 65502 at head 7c234e2ae69228b430977711dbcac3e6a1d3e448.
I found one remaining non-duplicated blocking issue and left it inline. During live duplicate suppression, two verified findings were not resubmitted because current-head comments 3568056557 and 3568056554 already cover the V1 ORC missing-key path and the enable.mapping.varbinary=false binary-like default representation issue.
Checkpoint conclusions:
- Scope/user focus: reviewed the authoritative bundle diff and changed-file list; no additional user-provided focus was supplied.
- Correctness/schema compatibility: one remaining issue affects Iceberg timestamp-with-zone initial-default normalization when
enable.mapping.timestamp_tz=false. - Lifecycle/cache/row-count: rechecked split cache keys, cache failure behavior, delete polarity, row-count carriers, and V1/V2 paths; no additional non-duplicate finding remains beyond existing live comments.
- Tests: current tests miss
TimestampType.withZone()initial defaults with the default timestamp-tz mapping disabled. - Validation: static review only per prompt. I did not build or run tests.
.worktree_initialized,thirdparty/installed, andthirdparty/installed/bin/protocare absent in this checkout;git diff --checkproduced no output.
| if (type.typeId() == TypeID.TIMESTAMP) { | ||
| // Iceberg formats timestamps as ISO-8601 (for example 2024-01-01T00:00:00), while | ||
| // Doris' DATETIMEV2 default parser requires a space between the date and time. | ||
| return humanValue.replace('T', ' '); |
There was a problem hiding this comment.
This still misses the timestamp-with-zone branch when enable.mapping.timestamp_tz is left at its default false. parseSchema() maps TimestampType.shouldAdjustToUTC() to Doris DATETIMEV2 unless that flag is enabled, but serializeInitialDefault() only knows about enableMappingVarbinary and just replaces T in the Iceberg human string. Iceberg timestamptz human strings carry an offset, so a value like 2024-01-01 00:00:00.123456+00:00 is later parsed by BE through the restored DATETIMEV2 type. DataTypeDateTimeV2SerDe::from_olap_string() expects YYYY-MM-DD HH:MM:SS[.ffffff] and falls back to MIN_DATETIME_V2 on parse failure, so old files missing this equality key compare against the wrong default and matching deletes can be missed. Please pass enableMappingTimestampTz into this normalization and emit the offset-free Doris datetime representation when the Iceberg field is timestamptz but Doris maps it to DATETIMEV2; also add coverage for TimestampType.withZone() with enable.mapping.timestamp_tz=false.
### What problem does this PR solve? Issue Number: None Related PR: apache#65502 Problem Summary: Iceberg equality-delete keys can be hidden from the query projection, absent from older data files, or mapped by historical names. V1 ORC attempted to read absent keys as physical columns, BY_NAME resolution could select stale field IDs, and binary or timestamptz initial defaults could lose their physical representation. Preserve complete schema metadata, resolve BY_NAME keys only by names and aliases, delay missing-key materialization until the batch row count is known, preserve binary defaults through an explicit Base64 marker, and inherit timestamp-with-zone mapping for delete readers. ### Release note Fix Iceberg equality-delete filtering across schema evolution, binary mappings, ORC files, and timestamp-with-zone values. ### Check List (For Author) - Test: - Unit Test: 65 focused BE Iceberg and equality-delete tests passed - Unit Test: 25 focused FE schema and Iceberg utility tests passed - Behavior changed: Yes, equality deletes now use correct schema mappings and initial-default representations - Does this need documentation: No
|
/review |
|
run buildall |
There was a problem hiding this comment.
I found four blocking correctness/compatibility issues in the Iceberg equality-delete schema-evolution changes.
Critical checkpoint conclusions:
- Goal/test proof: the goal is to preserve equality-delete correctness across evolved schemas and missing keys. Current tests cover many default and V2 cases, but they do not cover the four reported cases.
- Scope/focus: the change is broadly focused on this feature, but the FE schema/default metadata source and V1 hidden-key helper paths need tightening.
- Concurrency/lifecycle: no new thread or lock issue found. The V2 delete-file cache lifecycle/keying was rechecked and is covered by current code or existing comments.
- Compatibility/protocol: MCF-4 is a blocking FE/BE rolling/pruned-descriptor compatibility gap. Optional Thrift fields otherwise look compatible.
- Parallel paths: V2 has fallbacks that V1 lacks; V1 multi-key and name-mapping hidden-key paths diverge from the corrected V2 behavior.
- Error handling, memory, nullability: MCF-2 is a const-column/nullability materialization issue, and MCF-4 is a hard CHECK on descriptor shape. No additional memory-safety issue found.
- Data correctness: MCF-1, MCF-2, and MCF-3 can leave equality-deleted rows visible.
- Observability/config/persistence/writes: no new config, persistence, or write-path concern found.
- Tests: missing coverage remains for snapshot binary-like defaults, V1 multi-key missing/default keys, V1 id-less/name-mapped hidden keys, and V1 pruned-descriptor compatibility.
Review focus: no additional user-provided focus was supplied.
Validation: review-only per prompt; no builds or tests were run. Round 3 convergence completed with all subagents returning NO_NEW_VALUABLE_FINDINGS against this comment set.
| // historical names, types, and initial defaults when an old data file lacks such a key. | ||
| ExternalUtil.initSchemaInfoForAllColumn( | ||
| params, -1L, source.getTargetTable().getColumns(), nameMapping, | ||
| IcebergUtils.getBase64EncodedInitialDefaultFieldIds(icebergTable.schema())); |
There was a problem hiding this comment.
This uses the selected scan columns from source.getTargetTable().getColumns(), but computes the Base64-default field-id set from icebergTable.schema(), which is the table's latest schema. Time-travel/branch/tag scans load the requested snapshot into StatementContext; IcebergExternalTable.getFullSchema() then comes from that snapshot schema via IcebergUtils.getIcebergSchema(). If the selected snapshot still has a UUID/BINARY/FIXED field with an initial default that is absent or changed in the latest schema, ExternalUtil will send the default string without initial_default_value_base64_encoded. BE then treats the Base64 text as the literal string/VARBINARY default instead of decoding to the raw equality key bytes, so missing-key equality deletes can be missed. Please derive the Base64 field-id set from the same Iceberg schema/schema id used to build the scan columns, or carry this encoding bit with each parsed Column, and add snapshot/branch coverage for a binary-like initial default.
| [&](const ColumnWithTypeAndName& col) { return col.name == name; }); | ||
| DORIS_CHECK(expand_col != _expand_columns.end()); | ||
| (*this->col_name_to_block_idx_ref())[name] = block->columns(); | ||
| block->insert({value->clone_resized(rows), expand_col->type, name}); |
There was a problem hiding this comment.
This leaves the synthesized missing-key column as a ColumnConst. That works for single-column equality deletes because SimpleEqualityDelete expands consts before probing, but the V1 multi-key path hashes data columns directly in MultiEqualityDelete::filter_data_block() and then compares them directly in _equal(). ColumnConst does not implement the batch update_hashes_with_value() method and its compare_at() expects the RHS to also be const, while delete-file columns are materialized normal columns. A delete file on (id, added_key) against an old data file where added_key is logically the initial default will fail before it can remove matching rows. Please insert a full column here (or normalize consts in MultiEqualityDelete) and add a V1 multi-column equality-delete test with one absent/defaulted key.
| _expand_columns[i].name = table_col_name; | ||
| } | ||
|
|
||
| if (it == field_id_to_file_col_name.end()) { |
There was a problem hiding this comment.
At this point ctx->table_info_node has already been built with the Iceberg name mapping and can resolve id-less files by alias, but the hidden equality-key remap ignores it and only probes field_id_to_file_col_name. For migrated Parquet/ORC files without field IDs, projected columns are still readable through *_field_id_with_name_mapping(), while an unprojected equality-delete key falls into this branch and gets materialized as NULL/default. A delete on a renamed field such as table current_id mapped to physical legacy_id can therefore leave matching rows visible. Please resolve the hidden key through the same name-mapping node before treating it as missing, and add V1 coverage for an unprojected equality key in an id-less/name-mapped file.
| break; | ||
| } | ||
| } | ||
| DORIS_CHECK(table_field != nullptr); |
There was a problem hiding this comment.
This new path assumes the FE scan descriptor always carries the full current schema. That is true for the new IcebergScanNode, but older/pruned descriptors used initSchemaInfoForPrunedColumn() and only included projected slots, so an unprojected equality-delete key can be absent from history_schema_info during a rolling upgrade or with an older descriptor. V2 handles that case by falling back to projected metadata or the delete-file name/type when full schema metadata is unavailable; V1 reaches this DORIS_CHECK(table_field != nullptr) and aborts before it can apply even the NULL/default-free semantics. Please add the same compatibility fallback or return a controlled non-crashing error for missing schema metadata, and cover a pruned-descriptor V1 missing-key case.
FE UT Coverage ReportIncrement line coverage |
What problem does this PR solve?
Issue Number: None
Related PR: None
Problem Summary:
FileScannerV2 had three related Iceberg delete correctness and efficiency gaps:
This change builds an explicit keep filter by inverting delete matches, resolves equality keys by field id or name mapping and supplies typed NULL literals for missing fields, honors position bounds, and caches parsed delete data in
SplitReadOptions.cachefor reuse across splits. The implementation is limited to FileScannerV2.Release note
Fix Iceberg V2 row-level delete filtering across schema evolution and reuse parsed delete files across splits.
Check List (For Author)
EqualityDeletePredicate, JSON materialized-reader, and Iceberg V2 delete tests passed