Skip to content

[fix](be) Compare Iceberg equality-delete keys in the wider of the delete and data file types - #69711

Merged
Gabriel39 merged 4 commits into
apache:masterfrom
morningman:b2m-t35w-v2-eq-delete-compare-type
Oct 29, 2026
Merged

Gabriel39 merged 4 commits into
apache:masterfrom
morningman:b2m-t35w-v2-eq-delete-compare-type

Conversation

@morningman

@morningman morningman commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: None

Related PR: #69536, #68267

Problem Summary:

In short. FileScannerV2 now compares an Iceberg equality-delete key in the wider of the delete file's and the data file's own key types, instead of in the one table type the FE sends for the whole scan. For some table histories that single type is wrong: the reader then deletes rows it must keep, keeps rows it must delete, or fails the scan. This follows the review of #69536, where these cases came up.

Background. An equality delete removes a data row whose key values equal a tombstone's. After a type promotion the two sides can be stored in different types: tombstones written before an INT -> LONG promotion hold INT values that must still match LONG data. #68267 made FileScannerV2 compare in the current schema's type, and for a key that the current schema no longer has, the FE sends a type recovered from the retained schemas (#69536). The reader casts both the tombstones and the data keys to that one type.

The problem, and what it cost. One type per scan cannot describe every delete file:

History Table type from the FE Delete file Data file Before After
expireSnapshots().cleanExpiredMetadata(true) removed the schema of a schema-only INT -> LONG promotion of a dropped key INT LONG 4294967296 INT, one NULL key the tombstone becomes NULL in INT and deletes the NULL-key row row kept
A read pinned before a later DATE -> TIMESTAMP_NS promotion of the key TIMESTAMP_NS DATE 1000-01-01 DATE 1000-01-01 scan fails: TIMESTAMP_NS overflow when casting row ... row deleted
No retained schema gives the key a concrete type, so the FE sends Iceberg unknown as STRING STRING TIMESTAMP_NS 2024-01-02 00:00 DATE 2024-01-02 2024-01-02 != 2024-01-02 00:00:00.000000000, the row stays visible row deleted
The delete was prepared under a TIMESTAMP_NS schema that the table later abandoned for TIMESTAMP TIMESTAMP TIMESTAMP_NS 12:00:00.000000500 TIMESTAMP 12:00:00.000000 the tombstone is rounded to microseconds and deletes the row row kept

Three of the four return wrong rows without any error. The V1 reader is not changed by this PR.

How this PR fixes it.

  • The delete block keeps the delete file's own key types. When the predicate for a data file is built, each key is compared in the wider of the delete file's type and the data file's type (equality_delete_comparison_type). The accepted widenings are the Iceberg promotions (INT -> LONG, FLOAT -> DOUBLE, decimal precision, DATE -> TIMESTAMP and DATE -> TIMESTAMP_NS), a wider fractional scale of the same date-time kind, wider integers, STRING -> VARBINARY (an Iceberg BINARY key decodes as either one, depending on enable_mapping_varbinary), and TIMESTAMP -> TIMESTAMP_NS. The last one is not an Iceberg promotion; it is accepted for the abandoned-schema case above, because scaling microseconds to nanoseconds is exact. When the table field is timestamptz, a DATETIMEV2 key (Parquet INT96, which carries no instant annotation, as Spark and Hive write it) and a TIMESTAMPTZ key compare as TIMESTAMPTZ at the larger scale, the DATETIMEV2 side converted with the session time zone as the projection converts that column. When the table field is the STRING placeholder the FE sends for an Iceberg TIME, which Doris cannot represent, an ORC TIME key (a LONG annotated iceberg.long-type=TIME, read as BIGINT) and a Parquet one (TIME(MICROS), read as TIMEV2(6)) compare as TIMEV2(6), the BIGINT side converted exactly as microseconds from midnight by a dedicated expression (CAST(BIGINT AS TIME) reads the integer's digits as HHMMSS), and a value outside 00:00:00..23:59:59.999999 fails the scan. Two types that convert in neither direction fail the scan instead of being compared in a third type.
  • The data keys are cast to that type. When it is the data file's wider type, the tombstones are widened once per scanner, and the converted delete block and its hash index are cached next to the original (_convert_equality_delete_filter). A table without type changes is compared as before, with no cast and no copy.
  • A key that the data file lacks is compared through its initial default, in the wider of the delete file's type and the table type. When that default is NULL (no initial default, or a struct default {} whose leaf has none) the key is NULL in that file whatever its table type, so it is compared in the delete file's type (an unknown key arrives as STRING). A non-NULL default behind the STRING placeholder of an Iceberg TIME is Iceberg's text of the value (00:00), with at most six fractional digits, so it is parsed exactly in the tombstones' type, TIMEV2 (TIMEV2(6) against an ORC TIME tombstone), by the strict comparison cast. A non-NULL default of a timestamptz_ns key fails the scan with NotSupported: V2 reads its tombstones as TIMESTAMPTZ(6) (Parquet floors the nanoseconds, ORC rounds them), so no parse of the nanosecond default compares exactly; the default epoch would match a tombstone 100 ns later and delete every row of the file.
  • AncestorNullDefaultExpr, which gives a missing child the NULL of its NULL parent and its default otherwise, now opens its children. Its open() returned before opening them, because prepare() had already set the flag its guard checks, so a default cast to the comparison type below it was never opened and aborted the BE with Check failed: _open_finished in Cast.
  • The comparison casts are strict whatever the session's cast mode, through a new strict flag on EvolutionCast: a non-strict overflow would turn a key into NULL and let a NULL tombstone delete it. DATE -> TIMESTAMP_NS was already strict; other EvolutionCast users are unchanged.
  • A delete file without rows is skipped before its keys are resolved: it deletes nothing, but the predicate evaluates its keys first, so its strict casts could fail the scan (a DATE key 1000-01-01 against an empty TIMESTAMP_NS delete file).
  • The table types from the FE still resolve table fields and the defaults of missing keys; the delete-file cache key is unchanged.

Results.

  • 14 new BE unit tests in iceberg_reader_test.cpp: the four histories above; INT tombstones against LONG, INT and LONG data files of one scan (the third split reuses the cached widened copy); a missing key's INT default against a LONG tombstone; a missing key without a default under an unknown table type; a microsecond key outside the nanosecond range, which must fail; and incomparable key types. With the reader before this PR, 6 of them fail; the other three pin behavior the old code shares. Removing the TIMESTAMP -> TIMESTAMP_NS widening fails 2, and making the casts non-strict fails 1. Three more came from review: an INT96 data key against an adjusted-UTC tombstone in Asia/Shanghai, a NULL leaf under a struct default {}, and an empty TIMESTAMP_NS delete file against a DATE key; removing the INT96 rule, the NULL-leaf walk or the empty-file skip fails its test. Two more came from the third round: Iceberg TIME keys across all four ORC / Parquet data and delete file pairs, with NULL and non-NULL tombstones, and a nested Iceberg TIME key added with a text default, against Parquet TIME and ORC TIME tombstones, under NULL and non-NULL parents. Removing the TIME pair rule or the default parsing, or converting the ORC TIME with a SQL cast, fails them; leaving AncestorNullDefaultExpr's children unopened aborts. One more came from the fourth round: a nested timestamptz_ns key with the defaults ...0000001, the epoch and ...0000005 fails against an epoch, a 100 ns and a 500 ns tombstone, while a key without a default still matches NULL tombstones only; parsing those defaults again fails it.
  • End to end on a local cluster: a v2 table in a file:// Hadoop catalog, built with the Iceberg 1.12.0 Java API: k INT with rows (1, NULL), (2, 0), (3, 1); k promoted to LONG without a snapshot; a delete of k = 4294967296 written under that schema; k dropped; the delete committed; then expireSnapshots().retainLast(2).cleanExpiredMetadata(true), which leaves schema 0 (k INT) and schema 2 (no k). SELECT id through FileScannerV2 returned 2, 3 before this PR and returns 1, 2, 3 with it.

branch-4.1 and branch-4.2 have the same FileScannerV2 code.

Release note

Fix FileScannerV2 comparing Iceberg equality-delete keys in a single scan-wide type, which could delete or keep the wrong rows, or fail the scan, after a key's type changed (for example a schema-only promotion removed by cleanExpiredMetadata, or a key whose retained schemas only have it as unknown).

Check List (For Author)

  • Test
    • Regression test
    • Unit Test
      • run-be-ut.sh --run --filter='IcebergV2ReaderTest.*:*EqualityDelete*:*ColumnMapper*:*EvolutionCast*:*TableReader*': 526 of 526 pass on macOS arm64 (ASAN), including the 14 new cases.
      • Rebased onto master 43f9ceb48d7: [refine](expr) Bind CAST executors for direct column execution #69639 moved the strict-mode override of EvolutionCast from prepare() into configure_cast_options(), so the new strict flag now sets CastOptions::strict_mode there, next to the existing DATE -> TIMESTAMP_NS rule. The run above is on the content before the rebase (the third-round commit applies to both unchanged); the 67 TUs that this PR changes or that master changed since 5caaeb3e3d4 were syntax-checked against the rebased tree with the UT build flags, and CI runs the suites again.
    • Manual test (add detailed scripts or steps below)
      • The local end-to-end check described under Results, with an ASAN BE built from this change.
    • No need to test or manual test. Explain why:
  • Behavior changed:
    • No.
    • Yes. FileScannerV2 compares equality-delete keys per delete file and data file; two key types that convert in neither direction now fail the scan; the comparison casts are strict.
  • Does this need documentation?
    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot 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.

Three P2 regressions remain in the equality-delete comparison change: imported INT96 timestamp keys can be rejected against normal timestamp tombstones, a missing struct default can cause a provably NULL historical key to be rejected, and an empty nanosecond delete set can cause a timestamp overflow. All three are reported inline; no existing inline findings were present to carry forward.

Reviewed all five changed files and the complete affected path: scan/split preparation -> delete-file schema and loading -> field-ID/name/history resolution -> nested extraction/defaults -> per-file comparison selection -> cached converted block/hash index -> file-local predicate -> table materialization. The comparison change belongs to table semantics and continues to use format-neutral schemas and expressions. External compatibility was checked against the Iceberg specification and reference implementation, including imported representations.

Critical checkpoint conclusions:

  • Goal and proof: per-delete/data-file widening addresses the reported scalar histories without narrowing keys. Nine new BE tests cover those histories, but the three inline cases remain uncovered and prevent considering the fix complete.
  • Scope and reuse: the four production-file changes are focused; conversion reuses EvolutionCast, existing predicates and the split cache. No unrelated source changes.
  • Concurrency and locks: the cache is shared by scanners of one scan node. Existing per-entry construction locks protect immutable results; conversion adds no lock inversion or shard-wide heavy-work lock.
  • Lifecycle and memory safety: converted predicates retain owning column pointers and a shared immutable index. Const block execution preserves original cached columns; expression contexts close through RAII, failed factories are not admitted, and split-local state resets. No new cross-translation-unit static initialization.
  • Configuration: no new option or dynamic-configuration behavior.
  • Compatibility: no storage, wire, serialized-block or dependency/linker format changes. External physical representations and historical defaults need the fixes identified inline; final binary ABI was not exercised.
  • Thrift/protobuf IDs: no gensrc changes, so cross-branch field-ID comparison is inapplicable.
  • Parallel paths: V1 remains intentionally unchanged. Other EvolutionCast users retain strict=false by default, and strictness survives cloning. Parquet/ORC and existing UUID/binary/name-mapping paths were traced.
  • Conditional branches and errors: new branches have concrete explanatory examples, and conversion/type errors propagate through Status. The missing-root default condition is semantically incomplete as described inline, and empty delete sets must be removed before comparison casts. Strict out-of-range timestamp conversion for nonempty sets deliberately fails rather than creating a NULL key.
  • Test coverage: inspected all nine additions and adjacent equality-delete, nested-default, cache and cast tests. Alternating INT/LONG splits exercise reuse; overflow and incompatible types have negative assertions. Existing composite matching supports the underlying predicate, but composite widening, conversion contention and explicit strict=true clone combinations lack focused new tests. Empty tombstone coverage is a concrete correctness gap identified inline.
  • Expected results: survivor vectors and error assertions match the scalar inputs on inspection; no regression .out files changed. No tests were executed.
  • Observability: debug output now distinguishes table and delete key types, existing error text identifies the key/data file, and newly cached indexes update the hash-index memory counter. No additional critical metric requirement was identified.
  • Transactions and persistence: this is a read-side change; snapshot/sequence applicability is supplied by existing planning. No EditLog, failover, MoW bitmap or persistence change.
  • Data writes and atomicity: no write or commit path changed.
  • FE/BE variable propagation: no new transmitted variable; the FE carrier was inspected to verify historical types and defaults.
  • Performance: unchanged keys share columns; conversion/index construction occurs once per cached comparison-type tuple rather than per row or block. No benchmark was run, so performance is assessed structurally.
  • Additional correctness: checked null masking, ancestor NULL propagation, decimal widths/scales, exact integer widening, timestamp range errors, binary hashing and cache identity. No further verified regression was found in the completed scope.

There was no additional user-provided focus. Review evidence is static only: no builds, tests, benchmarks, runtime queries, external-writer execution, source edits or submodule initialization. The new fixtures are synthesized Parquet; direct ORC widening and external Spark/Hive interoperability execution remain validation gaps. These are testing limits, not unresolved review candidates.

Review converged after 3 round(s).

to_type->get_precision() >= from_type->get_precision();
case TYPE_DATEV2:
return to_primitive == TYPE_DATETIMEV2 || to_primitive == TYPE_TIMESTAMP_NS;
case TYPE_DATETIMEV2:

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.

[P2] Resolve INT96 timestamp semantics before selecting the key type

With enable_mapping_timestamp_tz=true, an imported Spark/Hive Parquet file can expose an Iceberg timestamptz key as DATETIMEV2(6) (INT96), while a normal adjusted-UTC INT64 equality-delete file exposes the same logical key as TIMESTAMPTZ(6). Iceberg's importer explicitly treats INT96 as a zoned timestamp. For an id-only query the hidden key reaches this selector from the raw file schema, before ParquetReader::open applies projection timestamp semantics. Neither widening direction accepts this pair, so the scan now fails with NotSupported, even in UTC; previously both keys were converted to the table's TIMESTAMPTZ type and the delete applied. Resolve the carrier's timestamp semantics from the Iceberg field metadata before choosing the comparison domain, and cover imported INT96 data against ordinary timestamp tombstones.

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.

Confirmed, and fixed in 3e250364574. Parquet INT96 carries no instant annotation, so the V2 Parquet schema maps it to DATETIMEV2(6) (NativeFieldDescriptor::get_doris_type), while an adjusted-UTC INT64 delete key maps to TIMESTAMPTZ(6); neither direction of the new widening table accepts that pair, so the split failed with NotSupported where the previous code compared both sides in the table's TIMESTAMPTZ. One correction: this is not limited to id-only queries. Nothing re-types an INT96 column of an Iceberg scan at open: apply_projection_timestamp_semantics acts only on timestamp_is_adjusted_to_utc, which only the Paimon connector sets, and the key is resolved from the reader's file schema in every case, so SELECT ts failed the same way. enable_mapping_timestamp_tz is always true on master, so this was the default configuration. The fix compares a DATETIMEV2 / TIMESTAMPTZ pair in TIMESTAMPTZ, at the larger of the two scales, when the table field is timestamptz, converting the DATETIMEV2 side with the same session-time-zone cast the projection applies to that column (strict, like the other comparison casts), so a tombstone matches exactly the value a query returns. IcebergEqualityDeleteComparesInt96DataKeyAsTimestampTz reads an INT96 data file against an adjusted-UTC tombstone in Asia/Shanghai, with and without the key projected.

auto cast_expr = EvolutionCast::create_shared(filter.key_types[key_idx]);
cast_expr->add_child(default_expr);
default_expr = std::move(cast_expr);
if (missing_root.initial_default_value.has_value()) {

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.

[P2] Recognize NULL leaf defaults inside a struct default

A non-null default on missing_root does not mean the extracted key is non-null: Iceberg permits a struct default {} whose child defaults to NULL. Consider a missing payload struct containing only optional k UNKNOWN, with parent default {} and child default NULL. Promote k to DATE without a snapshot, prepare a NULL DATE equality delete, drop payload, commit the delete, then clean expired metadata while retaining the UNKNOWN snapshot. Only the historical UNKNOWN definition remains, so FE sends k as STRING. The recursive default builder correctly produces a NULL STRING leaf, but this branch sees the parent's {} and rejects DATE/STRING before NULL matching. Previously the NULL tombstone and default matched in STRING. Determine whether the extracted leaf is provably NULL, including ancestor defaults, and use the tombstone domain for that case; extend the missing-whole-struct test with this history.

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.

Confirmed in the reader, and fixed in 3e250364574. Under a struct initial default {}, a sub-field without its own default extracts as NULL, so the key should be compared in the delete file's type exactly like a missing root without a default; the branch now checks the extracted leaf instead of the root, walking the default value along the key's path with the same child matching the key expression uses. On reachability: this history cannot reach the reader through the current FE. Iceberg 1.12, which the FE uses, rejects any non-null default on a nested type (Types.NestedField.castDefault: "Invalid default value for struct ... (must be null)"), and SchemaParser cannot load "initial-default": {} on a struct (Expressions.lit has no struct literal), so such a table fails to load before planning and the FE never sends a struct initial_default_value. The BE does implement the spec's {} default (IcebergMissingWholeStructEqualityKeyMaterializesDefault), so the null-leaf case is now covered by IcebergMissingWholeStructEqualityKeyComparesNullLeafInTombstoneType.

RETURN_IF_ERROR(equality_delete_comparison_type(
filter.field_ids[idx], filter.delete_key_types[idx], key_expr->data_type(),
_data_file_path(), &comparison_type));
key_expr = cast_equality_delete_key(std::move(key_expr), comparison_type);

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.

[P2] Skip empty delete sets before constructing strict key casts

This adds a strict comparison cast even when the loaded delete block has zero rows. Iceberg accepts zero-record equality-delete descriptors, and absent optional statistics let them reach this reader. For example, retain DATE key 1000-01-01, promote it to TIMESTAMP_NS without a snapshot, prepare an empty NS delete, then drop the key, commit the delete and clean the unreferenced promotion schema. The recovered table key is DATE. With an unrelated nonempty, nonmatching delete keeping the scan's equality-delete summary positive, the empty NS filter still reaches this line and casts the DATE key to NS. EqualityDeletePredicate evaluates its children before checking the empty hash index, so the scan fails with timestamp overflow despite no tombstone to compare. Previously the data key stayed DATE and the row survived. Skip empty loaded filters before key resolution/comparison casts, and cover this case separately from the intentional nonempty NULL-tombstone overflow error.

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.

Confirmed, and fixed in 3e250364574: _append_equality_delete_predicates now skips a loaded filter whose delete block has no rows before resolving its keys, so an empty delete file adds no hidden key column, no comparison cast and no predicate. Two notes on scope. Iceberg's own writers never commit such a file: the rolling writers delete a file that received no rows (BaseTaskWriter, RollingFileWriter), and with column statistics present DeleteFileIndex prunes a zero-record equality delete against data files without NULL keys, so this needs another writer that commits an empty file without statistics, which FileMetadata allows. And the evaluation order predates this PR: EqualityDeletePredicate has always evaluated its key expressions before checking for an empty index, and on master a key whose table type is TIMESTAMP_NS already failed this way on an empty delete file through the DATE -> TIMESTAMP_NS evolution cast, which is strict in every session. The skip covers both. With a nonempty delete file, a key outside the TIMESTAMP_NS range still fails as intended. IcebergEqualityDeleteSkipsEmptyDeleteFileBeforeKeyCasts covers a present and a defaulted DATE key against an empty nanosecond delete file.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28810 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 550d861f6a9b69c5f225a3113a986ce6ea6abcc5, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17586	5162	5080	5080
q2	2134	311	249	249
q3	10261	1369	831	831
q4	4682	463	345	345
q5	7434	809	535	535
q6	183	185	153	153
q7	738	792	609	609
q8	9252	1352	1011	1011
q9	5543	4206	4163	4163
q10	6815	1292	1048	1048
q11	424	259	239	239
q12	648	399	291	291
q13	18067	3646	3046	3046
q14	267	269	245	245
q15	q16	715	723	677	677
q17	1193	753	578	578
q18	7002	6163	6113	6113
q19	1117	962	602	602
q20	393	334	227	227
q21	5256	2481	2568	2481
q22	371	328	287	287
Total cold run time: 100081 ms
Total hot run time: 28810 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5947	5902	5896	5896
q2	703	580	579	579
q3	5570	5824	5399	5399
q4	2054	2136	1571	1571
q5	5731	5783	5633	5633
q6	245	196	143	143
q7	2296	2071	1797	1797
q8	2993	2476	2482	2476
q9	7498	7443	7473	7443
q10	3643	3573	3150	3150
q11	575	416	405	405
q12	694	675	518	518
q13	3349	3653	3109	3109
q14	279	294	268	268
q15	q16	662	679	603	603
q17	7693	7018	6892	6892
q18	12995	12177	12851	12177
q19	922	819	842	819
q20	2145	2162	1917	1917
q21	5724	4763	4777	4763
q22	505	484	404	404
Total cold run time: 72223 ms
Total hot run time: 65962 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151519 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 550d861f6a9b69c5f225a3113a986ce6ea6abcc5, data reload: false

query5	4311	597	424	424
query6	440	209	195	195
query7	4805	444	244	244
query8	328	170	158	158
query9	8761	3234	3248	3234
query10	464	306	261	261
query11	6021	3855	3575	3575
query12	142	91	87	87
query13	1248	449	332	332
query14	6136	3950	3759	3759
query14_1	3436	3460	3398	3398
query15	210	196	184	184
query16	953	442	416	416
query17	864	653	543	543
query18	2435	429	332	332
query19	204	167	135	135
query20	89	85	81	81
query21	221	136	116	116
query22	12979	13004	12750	12750
query23	13455	12853	12515	12515
query23_1	12501	12424	12479	12424
query24	7424	888	503	503
query24_1	516	523	498	498
query25	546	426	375	375
query26	1260	268	154	154
query27	2720	446	279	279
query28	4605	2042	2019	2019
query29	1560	624	470	470
query30	298	219	189	189
query31	1021	932	825	825
query32	137	98	97	97
query33	549	315	249	249
query34	896	856	513	513
query35	757	753	642	642
query36	844	845	703	703
query37	123	101	85	85
query38	1764	1752	1668	1668
query39	794	794	749	749
query39_1	735	731	738	731
query40	218	121	100	100
query41	71	73	70	70
query42	90	84	87	84
query43	345	364	312	312
query44	1369	717	713	713
query45	181	175	168	168
query46	850	966	574	574
query47	3008	2681	2849	2681
query48	336	310	219	219
query49	581	395	290	290
query50	699	284	216	216
query51	10264	10546	10495	10495
query52	80	85	77	77
query53	194	206	154	154
query54	231	205	186	186
query55	73	74	64	64
query56	219	231	208	208
query57	1664	1619	1578	1578
query58	280	284	256	256
query59	2098	2162	1961	1961
query60	300	251	226	226
query61	142	150	148	148
query62	388	336	283	283
query63	189	162	156	156
query64	2731	963	770	770
query65	3328	3281	3255	3255
query66	1773	412	296	296
query67	20222	20148	20105	20105
query68	3010	1015	620	620
query69	390	292	263	263
query70	878	782	775	775
query71	300	241	208	208
query72	2714	2535	2339	2339
query73	508	519	303	303
query74	4645	4503	4340	4340
query75	2314	2293	1911	1911
query76	2316	1015	600	600
query77	337	375	284	284
query78	9784	9676	9139	9139
query79	1711	862	560	560
query80	1236	436	349	349
query81	592	314	272	272
query82	602	133	112	112
query83	303	196	179	179
query84	310	132	100	100
query85	852	430	373	373
query86	395	237	231	231
query87	1996	1950	1795	1795
query88	3881	2779	2757	2757
query89	336	276	243	243
query90	1877	175	174	174
query91	159	154	129	129
query92	98	86	91	86
query93	1148	1021	583	583
query94	629	307	239	239
query95	587	330	315	315
query96	680	491	245	245
query97	2439	2430	2293	2293
query98	162	153	150	150
query99	780	763	646	646
Total cold run time: 236193 ms
Total hot run time: 151519 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.35 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 550d861f6a9b69c5f225a3113a986ce6ea6abcc5, data reload: false

query1	0.01	0.01	0.01
query2	0.09	0.05	0.05
query3	0.26	0.08	0.09
query4	1.60	0.10	0.11
query5	0.24	0.25	0.25
query6	1.18	0.68	0.67
query7	0.04	0.00	0.00
query8	0.06	0.04	0.04
query9	0.41	0.36	0.34
query10	0.62	0.58	0.58
query11	0.20	0.15	0.13
query12	0.18	0.14	0.15
query13	0.48	0.50	0.49
query14	1.03	1.00	1.01
query15	0.63	0.61	0.63
query16	0.34	0.35	0.36
query17	1.10	1.17	1.17
query18	0.20	0.20	0.19
query19	2.07	1.99	1.98
query20	0.02	0.01	0.02
query21	15.48	0.21	0.13
query22	4.83	0.06	0.05
query23	16.28	0.32	0.13
query24	2.93	0.49	0.32
query25	0.11	0.06	0.05
query26	1.29	0.22	0.15
query27	0.06	0.05	0.05
query28	2.94	0.77	0.38
query29	12.49	4.22	3.34
query30	0.28	0.14	0.10
query31	2.78	0.53	0.32
query32	3.23	0.60	0.51
query33	3.22	3.20	3.13
query34	15.29	4.00	3.38
query35	3.36	3.33	3.34
query36	0.54	0.44	0.43
query37	0.09	0.06	0.07
query38	0.05	0.04	0.04
query39	0.04	0.03	0.03
query40	0.17	0.14	0.15
query41	0.09	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.03	0.03
Total cold run time: 96.39 s
Total hot run time: 24.35 s

morningman added a commit to morningman/doris that referenced this pull request Oct 28, 2026
…he tombstone type, and skip empty delete files

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69711

Problem Summary:

Three cases of the wider-key-type comparison of this PR:

1. Parquet INT96 carries no instant annotation, so a timestamptz key in a
   Spark or Hive file that Iceberg imported reads as DATETIMEV2(6), while an
   adjusted-UTC tombstone of the same field, as a Flink upsert writes it,
   reads as TIMESTAMPTZ(6). Neither widens to the other, so the scan failed
   with NotSupported, in any query and with the default configuration. When
   the table field is timestamptz, such a pair now compares as TIMESTAMPTZ at
   the larger scale; the DATETIMEV2 side is converted with the session time
   zone, as the projection converts that column, so a tombstone matches the
   value a query returns.
2. A missing struct whose initial default is `{}` takes each child's own
   default, so a child without one extracts as NULL. The comparison looked
   only at the root's default and rejected types that do not convert (an
   `unknown` key read as STRING against DATE tombstones) before NULL matching.
   The default is now walked down the key's path, with the key expression's
   field-id-else-name matching, and a NULL leaf is compared in the delete
   file's type.
3. A delete file without rows deletes nothing, but EqualityDeletePredicate
   evaluates its keys before it looks at the delete set, so the strict
   comparison casts still ran: a DATE key 1000-01-01 against an empty
   TIMESTAMP_NS delete file failed the scan with an overflow. Such a filter is
   now skipped before its keys are resolved; it still disables aggregate
   pushdown as before.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - New: `IcebergEqualityDeleteComparesInt96DataKeyAsTimestampTz` (INT96
      data against an adjusted-UTC tombstone in Asia/Shanghai, with and
      without the key projected),
      `IcebergMissingWholeStructEqualityKeyComparesNullLeafInTombstoneType`
      and `IcebergEqualityDeleteSkipsEmptyDeleteFileBeforeKeyCasts`.
    - `IcebergV2ReaderTest.*:*EqualityDelete*:*ColumnMapper*:*EvolutionCast*:*TableReader*`
      on macOS arm64 (ASAN): 524 of 524 pass. Removing the INT96 rule, the
      NULL-leaf walk or the empty-file skip fails its new test.
    - clang-tidy (llvm 22) on the changed lines: all checks pass.
- Behavior changed: No (only cases this PR made fail are affected)
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@morningman

Copy link
Copy Markdown
Contributor Author

Addressed the first review round in 3e250364574. Per finding:

  • 4395446119 (P2, INT96 data keys against TIMESTAMPTZ tombstones): fixed. When the table field is timestamptz, a DATETIMEV2 / TIMESTAMPTZ pair compares as TIMESTAMPTZ at the larger scale, the DATETIMEV2 side converted with the session time zone as the projection converts that column. The failure was not limited to id-only queries, see the thread.
  • 4395446127 (P2, NULL leaf under a struct default): fixed. The default is walked down the key's path, and a NULL leaf is compared in the delete file's type. The current FE cannot produce this history (Iceberg 1.12 rejects a non-null default on a nested type), so it is covered by a unit test.
  • 4395446137 (P2, empty delete files and strict key casts): fixed. A filter whose delete block has no rows is skipped before its keys are resolved. The evaluation order predates this PR: on master a TIMESTAMP_NS table key already failed this way.

BE unit tests on macOS arm64 (ASAN), IcebergV2ReaderTest.*:*EqualityDelete*:*ColumnMapper*:*EvolutionCast*:*TableReader*: 524 of 524 pass, including the three new cases; removing each fix fails its new test. clang-tidy passes on the changed lines.

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot 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.

No new actionable findings at head 3e250364574ea91a891afeb4eb5f25ccbfad2ee7. Review converged in 1 round: both complete-review agents and the separate risk reviewer returned NO_NEW_VALUABLE_FINDINGS; all candidates and risk focuses were independently verified and disposed.

Existing review context: the reported INT96 mixed-carrier selector rejection (4395446119), NULL leaf under a struct default (4395446127), and empty-delete strict-cast failure (4395446137) are addressed for their originally reported cases. No duplicates are posted. There are no existing P0/P1 findings to carry forward. No additional user focus was supplied.

Critical checkpoint conclusions:

  • Goal and evidence: equality-delete keys retain their file types and compare in an exact common domain per data file. Inspected tests cover stale/unknown table metadata, widening, nanosecond distinctions, defaults, overflow and empty deletes.
  • Scope and abstraction: the five-file change stays in table-level delete semantics with an opt-in EvolutionCast flag; physical decoding remains behind the existing reader contract.
  • Concurrency and locks: the operator-shared cache serializes construction per typed key and publishes immutable completed blocks/indexes. No new mutation race or lock-order cycle was found.
  • Lifecycle and memory: predicate ColumnPtrs and indexes own their data; the const execute overload leaves source Block references valid. Context cleanup, failed factories, split resets and task memory attachment were traced. No new ownership cycle or static-initialization dependency was found.
  • Configuration: none added.
  • Compatibility: no persisted-format or wire-layout change. Existing EvolutionCast callers retain their default behavior; cloning preserves strict mode. Unsupported comparisons and range failures propagate errors.
  • Thrift/protobuf IDs: untouched.
  • Parallel paths: native Parquet and ORC consume the same table-layer comparison logic; data keys and converted tombstones use the same cast helper. V1 is outside this targeted change.
  • Conditional checks: widening, NULL-default and empty-filter branches document concrete cases; the empty-filter skip precedes key resolution and casts.
  • Test coverage: all added tests and changed helpers were inspected, including alternating split types and cached conversion reuse. See integration limits below.
  • Test results: expected survivors and errors agree with the constructed fixtures. No regression output files changed. No builds, tests, benchmarks or runtime checks were executed in this review.
  • Observability: debug output includes table/delete key types; errors identify field/type/path as applicable, and converted index allocations update the existing memory counter.
  • Transactions and persistence: no transaction, EditLog, version or delete-bitmap persistence path changes.
  • Writes and crash atomicity: no data-write path changes.
  • FE/BE variables: no transmitted fields added; existing field IDs, schema metadata and defaults were traced through their consumers.
  • Performance: identical domains avoid conversion; widened tombstones and indexes are cached by source identity and ordered comparison types. No verified new per-row allocation or redundant deep-copy regression was found.
  • Other issues: no additional substantiated PR regression remains after the final changed-file, candidate and risk sweep.

Coverage limits: static inspection does not establish runtime correctness. The new fixtures use synthetic scan metadata and Arrow-produced Parquet; they do not provide end-to-end FE history-cleanup or representative Spark/Flink/ORC interoperability validation. The INT96 addition exercises a legacy timestamp descriptor, leaving current timestamp-contract, reverse-tombstone and DST integration coverage absent. Dedicated concurrent/multi-key conversion and decimal/time-scale fixtures are also absent. Adjacent pre-existing timestamp behavior and unsupported default histories were distinguished from new PR defects in the ledger; these test limits are not unresolved new findings.

Review converged after 1 round(s).

morningman and others added 2 commits October 28, 2026 16:00
…lete and data file types

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69536, apache#68267

Problem Summary:

**In short.** FileScannerV2 now compares an Iceberg equality-delete key in the wider of the delete file's and the data file's own key types, instead of in the one table type the FE sends for the whole scan. For some table histories that single type is wrong: the reader then deletes rows it must keep, keeps rows it must delete, or fails the scan. This follows the review of apache#69536, where these cases came up.

**Background.** An equality delete removes a data row whose key values equal a tombstone's. After a type promotion the two sides can be stored in different types: tombstones written before an INT -> LONG promotion hold INT values that must still match LONG data. apache#68267 made FileScannerV2 compare in the current schema's type, and for a key that the current schema no longer has, the FE sends a type recovered from the retained schemas (apache#69536). The reader casts both the tombstones and the data keys to that one type.

**The problem, and what it cost.** One type per scan cannot describe every delete file:

| History | Table type from the FE | Delete file | Data file | Before | After |
|---|---|---|---|---|---|
| `expireSnapshots().cleanExpiredMetadata(true)` removed the schema of a schema-only INT -> LONG promotion of a dropped key | INT | LONG 4294967296 | INT, one NULL key | the tombstone becomes NULL in INT and deletes the NULL-key row | row kept |
| A read pinned before a later DATE -> TIMESTAMP_NS promotion of the key | TIMESTAMP_NS | DATE 1000-01-01 | DATE 1000-01-01 | scan fails: `TIMESTAMP_NS overflow when casting row ...` | row deleted |
| No retained schema gives the key a concrete type, so the FE sends Iceberg `unknown` as STRING | STRING | TIMESTAMP_NS 2024-01-02 00:00 | DATE 2024-01-02 | `2024-01-02` != `2024-01-02 00:00:00.000000000`, the row stays visible | row deleted |
| The delete was prepared under a TIMESTAMP_NS schema that the table later abandoned for TIMESTAMP | TIMESTAMP | TIMESTAMP_NS 12:00:00.000000500 | TIMESTAMP 12:00:00.000000 | the tombstone is rounded to microseconds and deletes the row | row kept |

Three of the four return wrong rows without any error. The V1 reader is not changed by this PR.

**How this PR fixes it.**

- The delete block keeps the delete file's own key types. When the predicate for a data file is built, each key is compared in the wider of the delete file's type and the data file's type (`equality_delete_comparison_type`). The accepted widenings are the Iceberg promotions (INT -> LONG, FLOAT -> DOUBLE, decimal precision, DATE -> TIMESTAMP and DATE -> TIMESTAMP_NS), a wider fractional scale of the same date-time kind, wider integers, STRING -> VARBINARY (an Iceberg BINARY key decodes as either one, depending on `enable_mapping_varbinary`), and TIMESTAMP -> TIMESTAMP_NS. The last one is not an Iceberg promotion; it is accepted for the abandoned-schema case above, because scaling microseconds to nanoseconds is exact. Two types that convert in neither direction fail the scan instead of being compared in a third type.
- The data keys are cast to that type. When it is the data file's wider type, the tombstones are widened once per scanner, and the converted delete block and its hash index are cached next to the original (`_convert_equality_delete_filter`). A table without type changes is compared as before, with no cast and no copy.
- A key that the data file lacks is compared through its initial default, in the wider of the delete file's type and the table type. Without an initial default the key is NULL in that file whatever its table type, so it is compared in the delete file's type (an `unknown` key arrives as STRING).
- The comparison casts are strict whatever the session's cast mode, through a new `strict` flag on `EvolutionCast`: a non-strict overflow would turn a key into NULL and let a NULL tombstone delete it. DATE -> TIMESTAMP_NS was already strict; other `EvolutionCast` users are unchanged.
- The table types from the FE still resolve table fields and the defaults of missing keys; the delete-file cache key is unchanged.

**Results.**

- 9 new BE unit tests in `iceberg_reader_test.cpp`: the four histories above; INT tombstones against LONG, INT and LONG data files of one scan (the third split reuses the cached widened copy); a missing key's INT default against a LONG tombstone; a missing key without a default under an `unknown` table type; a microsecond key outside the nanosecond range, which must fail; and incomparable key types. With the reader before this PR, 6 of them fail; the other three pin behavior the old code shares. Removing the TIMESTAMP -> TIMESTAMP_NS widening fails 2, and making the casts non-strict fails 1.
- End to end on a local cluster: a v2 table in a `file://` Hadoop catalog, built with the Iceberg 1.12.0 Java API: `k INT` with rows (1, NULL), (2, 0), (3, 1); `k` promoted to LONG without a snapshot; a delete of `k = 4294967296` written under that schema; `k` dropped; the delete committed; then `expireSnapshots().retainLast(2).cleanExpiredMetadata(true)`, which leaves schema 0 (`k INT`) and schema 2 (no `k`). `SELECT id` through FileScannerV2 returned 2, 3 before this PR and returns 1, 2, 3 with it.

branch-4.1 and branch-4.2 have the same FileScannerV2 code.

### Release note

Fix FileScannerV2 comparing Iceberg equality-delete keys in a single scan-wide type, which could delete or keep the wrong rows, or fail the scan, after a key's type changed (for example a schema-only promotion removed by `cleanExpiredMetadata`, or a key whose retained schemas only have it as `unknown`).

### Check List (For Author)

- Test
    - [ ] Regression test
    - [x] Unit Test
        - `run-be-ut.sh --run --filter='IcebergV2ReaderTest.*:*EqualityDelete*:*ColumnMapper*:*EvolutionCast*'`: 273 of 273 pass on macOS arm64 (ASAN), including the 9 new cases.
    - [x] Manual test (add detailed scripts or steps below)
        - The local end-to-end check described under Results, with an ASAN BE built from this change.
    - [ ] No need to test or manual test. Explain why:
- Behavior changed:
    - [ ] No.
    - [x] Yes. FileScannerV2 compares equality-delete keys per delete file and data file; two key types that convert in neither direction now fail the scan; the comparison casts are strict.
- Does this need documentation?
    - [x] No.
    - [ ] Yes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he tombstone type, and skip empty delete files

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69711

Problem Summary:

Three cases of the wider-key-type comparison of this PR:

1. Parquet INT96 carries no instant annotation, so a timestamptz key in a
   Spark or Hive file that Iceberg imported reads as DATETIMEV2(6), while an
   adjusted-UTC tombstone of the same field, as a Flink upsert writes it,
   reads as TIMESTAMPTZ(6). Neither widens to the other, so the scan failed
   with NotSupported, in any query and with the default configuration. When
   the table field is timestamptz, such a pair now compares as TIMESTAMPTZ at
   the larger scale; the DATETIMEV2 side is converted with the session time
   zone, as the projection converts that column, so a tombstone matches the
   value a query returns.
2. A missing struct whose initial default is `{}` takes each child's own
   default, so a child without one extracts as NULL. The comparison looked
   only at the root's default and rejected types that do not convert (an
   `unknown` key read as STRING against DATE tombstones) before NULL matching.
   The default is now walked down the key's path, with the key expression's
   field-id-else-name matching, and a NULL leaf is compared in the delete
   file's type.
3. A delete file without rows deletes nothing, but EqualityDeletePredicate
   evaluates its keys before it looks at the delete set, so the strict
   comparison casts still ran: a DATE key 1000-01-01 against an empty
   TIMESTAMP_NS delete file failed the scan with an overflow. Such a filter is
   now skipped before its keys are resolved; it still disables aggregate
   pushdown as before.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - New: `IcebergEqualityDeleteComparesInt96DataKeyAsTimestampTz` (INT96
      data against an adjusted-UTC tombstone in Asia/Shanghai, with and
      without the key projected),
      `IcebergMissingWholeStructEqualityKeyComparesNullLeafInTombstoneType`
      and `IcebergEqualityDeleteSkipsEmptyDeleteFileBeforeKeyCasts`.
    - `IcebergV2ReaderTest.*:*EqualityDelete*:*ColumnMapper*:*EvolutionCast*:*TableReader*`
      on macOS arm64 (ASAN): 524 of 524 pass. Removing the INT96 rule, the
      NULL-leaf walk or the empty-file skip fails its new test.
    - clang-tidy (llvm 22) on the changed lines: all checks pass.
- Behavior changed: No (only cases this PR made fail are affected)
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@morningman
morningman force-pushed the b2m-t35w-v2-eq-delete-compare-type branch from 3e25036 to c313a4f Compare October 28, 2026 08:03
@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot 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.

Two new P2 findings: mixed ORC/Parquet TIME carriers reject a valid hidden equality key, and missing primitive defaults carried as STRING are rejected before a NULL physical parent can make the key NULL. The latter is verified for TIME and zoned nanosecond timestamps with all-Parquet files. Both turn previously successful id-only NULL-delete scans into errors. The inline comments describe the separate paths and fix requirements. No new P0/P1 finding was verified.

Reviewed all five changed files at c313a4f, using the supplied aggregate diff from merge base 43f9ceb. The complete path checked was FE equality schema metadata -> per-split delete-file load/cache -> physical key binding and request localization -> comparison casts/hash matching -> table materialization, including Parquet, ORC, missing/nested fields, legacy/name-based descriptors, and split reuse.

Critical checkpoint conclusions:

  • Goal and correctness: the per-file domain choice addresses the documented lossy narrowing and timestamp-precision histories. The two reported physical-carrier/default rejections are the verified compatibility exceptions. Snapshot selection, equality-field identities and position-delete/DV row coordinates are unchanged.
  • Scope and clarity: production changes stay within the equality-delete reader and an opt-in EvolutionCast strict flag, with focused tests and scenario comments.
  • Concurrency and locks: scanners share immutable cached filters; the existing cache map/per-entry locks serialize construction. Converted-filter construction does not recursively acquire the cache. No new thread or lock order was introduced.
  • Lifecycle and memory: predicates retain shared column/index owners across local filter/context destruction; const-Block execution preserves input positions. Prepared/opened local contexts close through RAII, and split preparation clears local filters. No new cross-translation-unit initializer or ownership cycle was found.
  • Configuration: no configuration item was added; strict comparison casts explicitly override the existing session cast mode.
  • Compatibility: no new storage or wire format is introduced. Existing descriptors still use the existing schema/name fallback. Physical-format interoperability is the source of the reported P2; runtime ABI/glibc compatibility was not measured in this static review.
  • Thrift/protobuf: neither gensrc/thrift nor gensrc/proto changes.
  • Parallel paths: V1 remains unchanged as scoped; other EvolutionCast callers, including Paimon precision truncation, keep their default behavior. Parquet and ORC paths were both inspected.
  • Conditional checks: empty-delete, default-leaf and timestamp branches have concrete explanations; the empty-set skip precedes key resolution and strict casts. Error Status values propagate instead of silently continuing.
  • Test coverage: all 12 new BE tests and fixture changes were inspected, covering type histories, split-cache reuse, missing/default/NULL keys, timestamp precision, overflow, incomparable types and INT96 hidden/projected keys. Mixed ORC/Parquet TIME and missing nested TIME/zoned-nanosecond-default/NULL-parent coverage are absent. No builds, tests or benchmarks were run for this review.
  • Expected results: no regression .out file changed; the new unit-test expected IDs and negative Status assertions agree with the inspected paths. This is a static conclusion, not an executed test result.
  • Observability: debug output now shows delete-file key types; existing setup timers and hash-index memory accounting cover the added setup work. No new critical logging requirement was identified.
  • Transactions and persistence: no EditLog, transaction state, failover, or persistence path changes.
  • Data writes and atomicity: no data-write or commit path changes; this PR affects read-side delete application.
  • FE-to-BE variables: no new transmitted variable; FE schema serialization and hidden-key reachability were inspected as compatibility inputs.
  • Performance: unchanged keys share columns, and widened tombstone blocks/indexes are cached per source/type tuple outside row loops. Each variant retains additional memory until operator teardown. No measured performance claim is made.
  • Other concerns: namespace/local-position reconciliation, nested ancestor NULL masking, cache failure paths, strict-mode cloning and condition-cache bypass were checked; no additional verified defect was found.

Existing review context was revalidated and deduplicated. The current head skips empty delete sets before key work (4395446137), checks the extracted default leaf (4395446127), and handles the reported mixed INT96/TIMESTAMPTZ case when table metadata identifies timestamptz (4395446119). Their targeted tests are present. All existing original findings are P2; there are no existing P0/P1 comment IDs to carry. Additional variants of those same concerns were not reposted.

User focus: no additional focus was supplied. Validation limits: static source review only, without external-writer runtime interoperability, concurrent converted-cache stress, cancellation, or memory/performance measurements. The synthetic Parquet tests do not establish those broader properties or cover the reported missing-default cases.

Convergence: false after three rounds. The final sweep verified the zoned-nanosecond extension of the missing-default defect, and the final comments separate that default path from the physical TIME-carrier path. Earlier final-round payload audits covered the preceding single-comment payload; this final two-comment set has not had a further complete no-new-findings round. No unresolved concrete candidate is being withheld. The three-round cap precludes another round; both verified P2 findings are delivered.

Review did not converge after 3 rounds. This automated review cannot pass the review check.

Three review rounds completed. The final sweep verified a zoned-nanosecond instance of the missing-default/NULL-parent defect and required splitting the prior consolidated TIME comment into two findings with separate anchors and fix scopes. The latest complete payload audits cover the preceding single-comment payload, so the final two-comment payload has not received a complete no-new-findings round from all reviewers. No unresolved concrete candidate remains; final payload-wide revalidation is the remaining coverage limit. Per the three-round cap, deliver both verified P2 findings without a fourth round.

to_primitive == TYPE_TIMESTAMP_NS;
case TYPE_TIMESTAMPTZ:
case TYPE_TIMEV2:
return to_primitive == from_primitive && to_type->get_scale() >= from_type->get_scale();

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.

[P2] Normalize ORC TIME keys before choosing the comparison domain

An id-only scan now fails for valid mixed-format equality deletes on Iceberg TIME. The Iceberg mappings store ORC TIME as LONG with iceberg.long-type=TIME, which OrcReader exposes as BIGINT, while a Parquet TIME(MICROS, false) tombstone exposes TIMEV2(6). Neither widening direction accepts this pair. For a data row (id=1, k=NULL) and a nonempty NULL tombstone on k, FE permits SELECT id and carries the hidden key as STRING; previously both nullable casts preserved NULL and the row was deleted, but this now returns NotSupported before matching. TIME being unprojectable does not prevent this hidden dependency. Normalize its physical carriers with the correct microsecond units and add mixed ORC/Parquet hidden-key coverage; an ordinary BIGINT-to-TIME SQL cast has different semantics.

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.

Confirmed, and fixed in 3c2a8e9cb87. ORC stores an Iceberg TIME as a LONG annotated iceberg.long-type=TIME, which the V2 ORC reader exposes as BIGINT, while Parquet's TIME(MICROS, isAdjustedToUTC=false) reads as TIMEV2(6); the FE carries the field, which Doris cannot represent, as a STRING placeholder, so neither widening direction applied and the split failed with NotSupported. Two corrections to the framing. It was not limited to id-only scans: the column cannot be projected at all, so the key is always hidden and every scan of such a data file failed. And the previous result only held for NULL tombstones: the old code compared the pair as two differently formatted strings (3600000001 from the ORC key, 01:00:00.000001 from the Parquet tombstone), so a non-NULL tombstone on a mixed-format TIME key was silently not applied.

When the table field is that STRING placeholder and the delete and data files hold the key as BIGINT and TIMEV2, in either order, the key now compares as TIMEV2(6). The BIGINT side is converted as microseconds from midnight by a dedicated expression (IcebergTimeMicrosExpr; TIMEV2 holds microseconds, so the conversion is exact), not by CAST(BIGINT AS TIME), which reads the integer's digits as HHMMSS, and a value outside 00:00:00..23:59:59.999999 fails the scan like the other strict comparison casts. The same conversion widens ORC tombstones against a Parquet data file. Every other pair still fails as before. IcebergEqualityDeleteComparesTimeKeysAcrossOrcAndParquet covers all four ORC / Parquet data and delete file pairs with a NULL and a non-NULL tombstone; removing the rule fails the mixed pairs with NotSupported, and a SQL cast instead of the exact conversion fails them as well.

*comparison_type = remove_nullable(filter.delete_key_types[key_idx]);
} else {
RETURN_IF_ERROR(equality_delete_comparison_type(
filter.field_ids[key_idx], filter.delete_key_types[key_idx], table_leaf->type,

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.

[P2] Preserve NULL parents when resolving a missing key's default domain

This rejects valid missing-child keys before their runtime ancestor NULL mask can apply. Write Parquet data with nullable payload and a sibling field, then add optional payload.k TIME with initial default midnight (or timestamptz_ns with initial default epoch), and apply a nonempty Parquet NULL tombstone on k to a row whose payload is NULL. FE carries these unprojectable leaf types/defaults as STRING; the tombstone reads as TIMEV2 or TIMESTAMPTZ. Because the default literal is non-NULL, this call rejects STRING versus the physical type before building AncestorNullDefaultExpr, so SELECT id fails. Previously the STRING default was masked to NULL by the parent and matched the NULL tombstone. These are valid primitive defaults, and missing old-child statistics retain the delete. Resolve the typed default without rejecting this NULL-parent path, and add an all-Parquet regression; normalizing ORC TIME alone does not fix 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.

Confirmed, and fixed in 3c2a8e9cb87. For a primitive Doris cannot represent (Iceberg TIME, timestamptz_ns) the FE sends a STRING placeholder whose initial default is Iceberg's text of the value (00:00 for a TIME midnight, 1970-01-01 00:00:00+00:00 for a timestamptz_ns epoch), so a non-NULL default compared STRING against the tombstones' physical type and the split failed before AncestorNullDefaultExpr was built. One correction: this was not limited to rows under a NULL parent, since the comparison type is chosen per data file, so every data file written before the field was added failed. And the previous result only held for NULL tombstones: the default 00:00 never matched a tombstone cast to 00:00:00.000000, so a delete of the default value was silently not applied.

A non-NULL default behind such a placeholder is now parsed in the delete file's type with the strict comparison cast: TIMEV2 or TIMESTAMPTZ, and TIMEV2(6) against an ORC TIME tombstone (BIGINT, see the other thread). Rows under a NULL parent still get NULL from AncestorNullDefaultExpr and the others the parsed value; text that does not parse fails the scan. Testing this also exposed that AncestorNullDefaultExpr::open() never opened its children (its RETURN_IF_ERROR_OR_PREPARED guard returned at once, since prepare() had set the flag), so any default cast to the comparison type below it aborted the BE with Check failed: _open_finished in Cast; it now opens them, as EqualityDeletePredicate does. On reachability: the history needs a v3 table, a nested field added with an initial default through the Iceberg Java API (Spark and Doris cannot set defaults), and an equality delete keyed on that field. IcebergMissingNestedPlaceholderKeyParsesTextDefaultInTombstoneType covers a TIME and a timestamptz_ns default under NULL and non-NULL parents, against Parquet TIME, ORC TIME and Parquet timestamptz_ns tombstones.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28361 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit c313a4f6a56b74f87b7f33a6d516df82db9e5ac1, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17634	5170	5029	5029
q2	2143	298	253	253
q3	10251	1415	810	810
q4	4684	475	337	337
q5	7430	804	534	534
q6	177	182	153	153
q7	766	813	605	605
q8	9244	1299	1060	1060
q9	5449	4230	4152	4152
q10	6835	1307	1046	1046
q11	415	249	234	234
q12	632	408	286	286
q13	18071	3618	3028	3028
q14	273	270	254	254
q15	q16	734	736	670	670
q17	1202	745	567	567
q18	6954	6172	6115	6115
q19	1090	955	600	600
q20	386	339	246	246
q21	4781	2120	2276	2120
q22	349	299	262	262
Total cold run time: 99500 ms
Total hot run time: 28361 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5557	5556	5579	5556
q2	693	573	554	554
q3	5200	5386	4857	4857
q4	1995	2179	1603	1603
q5	5256	5159	5503	5159
q6	250	195	153	153
q7	2364	2041	1871	1871
q8	2994	2657	2732	2657
q9	8040	8004	7985	7985
q10	3826	3982	3320	3320
q11	627	480	435	435
q12	701	694	549	549
q13	3517	3909	3356	3356
q14	321	332	285	285
q15	q16	692	728	664	664
q17	8222	7068	6934	6934
q18	12990	12184	12835	12184
q19	911	808	802	802
q20	2163	2159	1925	1925
q21	5616	4670	4797	4670
q22	484	444	406	406
Total cold run time: 72419 ms
Total hot run time: 65925 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151199 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit c313a4f6a56b74f87b7f33a6d516df82db9e5ac1, data reload: false

query5	4304	577	449	449
query6	413	215	191	191
query7	4812	449	224	224
query8	318	172	149	149
query9	8802	3227	3254	3227
query10	476	318	262	262
query11	5993	3848	3565	3565
query12	158	96	92	92
query13	1248	431	319	319
query14	6163	3915	3679	3679
query14_1	3396	3355	3425	3355
query15	205	198	182	182
query16	950	424	417	417
query17	845	661	539	539
query18	2428	420	311	311
query19	196	162	141	141
query20	88	84	83	83
query21	214	130	108	108
query22	12951	12997	12737	12737
query23	13418	13084	12666	12666
query23_1	12501	12557	12499	12499
query24	7399	815	473	473
query24_1	487	486	474	474
query25	533	425	365	365
query26	1257	253	146	146
query27	2772	446	280	280
query28	4632	2031	2038	2031
query29	1554	600	481	481
query30	298	222	183	183
query31	1013	927	819	819
query32	138	95	92	92
query33	536	298	251	251
query34	950	859	510	510
query35	730	771	639	639
query36	807	809	729	729
query37	131	104	83	83
query38	1752	1749	1677	1677
query39	785	832	735	735
query39_1	731	718	728	718
query40	212	112	92	92
query41	64	61	60	60
query42	87	81	78	78
query43	334	363	308	308
query44	1316	728	700	700
query45	176	175	165	165
query46	835	955	583	583
query47	3019	3031	2928	2928
query48	295	314	218	218
query49	573	392	279	279
query50	673	275	207	207
query51	10382	10154	10051	10051
query52	86	86	74	74
query53	186	204	150	150
query54	230	197	186	186
query55	76	71	70	70
query56	232	205	227	205
query57	1650	1610	1572	1572
query58	280	267	257	257
query59	2135	2164	2020	2020
query60	286	241	234	234
query61	150	151	145	145
query62	384	328	280	280
query63	185	158	157	157
query64	2724	987	831	831
query65	3339	3277	3264	3264
query66	1773	414	301	301
query67	20177	20158	20030	20030
query68	3077	1010	621	621
query69	386	311	242	242
query70	862	792	774	774
query71	302	237	220	220
query72	2729	2551	2275	2275
query73	519	523	302	302
query74	4625	4513	4338	4338
query75	2334	2278	1905	1905
query76	2309	1008	594	594
query77	339	372	278	278
query78	9894	9686	9110	9110
query79	1016	838	534	534
query80	706	415	338	338
query81	548	319	276	276
query82	571	134	109	109
query83	312	190	186	186
query84	305	121	104	104
query85	846	414	360	360
query86	363	241	230	230
query87	1995	1963	1816	1816
query88	3754	2782	2763	2763
query89	327	278	239	239
query90	1822	170	168	168
query91	156	155	125	125
query92	97	88	89	88
query93	1005	997	566	566
query94	504	311	278	278
query95	570	403	300	300
query96	680	486	243	243
query97	2468	2427	2348	2348
query98	169	156	148	148
query99	760	754	647	647
Total cold run time: 234418 ms
Total hot run time: 151199 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.66 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit c313a4f6a56b74f87b7f33a6d516df82db9e5ac1, data reload: false

query1	0.00	0.00	0.00
query2	0.09	0.04	0.04
query3	0.26	0.08	0.08
query4	1.61	0.10	0.10
query5	0.23	0.24	0.25
query6	1.16	0.68	0.67
query7	0.03	0.01	0.01
query8	0.05	0.04	0.04
query9	0.39	0.35	0.34
query10	0.60	0.58	0.56
query11	0.20	0.14	0.14
query12	0.19	0.15	0.15
query13	0.50	0.48	0.48
query14	1.01	1.03	1.01
query15	0.64	0.59	0.61
query16	0.34	0.35	0.35
query17	1.11	1.09	1.17
query18	0.21	0.19	0.19
query19	2.08	2.03	2.04
query20	0.02	0.01	0.01
query21	15.44	0.19	0.13
query22	4.92	0.05	0.06
query23	16.27	0.35	0.13
query24	2.78	0.74	0.62
query25	0.08	0.06	0.05
query26	1.67	0.22	0.15
query27	0.04	0.03	0.05
query28	2.89	0.75	0.36
query29	12.50	4.20	3.34
query30	0.28	0.13	0.09
query31	2.76	0.54	0.32
query32	3.22	0.60	0.51
query33	3.20	3.21	3.20
query34	15.48	4.02	3.38
query35	3.38	3.36	3.37
query36	0.57	0.46	0.45
query37	0.10	0.07	0.07
query38	0.05	0.04	0.04
query39	0.03	0.04	0.03
query40	0.18	0.16	0.14
query41	0.08	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.04	0.03
Total cold run time: 96.72 s
Total hot run time: 24.66 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 100.00% (5/5) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 67.08% (32389/48281)
Line Coverage 51.95% (343072/660361)
Region Coverage 47.37% (278758/588427)
Branch Coverage 49.05% (131500/268086)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (5/5) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.42% (36220/46786)
Line Coverage 62.65% (409302/653332)
Region Coverage 58.97% (345723/586299)
Branch Coverage 59.96% (160350/267434)

…n their physical types

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69711

Problem Summary: The FE sends an Iceberg primitive Doris cannot represent
(TIME, timestamptz_ns) as a STRING placeholder, with Iceberg's text form
as its initial default. Comparing equality-delete keys in the wider of the
delete file's and the data file's types rejected two valid histories with
NotSupported:

1. A TIME key across formats. ORC stores an Iceberg TIME as a LONG
   annotated iceberg.long-type=TIME, read as BIGINT, while Parquet's
   TIME(MICROS) reads as TIMEV2(6); neither widens to the other, so every
   scan of such a data file failed (the column cannot be projected, so
   the key is always hidden). Before the per-file comparison types both
   sides were compared as STRING ("3600000001" vs "01:00:00.000001"), so
   only NULL tombstones matched.
2. A missing key whose non-NULL default is the placeholder text, e.g. a
   nested TIME payload.k added with the default 00:00, against TIMEV2 or
   TIMESTAMPTZ tombstones: STRING vs the tombstone type failed before the
   NULL-parent masking was built, for every older data file.

Under a STRING table type, a BIGINT/TIMEV2 pair now compares as TIMEV2(6),
the BIGINT side converted exactly as microseconds from midnight by a new
IcebergTimeMicrosExpr (CAST(BIGINT AS TIME) reads HHMMSS digits), failing
outside 00:00:00..23:59:59.999999. Such a non-NULL default is parsed in
the tombstones' type (TIMEV2, TIMESTAMPTZ; an ORC TIME tombstone uses
TIMEV2(6)) by the strict comparison cast. Every other incomparable pair
still fails.

AncestorNullDefaultExpr::open now opens its children: the
RETURN_IF_ERROR_OR_PREPARED guard returned at once after prepare(), so a
default cast to the comparison type under a NULL-masking parent was never
opened and aborted the BE with "Check failed: _open_finished" in Cast.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - IcebergEqualityDeleteComparesTimeKeysAcrossOrcAndParquet (all four
      ORC/Parquet data/delete pairs, NULL and non-NULL tombstones) and
      IcebergMissingNestedPlaceholderKeyParsesTextDefaultInTombstoneType
      (Parquet TIME, ORC TIME and Parquet timestamptz_ns tombstones
      against a nested text default under NULL and non-NULL parents);
      278 tests in IcebergV2ReaderTest and the equality-delete, column
      mapper and evolution-cast suites pass
- Behavior changed: Yes. Such Iceberg equality deletes are applied
  instead of failing the scan.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@morningman

Copy link
Copy Markdown
Contributor Author

Addressed the third review round in 3c2a8e9cb87. Per finding:

  • 4398396743 (P2, ORC and Parquet TIME keys): fixed. Under the STRING placeholder the FE sends for an Iceberg TIME, a BIGINT (ORC) / TIMEV2 (Parquet) key pair compares as TIMEV2(6), the BIGINT side converted exactly as microseconds from midnight rather than through a SQL cast. Before this PR such a pair only matched NULL tombstones, see the thread.
  • 4398396750 (P2, a placeholder's text default): fixed. A non-NULL default behind the placeholder is parsed in the tombstones' type with the strict comparison cast. This also fixes AncestorNullDefaultExpr::open(), which never opened its children, so a default cast below it aborted the BE.

BE unit tests on macOS arm64 (ASAN), IcebergV2ReaderTest.*:*EqualityDelete*:*ColumnMapper*:*EvolutionCast*:*TableReader*: 526 of 526 pass, including the two new cases. Removing the TIME pair rule or the default parsing, or using a SQL cast for the ORC TIME, fails the new tests; leaving the children unopened aborts. clang-tidy passes on the changed lines, and the touched TUs were syntax-checked against the PR head.

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot 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.

Reviewed the complete aggregate PR at 3c2a8e9. One new P2 correctness finding: the missing timestamptz_ns default parser can collapse distinct instants and delete rows that should survive. The targeted widening repairs otherwise follow the intended per-file comparison design.

Critical checkpoint conclusions:

  1. Goal and evidence: Per-data/delete-file domains address stale table types and lossy narrowing; the 14 added BE tests cover the reported histories. Exact equality remains incomplete for fractional zoned-nanosecond defaults, as detailed inline.
  2. Scope and abstraction: Five focused files; table semantics consume format-neutral schema/block contracts. The new EvolutionCast option defaults to existing behavior for other callers.
  3. Concurrency and locks: The operator-owned scan cache is shared by scanners; per-entry locks serialize immutable filter creation. No new threads, nested cache acquisition, or lock-order regression found.
  4. Lifecycle and initialization: Predicates retain column/hash ownership; converted expressions close through VExprContext destruction. Split state resets, const materialization, ancestor NULL propagation, and child opening were traced. No new global initialization dependency or ownership cycle.
  5. Configuration: No new configuration. Comparison casts deliberately override session strictness; unrelated casts retain their defaults.
  6. Compatibility: No storage/wire layout changes. Physical Parquet/ORC carriers, timestamp zones, integer/decimal/byte domains, defaults, and legacy metadata paths were checked; the new nanos-default precision regression is inline.
  7. Thrift/protobuf: No changes; cross-branch field-ID audit is not applicable.
  8. Parallel paths: Both widening directions, present/missing/nested keys, hidden/projected keys, split reuse, mixed-format TIME, empty deletes, and shared cloud/local native reader paths were covered. V1/JNI remain independent implementations.
  9. Conditions and failure behavior: New branches document concrete scenarios; empty deletes skip key casts, invalid TIME units fail, and overflow/incomparable types propagate errors. The nanos-default cast still silently loses precision.
  10. Test coverage: Added unit tests and adjacent integration paths were inspected statically. Missing coverage includes submicrosecond zoned defaults, composite converted keys, decimal promotion cases, actual FE/external-writer histories, and INT96 with the current timestamp-semantics marker.
  11. Test results: No golden output changes. Expected surviving IDs and explicit error assertions were checked against the code; no build or test was executed in this review.
  12. Observability: Existing prepare timers and cache/hash-memory counters remain; debug output distinguishes table/delete types. No new per-row logging or actionable observability regression found.
  13. Persistence and failover: No EditLog, transaction, rowset-version, or persistence changes. Upstream snapshot/delete applicability is retained; native MoW bitmap invariants are not affected.
  14. Writes and crashes: This is a read-path change. No new write atomicity issue or independently verified crash/leak was found; incorrect read visibility is captured inline.
  15. FE/BE variables: No new transmitted field; existing schema, optionality, default, and semantics metadata remain the inputs.
  16. Performance and memory: Tombstones convert once per cached ordered type vector; unchanged columns share immutable storage. Additional converted columns/hash indexes are intentional. No separate actionable allocation, I/O, or hot-loop regression found.
  17. Other concerns: Field IDs, nested child ordinals, file-local block positions, NULL constants, mapper reconciliation, residual deletes, and final materialization were traced end to end.

Existing threads: INT96 carrier selection (4395446119), NULL struct-default leaf (4395446127), empty delete files (4395446137), mixed ORC/Parquet TIME (4398396743), and placeholder-default type rejection/child opening (4398396750) have corresponding repairs and focused tests in this head; they are not reposted. The inline finding is a distinct precision defect introduced by the placeholder parsing repair. No existing P0/P1 remains to carry in existing_blocking_comment_ids.

User focus: No additional focus was supplied; the whole PR was reviewed.

Coverage limits: Static source review only, including FE metadata producers and primary Iceberg 1.12 schema/delete-planning contracts. No builds, unit/regression tests, benchmarks, or external-writer execution were performed. Test gaps above remain validation limits, with no unresolved concrete candidate. Review converged after two rounds.

Review converged after 2 round(s).

switch (remove_nullable(delete_type)->get_primitive_type()) {
case TYPE_TIMEV2:
case TYPE_TIMESTAMPTZ:
return remove_nullable(delete_type);

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.

[P2] Preserve nanoseconds in missing timestamptz_ns defaults

In an Iceberg v3 table, write id-only Parquet data, then add optional k timestamptz_ns with initial default 1970-01-01 00:00:00.000000100+00:00, and commit a newer equality delete on k = epoch. The old file has no k statistics, so the delete reaches BE. FE preserves the default as STRING, but this branch selects the tombstone's TIMESTAMPTZ(6). parse_fractional_seconds rounds 100 ns to 0 us even in strict mode, so the default now equals the epoch tombstone and SELECT id silently loses surviving rows; previously their STRING values differed. A 500 ns default against a matching 500 ns tombstone also diverges: the default rounds to 1 us while Parquet decoding floors the tombstone to 0 us. Preserve exact nanosecond identity or explicitly reject an inexact comparison domain, and extend the placeholder-default test with submicrosecond values. This is value loss introduced by the default-parsing repair, distinct from the earlier STRING/physical-type rejection.

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.

Confirmed, and fixed in 0d70d8c3718, by rejecting this comparison rather than parsing the default more precisely, because the precision is already gone on the tombstone side. The V2 readers carry every timestamptz_ns value as TIMESTAMPTZ(6): Parquet floors the nanoseconds (parquet_timestamp_micros) and ORC rounds them half-up (round_orc_timestamp_to_microseconds). The 500 ns example is that loss, and it also hits a microsecond-exact default: with the default 1970-01-01 00:00:00+00:00, a tombstone 100 ns after the epoch reads as the epoch and deletes every row of the files written before k was added. So no parsing of the default can tell a tombstone equal to it from one in the same microsecond.

On the previous behaviour: the STRING comparison kept these rows only because it almost never matched a non-NULL tombstone. It compared Iceberg's text of the default with the tombstone cast to text, which has six fractional digits and the session's offset, so a tombstone equal to the default was silently not applied either. The previous head of this PR failed such a split with NotSupported, and the parsing added in the third round turned that error into these silent results.

A non-NULL initial default of a timestamptz_ns key (a TIMESTAMPTZ tombstone behind the STRING placeholder) now fails the split with NotSupported again, with a message naming the field and the data file. This narrows the change made for 4398396750: TIME defaults are still parsed, which is exact, since an Iceberg TIME is microseconds both in its text (at most six fractional digits) and in the files. NULL defaults are unaffected. A key the data file has still compares as TIMESTAMPTZ(6), as on master, which compares the same microsecond values through the STRING placeholder; making that exact needs a nanosecond carrier in both readers and is outside this PR. IcebergMissingPlaceholderTimestampTzNsDefaultFailsTheScan covers a nested timestamptz_ns key with the defaults ...0000001, the epoch and ...0000005 against an epoch, a 100 ns and a 500 ns tombstone, and a key without a default (a NULL tombstone still deletes, an epoch one does not).

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28552 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 3c2a8e9cb87231251e18d7f397754222db69b75e, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17605	5178	5071	5071
q2	2138	356	242	242
q3	10185	1385	844	844
q4	4688	464	337	337
q5	7443	820	525	525
q6	184	186	156	156
q7	785	795	595	595
q8	9250	1317	1095	1095
q9	5545	4222	4194	4194
q10	6823	1288	1051	1051
q11	431	253	231	231
q12	635	396	286	286
q13	18136	3676	3033	3033
q14	272	273	239	239
q15	q16	761	740	651	651
q17	1213	754	574	574
q18	7025	6184	6156	6156
q19	1264	970	636	636
q20	404	351	233	233
q21	5604	2146	2314	2146
q22	353	305	257	257
Total cold run time: 100744 ms
Total hot run time: 28552 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5616	5624	5624	5624
q2	676	585	545	545
q3	5230	5406	4928	4928
q4	2017	2142	1570	1570
q5	5268	5150	5177	5150
q6	239	184	141	141
q7	2091	1845	1696	1696
q8	2847	2449	2521	2449
q9	7554	7552	7534	7534
q10	3649	3600	3157	3157
q11	592	418	390	390
q12	682	668	512	512
q13	3329	3647	3076	3076
q14	299	303	269	269
q15	q16	669	722	619	619
q17	7773	7116	6937	6937
q18	13043	12259	13000	12259
q19	925	793	823	793
q20	2158	2162	1916	1916
q21	5686	4683	4765	4683
q22	496	470	425	425
Total cold run time: 70839 ms
Total hot run time: 64673 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151100 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 3c2a8e9cb87231251e18d7f397754222db69b75e, data reload: false

query5	4301	627	437	437
query6	429	211	187	187
query7	4793	457	223	223
query8	313	174	151	151
query9	8798	3200	3244	3200
query10	467	322	257	257
query11	6019	3866	3572	3572
query12	152	93	86	86
query13	1265	441	334	334
query14	6150	3906	3653	3653
query14_1	3426	3429	3433	3429
query15	210	209	198	198
query16	935	399	423	399
query17	858	652	534	534
query18	2423	435	309	309
query19	211	165	138	138
query20	87	85	86	85
query21	214	127	106	106
query22	13039	13143	12884	12884
query23	13550	12835	12349	12349
query23_1	12553	12655	12562	12562
query24	7386	849	484	484
query24_1	498	507	477	477
query25	533	420	366	366
query26	1258	258	141	141
query27	2741	442	276	276
query28	4620	2048	2063	2048
query29	1561	602	461	461
query30	295	218	186	186
query31	1004	927	825	825
query32	149	95	90	90
query33	527	305	244	244
query34	872	861	512	512
query35	792	770	640	640
query36	831	853	756	756
query37	130	100	87	87
query38	1865	1715	1654	1654
query39	782	809	740	740
query39_1	733	728	738	728
query40	218	115	92	92
query41	64	73	62	62
query42	88	83	81	81
query43	341	359	312	312
query44	1312	707	714	707
query45	184	168	163	163
query46	833	974	565	565
query47	3013	2852	2676	2676
query48	296	300	222	222
query49	565	409	289	289
query50	679	282	211	211
query51	10310	10281	10296	10281
query52	82	82	72	72
query53	197	211	156	156
query54	236	190	180	180
query55	74	71	64	64
query56	252	224	218	218
query57	1642	1592	1572	1572
query58	286	270	280	270
query59	2128	2199	1931	1931
query60	297	239	229	229
query61	143	148	143	143
query62	383	344	284	284
query63	187	159	155	155
query64	2575	955	768	768
query65	3313	3286	3263	3263
query66	1757	467	307	307
query67	20311	20015	20033	20015
query68	2980	1004	621	621
query69	374	294	256	256
query70	843	828	795	795
query71	307	241	209	209
query72	2676	2540	2269	2269
query73	516	533	311	311
query74	4625	4538	4320	4320
query75	2344	2289	1924	1924
query76	2311	1022	598	598
query77	334	369	261	261
query78	9717	9776	9138	9138
query79	990	856	520	520
query80	1228	434	327	327
query81	575	316	279	279
query82	597	141	115	115
query83	295	194	189	189
query84	308	128	99	99
query85	859	431	372	372
query86	411	246	232	232
query87	2003	1948	1810	1810
query88	3837	2793	2778	2778
query89	323	284	242	242
query90	1917	168	165	165
query91	163	152	127	127
query92	98	80	92	80
query93	1084	999	577	577
query94	639	312	263	263
query95	572	337	368	337
query96	701	486	239	239
query97	2459	2458	2327	2327
query98	162	149	158	149
query99	776	767	654	654
Total cold run time: 235213 ms
Total hot run time: 151100 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.38 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 3c2a8e9cb87231251e18d7f397754222db69b75e, data reload: false

query1	0.01	0.01	0.01
query2	0.10	0.05	0.04
query3	0.26	0.09	0.09
query4	1.61	0.10	0.10
query5	0.26	0.24	0.25
query6	1.17	0.69	0.68
query7	0.04	0.01	0.00
query8	0.06	0.04	0.04
query9	0.40	0.34	0.34
query10	0.62	0.57	0.60
query11	0.20	0.14	0.14
query12	0.18	0.15	0.15
query13	0.49	0.50	0.50
query14	1.03	1.03	1.02
query15	0.64	0.60	0.61
query16	0.35	0.36	0.35
query17	1.14	1.15	1.20
query18	0.21	0.19	0.20
query19	2.02	1.99	1.91
query20	0.02	0.02	0.01
query21	15.45	0.20	0.14
query22	4.90	0.05	0.05
query23	16.26	0.32	0.13
query24	3.04	0.59	0.42
query25	0.11	0.05	0.05
query26	1.22	0.21	0.16
query27	0.04	0.04	0.03
query28	2.91	0.72	0.37
query29	12.49	4.17	3.28
query30	0.27	0.14	0.10
query31	2.77	0.55	0.31
query32	3.22	0.60	0.53
query33	3.16	3.24	3.18
query34	15.32	4.00	3.37
query35	3.34	3.38	3.36
query36	0.55	0.43	0.42
query37	0.09	0.07	0.06
query38	0.05	0.04	0.03
query39	0.03	0.03	0.03
query40	0.18	0.15	0.14
query41	0.09	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.04	0.03
Total cold run time: 96.38 s
Total hot run time: 24.38 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 100.00% (5/5) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 67.12% (32448/48344)
Line Coverage 51.99% (343590/660882)
Region Coverage 47.38% (279055/588914)
Branch Coverage 49.10% (131773/268352)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (5/5) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.41% (36264/46849)
Line Coverage 62.69% (409917/653853)
Region Coverage 59.12% (346927/586786)
Branch Coverage 59.99% (160601/267700)

…-delete keys

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69711

Problem Summary: A data file written before an Iceberg timestamptz_ns
column k was added does not store k, so an equality delete keyed on k
compares the column's initial default. The FE sends timestamptz_ns as a
STRING placeholder with the default as Iceberg's nanosecond text, and the
previous commit parsed that text in the tombstones' type, TIMESTAMPTZ(6).
That cannot compare exactly: V2 reads the tombstones as TIMESTAMPTZ(6),
Parquet flooring and ORC rounding the nanoseconds, and the strict cast
rounds the 7th fractional digit of the default half up. With Parquet
tombstones, the default 1970-01-01 00:00:00.0000001+00:00 matched an
epoch tombstone and deleted every row of such a file, the default epoch
matched a tombstone 100 ns later, and the default ...0000005 missed its
own tombstone. Before that commit this history failed the scan with
NotSupported.

A non-NULL timestamptz_ns default behind the STRING placeholder fails the
scan with a dedicated NotSupported again. TIME defaults (at most six
fractional digits, exact in TIMEV2(6)) are still parsed; NULL defaults
and keys the data file stores are unchanged.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - BE UT IcebergV2ReaderTest (116 tests) pass. New
      IcebergMissingPlaceholderTimestampTzNsDefaultFailsTheScan: the three
      defaults above fail with NotSupported, and a key without a default
      still matches NULL tombstones only; it fails when the defaults are
      parsed again. The timestamptz_ns case of
      IcebergMissingNestedPlaceholderKeyParsesTextDefaultInTombstoneType
      is removed.
- Behavior changed: Yes. Such scans fail instead of deleting rows by an
  inexact comparison.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@morningman

Copy link
Copy Markdown
Contributor Author

Addressed the fourth review round in 0d70d8c.

  • P2, a STRING-placeholder timestamptz_ns default compared to microsecond tombstones (r4401112878): fixed by failing the split with NotSupported again for a non-NULL timestamptz_ns default of a key the data file does not store. V2 reads such tombstones as TIMESTAMPTZ(6) (Parquet floors the nanoseconds, ORC rounds them), so no parse of the default compares exactly; the third round's parsing could delete every row of an older file for a tombstone 100 ns after the default. TIME defaults, which are exact in microseconds, are still parsed; NULL defaults and stored keys are unchanged. New BE UT IcebergMissingPlaceholderTimestampTzNsDefaultFailsTheScan.

The description is updated to match.

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot 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.

Static review of PR #69711 at 0d70d8c3718eb82ab5cf884c1e715e877333ad17: no new actionable finding. All five changed files and the affected schema-to-delete-predicate path were reviewed. Both complete-review agents and the dedicated risk reviewer returned NO_NEW_VALUABLE_FINDINGS; the main reviewer independently checked their evidence. Review converged in one round.

Critical checkpoint conclusions:

  1. Goal and correctness: per-data-file comparison domains avoid narrowing delete keys into the scan-wide table type. Numeric, decimal, date/nanosecond, binary, TIME, timestamp, missing/default and NULL paths were traced. Promotion/default checks used the Iceberg specification.
  2. Scope and reuse: changes stay within the Iceberg V2 reader, its tests, and an opt-in EvolutionCast strict flag; existing cast and expression interfaces are reused.
  3. Concurrency and locks: split progression is sequential per scanner. FileScanOperatorX shares the cache across scanner instances; per-entry initialization is synchronized, shard-map locking is short, and raw/converted cache lookups do not nest locks. Published blocks and indexes remain immutable.
  4. Lifecycle and memory: predicates retain owning column/index references; temporary conversion contexts close through RAII; split teardown releases request contexts. Clone preserves strict options, and default expressions now open their children. No new global initialization dependency was found.
  5. Configuration: none added. Existing timezone/type-mapping settings remain shared scan inputs; strict comparison casts do not change other callers' defaults.
  6. Compatibility: no on-disk, serialized block or wire-format change. Incomparable domains fail explicitly. Non-NULL missing timestamptz_ns defaults deliberately return NotSupported because their tombstones have already lost nanosecond precision.
  7. Thrift/Protobuf IDs: no gensrc changes; cross-branch field-ID comparison is not applicable.
  8. Parallel paths: Parquet and ORC V2 carriers, hidden/projected keys, missing roots/children, and cached/raw filters were checked. V1 is outside this reader-local fix; Paimon's existing EvolutionCast truncation behavior is preserved.
  9. Special conditions: new branches explain concrete failures, including empty deletes, TIME microsecond carriers, default precision, strict overflow and child-expression opening. Errors propagate as non-OK Status rather than creating NULL matches.
  10. Test coverage: inspected all 14 added tests and nearby existing mapping, nullable-parent, UUID/binary, timestamp and cache tests. They cover the stated histories, mixed widths/splits, empty deletes, missing defaults, TIME format pairs, overflow and rejected domains. No builds or tests were executed under this static-only contract.
  11. Expected results: inspected survivor IDs and error assertions agree with the traced domains. No generated result files were changed.
  12. Observability: debug output includes delete-file key types; conversion misses update existing index-memory accounting; new errors identify the field/file or expression. No additional logging issue was found.
  13. Persistence and failover: no EditLog, metadata persistence or transaction-state changes.
  14. Writes and atomicity: this is read-side delete application; commit/write atomicity is unchanged. Snapshot/delete eligibility remains upstream.
  15. FE-BE propagation: no new transmitted variable; existing field IDs, schema/default metadata and mapping options are consumed.
  16. Performance: equal types avoid conversion; widened tombstones and their index are cached per original filter and ordered comparison-type vector. Extra retained variants are a deliberate memory cost. No benchmark or runtime memory measurement was performed.
  17. Additional risks: checked nullable constants, COW ownership, multi-key ordering, failed conversion admission, cache identity and teardown. No distinct introduced defect was substantiated.
  18. User focus: review_focus.txt contains no additional focus points.

Existing review context: all six original inline findings are P2, with no existing P0/P1 to carry as a blocker. The INT96 type-pair rule, NULL-leaf default walk, empty-delete skip, mixed-format TIME conversion, and TIME default/ancestor/open fixes are present. The latest nanosecond-default finding is addressed by explicit failure; this intentionally narrows the earlier placeholder-default fix, including files whose parents happen to be NULL. These concerns were not reposted.

Validation limits: the added INT96 test covers legacy plans without the current FE timestamp-semantics marker. The current-plan session-timezone mismatch also exists in the merge-base implementation, so it is a pre-existing compatibility limitation rather than a new finding. Dedicated mixed-key conversion, concurrent construction, conversion-failure retry and late-refresh tests are absent; those paths were traced statically. External-engine interoperability, sanitizer/runtime behavior and performance were not exercised. No unresolved introduced candidate or unreviewed changed-file scope remains.

Review converged after 1 round(s).

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28735 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 0d70d8c3718eb82ab5cf884c1e715e877333ad17, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17588	5172	5043	5043
q2	2116	351	257	257
q3	10204	1389	822	822
q4	4681	459	336	336
q5	7432	797	526	526
q6	185	184	153	153
q7	743	795	597	597
q8	9258	1331	1088	1088
q9	5520	4197	4163	4163
q10	6816	1276	1034	1034
q11	421	250	226	226
q12	633	398	279	279
q13	18118	3634	3025	3025
q14	272	276	246	246
q15	q16	759	718	665	665
q17	1222	754	601	601
q18	7136	6200	6113	6113
q19	1263	961	625	625
q20	398	341	230	230
q21	6019	2444	2434	2434
q22	388	323	272	272
Total cold run time: 101172 ms
Total hot run time: 28735 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5988	5933	6092	5933
q2	747	605	571	571
q3	5639	5875	5319	5319
q4	2008	2176	1581	1581
q5	5838	5705	5697	5697
q6	244	189	146	146
q7	2204	1964	1763	1763
q8	3008	2667	2654	2654
q9	7745	7482	7500	7482
q10	3653	3562	3129	3129
q11	598	418	387	387
q12	690	666	499	499
q13	3266	3550	3056	3056
q14	300	291	258	258
q15	q16	665	693	618	618
q17	7799	7045	6916	6916
q18	12961	12352	12976	12352
q19	998	838	841	838
q20	2173	2174	1928	1928
q21	5685	4612	4749	4612
q22	486	469	401	401
Total cold run time: 72695 ms
Total hot run time: 66140 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151496 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 0d70d8c3718eb82ab5cf884c1e715e877333ad17, data reload: false

query5	4319	609	434	434
query6	428	210	189	189
query7	4804	448	235	235
query8	313	172	149	149
query9	8798	3295	3292	3292
query10	443	328	266	266
query11	5891	3832	3573	3573
query12	152	92	88	88
query13	1253	448	312	312
query14	6101	3954	3674	3674
query14_1	3448	3403	3424	3403
query15	203	195	178	178
query16	952	416	403	403
query17	835	656	526	526
query18	2413	412	311	311
query19	186	155	129	129
query20	83	82	81	81
query21	207	131	112	112
query22	12946	12935	12747	12747
query23	13339	12774	12549	12549
query23_1	12471	12498	12485	12485
query24	7407	811	473	473
query24_1	479	477	489	477
query25	511	392	349	349
query26	1252	262	142	142
query27	2750	444	280	280
query28	4654	2100	2112	2100
query29	1576	619	459	459
query30	303	230	187	187
query31	1006	936	814	814
query32	158	94	92	92
query33	537	308	241	241
query34	984	860	522	522
query35	740	805	655	655
query36	830	850	716	716
query37	124	102	86	86
query38	1764	1726	1685	1685
query39	793	806	734	734
query39_1	729	726	745	726
query40	228	119	98	98
query41	79	71	66	66
query42	91	83	84	83
query43	347	364	314	314
query44	1368	724	731	724
query45	188	179	168	168
query46	842	971	580	580
query47	2961	3050	2587	2587
query48	313	319	234	234
query49	586	401	305	305
query50	692	281	231	231
query51	10251	10482	10463	10463
query52	90	84	77	77
query53	200	210	155	155
query54	246	213	205	205
query55	75	73	70	70
query56	250	236	231	231
query57	1616	1600	1551	1551
query58	292	278	301	278
query59	2131	2196	2003	2003
query60	290	256	236	236
query61	168	162	160	160
query62	392	338	285	285
query63	196	164	167	164
query64	2896	1011	770	770
query65	3335	3285	3266	3266
query66	1771	428	296	296
query67	19829	20073	20127	20073
query68	2996	1022	627	627
query69	387	295	242	242
query70	885	802	759	759
query71	302	237	210	210
query72	2764	2577	2272	2272
query73	535	540	306	306
query74	4599	4486	4338	4338
query75	2333	2281	1921	1921
query76	2327	1028	633	633
query77	344	375	275	275
query78	9708	9685	9116	9116
query79	1020	862	550	550
query80	1175	448	341	341
query81	589	321	269	269
query82	578	142	107	107
query83	326	202	183	183
query84	313	133	102	102
query85	872	436	382	382
query86	381	246	232	232
query87	2027	1937	1815	1815
query88	3832	2831	2833	2831
query89	332	282	240	240
query90	1884	174	164	164
query91	167	151	130	130
query92	103	90	91	90
query93	1026	1011	577	577
query94	604	344	265	265
query95	572	392	297	297
query96	696	493	244	244
query97	2443	2431	2315	2315
query98	164	151	151	151
query99	765	767	640	640
Total cold run time: 234751 ms
Total hot run time: 151496 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.36 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 0d70d8c3718eb82ab5cf884c1e715e877333ad17, data reload: false

query1	0.01	0.00	0.01
query2	0.10	0.05	0.05
query3	0.26	0.09	0.09
query4	1.62	0.11	0.10
query5	0.25	0.24	0.24
query6	1.16	0.67	0.68
query7	0.04	0.01	0.00
query8	0.06	0.04	0.04
query9	0.40	0.33	0.34
query10	0.62	0.61	0.59
query11	0.19	0.15	0.14
query12	0.18	0.15	0.15
query13	0.50	0.49	0.50
query14	1.01	1.01	1.03
query15	0.63	0.62	0.60
query16	0.36	0.33	0.33
query17	1.14	1.19	1.13
query18	0.20	0.19	0.20
query19	2.10	2.06	1.96
query20	0.02	0.01	0.01
query21	15.42	0.21	0.14
query22	4.79	0.06	0.05
query23	16.25	0.34	0.12
query24	2.91	0.50	0.41
query25	0.09	0.07	0.05
query26	1.19	0.22	0.16
query27	0.05	0.04	0.04
query28	3.11	0.74	0.38
query29	12.48	4.20	3.30
query30	0.29	0.13	0.10
query31	2.77	0.55	0.32
query32	3.21	0.59	0.50
query33	3.22	3.17	3.15
query34	15.20	3.94	3.40
query35	3.32	3.33	3.34
query36	0.54	0.43	0.46
query37	0.09	0.07	0.06
query38	0.04	0.03	0.04
query39	0.05	0.04	0.04
query40	0.18	0.14	0.15
query41	0.08	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.03	0.03
Total cold run time: 96.21 s
Total hot run time: 24.36 s

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 100.00% (5/5) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 67.12% (32449/48344)
Line Coverage 51.99% (343616/660890)
Region Coverage 47.42% (279272/588918)
Branch Coverage 49.10% (131776/268356)

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 100.00% (5/5) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.46% (36289/46849)
Line Coverage 62.71% (410021/653861)
Region Coverage 59.20% (347400/586790)
Branch Coverage 60.06% (160794/267704)

@Gabriel39
Gabriel39 merged commit 978d5c8 into apache:master Oct 29, 2026
45 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

PR approved by anyone and no changes requested.

@github-actions github-actions Bot added the approved Indicates a PR has been approved by one committer. label Oct 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

PR approved by at least one committer and no changes requested.

Gabriel39 pushed a commit that referenced this pull request Nov 2, 2026
…Iceberg table type (#69845)

### What problem does this PR solve?

Issue Number: None

Related PR: #69711

Problem Summary:

**In short.** An Iceberg `timestamptz` column stored as Parquet INT96,
the encoding Spark and Hive use for timestamps, reads shifted by the
session time zone's offset in any non-UTC session: in Asia/Shanghai,
2024-01-01T00:00:00Z reads as `2024-01-01 00:00:00+08:00`, eight hours
early, while Iceberg itself reads `2024-01-01 08:00:00+08:00`. Equality
deletes on such a key miss their rows for the same reason. This PR has
the FE mark each `timestamptz` field of the scan's schema dictionary,
and FileScannerV2 then decodes the matching Parquet columns of data
files and equality-delete files as instants, which is how Iceberg reads
them. It came up in the review of #69711.

**Background.**

- Parquet INT96 has no logical annotation, so a reader cannot tell from
the file whether an INT96 column holds an instant or a wall clock. Spark
writes its `timestamp` type (an instant) as INT96 by default, and
`add_files`, `migrate` and `snapshot` attach such files to Iceberg
tables unchanged, as `timestamptz` columns.
- Iceberg does not look at the file annotation at all: its Parquet
readers choose the timestamp reader from the table type, so a
`timestamptz` field reads INT96, and INT64 with or without the UTC
annotation, as an instant.
- The FE sends `parquet_timestamp_semantics_version = 1` for every file
scan. Parquet scans therefore always use FileScannerV2, and INT96
decodes as a UTC wall clock (`DATETIMEV2`) unless the table format
supplies the semantics. Paimon has the same ambiguity (TIMESTAMP and
TIMESTAMP_LTZ share INT96) and solves it with
`timestamp_is_adjusted_to_utc` on each field of its schema dictionary,
which `PaimonReader` applies to the file schema. The Iceberg FE never
set that field.

**The problem, and what it cost.** For a `timestamptz` column, the
projection casts the INT96 wall clock to `TIMESTAMPTZ` in the session
time zone, so the instant moves by the session offset. Reproduced on
master with an INT96 file attached like `add_files` and an
Iceberg-written INT64 file holding the same instants, both in one table:

| Session | Rows | Iceberg Java | Before | After |
|---|---|---|---|---|
| UTC | INT96 and INT64 | 2024-01-01 00:00:00 UTC | correct | correct |
| Asia/Shanghai | INT64 (written by Iceberg) | `2024-01-01
08:00:00+08:00` | correct | correct |
| Asia/Shanghai | INT96 (written by Spark) | `2024-01-01 08:00:00+08:00`
| `2024-01-01 00:00:00+08:00`, 8 hours early | `2024-01-01
08:00:00+08:00` |

The result is the same with field ids in the file, with only the table's
name mapping, whatever the catalog's `enable.mapping.timestamp_tz` says
(master always maps `timestamptz` to TIMESTAMPTZ), and with
`enable_file_scanner_v2` on or off. The same shift breaks equality
deletes, which compare the projected values:

| Equality delete on a `timestamptz` key, non-UTC session | Before |
After |
|---|---|---|
| INT96 data key, Iceberg-written tombstone of the same instant | the
tombstone misses the row | the row is deleted |

Wrong values and missed or extra deletes come back without any error.

**How this PR fixes it.**

- FE: `IcebergSchemaUtils.buildField` sets `timestamp_is_adjusted_to_utc
= true` on every `timestamptz` field, at every nesting level; the
dictionary also carries the hidden equality-delete keys. `timestamp`
fields stay unmarked: marking them `false` would make BE read the
Parquet statistics of an adjusted-UTC INT64 file as UTC wall clocks
while its rows still decode as session wall clocks, so row-group pruning
could drop matching rows.
- BE, data files: `IcebergTableReader::annotate_file_schema` copies the
marker onto the Parquet file column that `TableColumnMapper` will select
for each field, using the mapper's own matchers
(`find_column_by_field_id` / `find_column_by_name`) after the positional
fallback ids are stamped, so field ids, the table's name mapping and
positional ids all work. Like the mapper, it accepts an ID-less wrapper
at every depth when `allow_idless_complex_wrapper_projection` is on. It
then rewrites the column type with the helper `PaimonReader` already
used, moved to `schema_history_util` as
`apply_file_timestamp_semantics`. The mapping, the scan request and
`ParquetReader` all see `TIMESTAMPTZ`, and INT96 decodes as an instant.
Only a `DATETIMEV2` or `TIMESTAMPTZ` leaf takes the marker; a
`TIMESTAMP_NS` leaf keeps its nanoseconds. Columns without a marked leaf
keep the types the file reader reported, and a plan from an older FE
carries no marker and reads as before.
- BE, equality deletes: three kinds of column are read without a
`ColumnMapping`: a hidden key column, the parent struct that is read for
the NULLs of a nested key the data file lacks, and the key columns of an
equality-delete file. Their projections now carry the markers of the
annotated file column (`equality_delete_root_projection`, built on
`attach_timestamp_semantics`, which the mapper now exports). Without
them Parquet would decode an INT96 leaf as `DATETIMEV2` under a
`TIMESTAMPTZ` file schema, and the read would fail its type check. That
holds even when the query projects the column, because reading it as a
predicate column replaces the projection its mapping marked.
Equality-delete files get the same annotation, so tombstones and data
keys of a `timestamptz` field are both instants. The `DATETIMEV2` /
`TIMESTAMPTZ` comparison rule from #69711 stays for plans from older
FEs.
- Refactor: `find_current_schema` and
`build_schema_identity_from_external_field` (now also carrying the
marker) move to `schema_history_util`; `TableReader`'s four copies of
the current-schema lookup use the helper.

**Results.**

- FE unit test
`IcebergSchemaUtilsTest#timestamptzFieldsCarryInstantMarkerAtEveryLevel`:
top-level, struct, list and map `timestamptz` fields are marked;
`timestamp` and `timestamp_ns` fields are not, under both values of the
mapping flag.
- 5 new BE unit tests in `iceberg_reader_test.cpp`, all failing without
the BE change: the file-schema annotation (field ids at every level,
`TIMESTAMP_NS` untouched, ORC untouched); INT96 `timestamptz` in
Asia/Shanghai with field ids, name mapping and positional ids, with and
without the marker; INT96 inside a struct and a list; an INT96 equality
key against an Iceberg tombstone, hidden or projected; and the DST
fall-back above. Leaving the delete files unannotated fails the DST case
(the second tombstone deletes the first row); projecting a hidden key or
a tombstone without its marker fails both equality-delete cases with a
read error; marking a `TIMESTAMP_NS` leaf fails the annotation case.
With neither data nor delete files annotated (the reader before this
PR), the INT96 key deletes nothing and the DST case deletes both rows.
The existing case for an older FE's plan (no marker) still passes.
- 2 more BE unit tests from review:
- INT96 under a nested ID-less wrapper struct, read and used as an
equality key. It fails when the walk accepts the wrapper only at the
root.
- A nested equality key missing from an older file whose parent struct
holds an INT96 `timestamptz`. It checks the survivors with the parent
hidden and projected, that the projected timestamp reads as an instant
in Asia/Shanghai, and a plan without the marker. It fails with the
reader's type check when the parent is read without the marker.
- End to end on a local cluster: an Iceberg table holding an INT96
Parquet file attached like `add_files` (one table with field ids in the
file, one relying on `schema.name-mapping.default`) and an
Iceberg-written INT64 file with the same instants, read through three
catalogs (`enable.mapping.timestamp_tz` unset, `true`, `false`), with
`enable_file_scanner_v2` on and off, in UTC and Asia/Shanghai: 24
combinations. Before this PR the INT96 rows were 8 hours early in every
Asia/Shanghai combination (8 of the 16 measured); with it the INT96 rows
equal the INT64 rows in all 24, and the INT64 rows and the UTC results
are unchanged.

branch-4.1 and branch-4.2 have the same code.

**Classes, and how they call each other.**

- `IcebergSchemaUtils` (FE): builds the schema dictionary; now marks
`timestamptz` fields.
- `schema_history_util`: `find_current_schema`,
`build_schema_identity_from_external_field`, and
`apply_file_timestamp_semantics` (moved from `PaimonReader`).
- `IcebergTableReader`: `annotate_file_schema` →
`_apply_positional_id_fallback` (the existing logic) and
`_apply_table_timestamp_semantics`; `_append_equality_delete_predicates`
(hidden keys), `_build_missing_equality_delete_key_expr` (the parent of
a missing nested key) and `_load_equality_delete_file` (tombstones)
project their columns with the markers through
`equality_delete_root_projection`.
- `column_mapper`: `attach_timestamp_semantics` is exported.

```
FE  IcebergSchemaUtils.buildField  --timestamptz-->  TField.timestamp_is_adjusted_to_utc = true
                                                     (history_schema_info, current_schema_id = -1)
BE  TableReader::open_reader
      +-- IcebergTableReader::annotate_file_schema                     (Parquet only)
      |     +-- _apply_positional_id_fallback
      |     +-- _apply_table_timestamp_semantics
      |           +-- find_current_schema, build_schema_identity_from_external_field
      |           +-- find_column_by_field_id | find_column_by_name    (the mapper's matchers)
      |           +-- apply_file_timestamp_semantics                   (shared with PaimonReader)
      +-- TableColumnMapper: mapping/projection take the marker from the file column
      +-- ParquetReader::open: apply_projection_timestamp_semantics -> INT96 decodes as TIMESTAMPTZ
    IcebergTableReader::_append_equality_delete_predicates
      +-- equality_delete_root_projection -> attach_timestamp_semantics   (hidden key)
      +-- _build_missing_equality_delete_key_expr
            +-- equality_delete_root_projection                           (parent of a missing key)
    IcebergTableReader::_load_equality_delete_file -> _apply_table_timestamp_semantics
                                                   + equality_delete_root_projection (tombstones)
```

ORC files are not changed: ORC's file type already separates `TIMESTAMP`
from `TIMESTAMP_INSTANT`.

### Release note

Fix Iceberg `timestamptz` columns stored as Parquet INT96 (written by
Spark or Hive and attached with `add_files`, `migrate` or `snapshot`)
reading shifted by the session time zone's offset in non-UTC sessions,
and equality deletes on such keys missing their rows.

### Check List (For Author)

- Test <!-- At least one of them must be included. -->
    - [ ] Regression test
    - [x] Unit Test
- BE: `run-be-ut.sh --run
--filter='IcebergV2ReaderTest.*:Paimon*ReaderTest.*:Hudi*Test.*:TableReaderTest.*:ColumnMapper*:FlussUnionLakeReaderTest.*:EqualityDeletePredicateTest.*:*PositionDelete*'`:
532 of 532 pass on macOS arm64 (ASAN), including the 5 new cases. After
the first review round, `IcebergV2ReaderTest` passes 122 of 122,
including the new nested ID-less wrapper case. It fails when the walk
accepts the wrapper only at the root. After the second review round, the
whole filter passes 534 of 534 and `IcebergV2ReaderTest` 123 of 123,
including the new missing nested key case. It fails when the missing
key's parent is read without the marker. They ran on a local base; the
commit was then applied to master `2a4191c1dc9` without conflicts, and
the changed sources and their test files were syntax-checked against
that tree with the UT build flags.
- FE: `mvn -pl :fe-connector-iceberg -am package
-Dtest=IcebergSchemaUtilsTest`: 35 of 35 pass, checkstyle passes.
    - [x] Manual test (add detailed scripts or steps below)
- The local end-to-end check under Results, with an ASAN BE and the
Iceberg connector built from this change.
    - [ ] No need to test or manual test. Explain why:
- Behavior changed:
    - [ ] No.
- [x] Yes. Parquet columns of Iceberg `timestamptz` fields decode as
instants by the table type, as Iceberg reads them: INT96 no longer
shifts by the session offset, an INT64 timestamp without the UTC
annotation under a `timestamptz` field reads its value as UTC, and
equality deletes on `timestamptz` keys compare instants. Plans from an
older FE are unchanged.
- Does this need documentation?
    - [x] No.
- [ ] Yes. <!-- Add document PR link here. eg:
apache/doris-website#1214 -->

### Check List (For Reviewer who merge this PR)

- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label <!-- Add branch pick label that this PR
should merge into -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Gabriel39 pushed a commit that referenced this pull request Nov 4, 2026
### What problem does this PR solve?

Issue Number: #69782

Related PR: #69711

Problem Summary:
`NestedStructFieldExpr::open()` reuses the prepare guard and skips
opening its child. Use the normal open lifecycle, completing the nested
traversal fix after #69711.

### Release note

Fix initialization of nested struct expressions in FileScannerV2 Iceberg
equality deletes.

### Check List (For Author)

- Test
- [x] Unit Test: added coverage for cast defaults, nullable ancestors,
and cloned contexts.
- Behavior changed:
    - [x] Yes. Open nested expression children before execution.
- Does this need documentation?
    - [x] No.

### Check List (For Reviewer who merge this PR)

- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
github-actions Bot pushed a commit that referenced this pull request Nov 4, 2026
### What problem does this PR solve?

Issue Number: #69782

Related PR: #69711

Problem Summary:
`NestedStructFieldExpr::open()` reuses the prepare guard and skips
opening its child. Use the normal open lifecycle, completing the nested
traversal fix after #69711.

### Release note

Fix initialization of nested struct expressions in FileScannerV2 Iceberg
equality deletes.

### Check List (For Author)

- Test
- [x] Unit Test: added coverage for cast defaults, nullable ancestors,
and cloned contexts.
- Behavior changed:
    - [x] Yes. Open nested expression children before execution.
- Does this need documentation?
    - [x] No.

### Check List (For Reviewer who merge this PR)

- [ ] Confirm the release note
- [ ] Confirm test cases
- [ ] Confirm document
- [ ] Add branch pick label
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by one committer. backport/4.2-master reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants