Skip to content

[fix](fe) Resolve dropped Iceberg equality-delete keys after a schema ID is reused - #69536

Merged
morningman merged 6 commits into
apache:masterfrom
morningman:b2m-t35n-iceberg-eq-carrier-reused-schema-id
Oct 26, 2026
Merged

morningman merged 6 commits into
apache:masterfrom
morningman:b2m-t35n-iceberg-eq-carrier-reused-schema-id

Conversation

@morningman

@morningman morningman commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: None

Related PR: #66538, #69394

Problem Summary:

In short. This ports a fix from branch-4.1 #66538 (Iceberg V3 default values). When a dropped column is still referenced by a live equality delete, the Iceberg connector now looks up the column's definition along the scan snapshot's parent chain first, and only then in the whole schema list. Master searched only the part of the schema list before the scan schema. That breaks as soon as Iceberg reuses a schema ID, and every query on such a table failed.

Background. An equality delete names its key columns by field ID. If a key column has since been dropped, the scan schema no longer has it, so the connector adds the column's historical definition to the schema dictionary it sends to BE (IcebergScanPlanProvider.schemaForPotentialEqualityDeletes). BE needs that field ID, type and initial default to read the key from data files. Master (#65851) took the definition from the metadata schema list, searching backwards from the scan schema's position, on the assumption that the list is in chronological order.

The problem, and what it cost. Schema IDs are not a sequence. When a schema change produces a schema equal to an existing one, Iceberg reuses that schema's ID and does not append to the list. Dropping a column that was added after the table was created is the common case: the schema returns to the initial one, so the current schema is the first entry of the list, and the schema that still defines the key comes after it. The backward search never reaches that schema, and the scan fails.

Reproduce: create a table with id; add column k; write data and an equality delete keyed on k; drop k; query the table.

Master today After this PR
Query after dropping an equality-delete key, when the schema returns to an earlier one fails: Iceberg equality-delete fields are absent from schema history: [2] succeeds
Same, after the snapshots that recorded the key have expired fails with the same error succeeds (the key comes from the metadata schema list)
Time travel to a snapshot whose lineage defines the key, while a later schema renames it earlier definition earlier definition (unchanged)

How this PR fixes it. It uses the same order as IcebergScanNode.getSchemaFieldsForScan on branch-4.1 and branch-4.2. First, walk the scan snapshot's parent chain and take each missing key from the schema that snapshot was written with. Then search the whole metadata schema list, newest first, for keys that only schema-only changes or expired ancestors can explain. Unlike branch-4.x, the walk indexes each schema at most once and stops as soon as every key is resolved, so a long lineage that shares one schema costs only map lookups. When no key is missing, nothing changes and no snapshot is read, as before.

When a dropped key has several historical definitions, the one recorded on the scan's lineage now wins, as on branch-4.x. The field ID and the initial default are the same either way. Only a name changed by a schema-only commit after the last write can differ. The existing test equalityCarrierKeepsLatestDefinitionOfDroppedEqualityKey (from #69394) renamed and then dropped the key with no write in between. It now writes a snapshot after each schema change, as the branch-4.1 test it was ported from does, and still expects k2. A dropped key nevertheless keeps the compatible changes its field ID has in any retained schema: the widest promotion of its type (the SDK's promotions such as INT→LONG, plus the v3 promotions that Iceberg 1.12.0's TypeUtil.isPromotionAllowed does not know: unknown to any primitive type, and DATE to an unzoned TIMESTAMP or TIMESTAMP_NS), and optional when any retained schema has it optional. The widest promotion does not depend on the order of the schemas. If the retained schemas promote the key to two types that do not promote to each other, which takes setting the current schema back to an earlier one (for example DATE→TIMESTAMP, back to DATE, then DATE→TIMESTAMP_NS), planning fails with an explicit error: neither type holds the other's values exactly, and the metadata does not record which one a delete file uses. A DATE initial default moves to midnight in the promoted type, since Iceberg 1.12.0's DateLiteral converts only to DATE. The carrier drops the write default of a rebuilt field: no reader uses it, and a stale one may not fit the promoted type. A delete committed after a schema-only promotion or required→optional change follows the relaxed definition even when no snapshot on the lineage records it: FileScannerV2 compares keys in the carrier's type, and both readers reject a data file that lacks a required key. A key is never narrowed, and a key required in every retained schema stays required.

Results. This PR ports four tests from #66538 into IcebergScanPlanProviderTest:

  • equalityCarrierResolvesDroppedKeyWhenSchemaIdIsReused uses a real in-memory table and also checks the scan dictionary that getScanNodeProperties builds.
  • equalityCarrierResolvesReusedSchemaIdAfterParentSnapshotsExpire is the same, with the snapshots that recorded the key expired.
  • equalityCarrierPrefersScanLineageOverLaterRenameForTimeTravel and equalityCarrierKeepsDefaultWhenTimeTravelParentExpiredBeforeLaterRename use table doubles with schema histories that the SDK cannot build.
  • equalityCarrierKeepsPromotedTypeOfKeyDroppedBeforeItsDeleteCommits (new) promotes a key from INT to LONG without a snapshot, prepares an equality delete, drops the key and then commits the delete; the carrier must say LONG. It fails without the widening.
  • equalityCarrierKeepsLineageTimestampOfPromotedDateKey and equalityCarrierWidensDateKeyPromotedWithoutASnapshot (new) promote a v3 DATE key to TIMESTAMP / TIMESTAMP_NS, published directly since SchemaUpdate in 1.12.0 rejects these promotions; the carrier must keep the lineage's TIMESTAMP and widen a schema-only DATE→TIMESTAMP_NS. equalityCarrierKeepsSchemaOnlyOptionalityOfDroppedKey and equalityCarrierKeepsAlwaysRequiredDroppedKeyRequired (new) drop a required key while a delete on it is pending, with and without a schema-only makeColumnOptional; the carrier must be optional and required respectively.
  • equalityCarrierConvertsDefaultsOfDateKeyPromotedToTimestamp and equalityCarrierConvertsDefaultsOfDateKeyPromotedToTimestampNs (new) give the v3 DATE key an initial and a write default, promote it to TIMESTAMP / TIMESTAMP_NS without a snapshot and drop it while a delete on it is pending; the carrier must hold the initial default as midnight in the promoted type and no write default, and the scan dictionary must carry 2024-01-02 00:00:00. Without the conversion, planning fails with Cannot cast default value to timestamp.
  • equalityCarrierFollowsUnknownKeyPromotedWithoutASnapshot (new) promotes a v3 key from unknown to DATE and then TIMESTAMP_NS without snapshots, and commits a pending append and a pending delete after the key is dropped, so the lineage only records unknown; the carrier and the scan dictionary must say TIMESTAMP_NS. Without the unknown promotion the carrier stays unknown, which the dictionary sends as STRING. equalityCarrierDropsStaleWriteDefaultOfPromotedDateKey (new) records a DATE write default outside the TIMESTAMP_NS range, clears it and promotes the key to TIMESTAMP_NS without snapshots; planning must not fail on the stale write default, which converting fails with long overflow.
  • equalityCarrierRejectsDateKeyPromotedToTimestampThenTimestampNs and equalityCarrierRejectsDateKeyPromotedToTimestampNsThenTimestamp (new) promote a v3 DATE key to one of TIMESTAMP / TIMESTAMP_NS without a snapshot, set the DATE schema current again, promote it to the other and drop it while a delete on it is pending; planning must fail in either order and name both types. Without the check the carrier takes whichever type the schema list holds first, and a TIMESTAMP carrier rounds a nanosecond tombstone to the microsecond. equalityCarrierRejectsUnknownKeyPromotedToUnrelatedTypes (new) does the same with unknown promoted to INT and then STRING. equalityCarrierKeepsLineagePromotionBesideAnAbandonedOne (new) commits the delete while TIMESTAMP_NS is current, so the lineage records TIMESTAMP_NS; the abandoned TIMESTAMP schema must not fail planning.

With master's search, the two reused-ID tests fail with the error above. Without the lineage walk, the time-travel test gets k2 instead of k. All 1706 tests of fe-connector-iceberg pass.

Original author: @hubgeter

Release note

Fix: queries on an Iceberg table failed with "equality-delete fields are absent from schema history" after a column used as an equality-delete key was dropped and the table schema returned to an earlier one.

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Behavior changed:

    • No.
    • Yes.
      Such queries no longer fail. When a dropped equality-delete key has several historical definitions, the one recorded on the scan snapshot's lineage wins, as on branch-4.x. If retained schemas promote a dropped key to two unrelated types, which takes a rollback of the current schema, planning fails with an explicit error.
  • 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

… ID is reused

### What problem does this PR solve?

Issue Number: None

Related PR: apache#66538, apache#69394

Problem Summary: When an Iceberg table drops a column so that its schema
becomes equal to an earlier one, Iceberg reuses the earlier schema ID
instead of adding a new schema, and the metadata schema list keeps that
schema at its original, earlier position. The equality-delete schema
carrier in the Iceberg connector located the scan schema in that list by
ID and searched only the schemas listed before it. A live equality delete
keyed on the dropped column therefore could not find the schema that still
defines the key, and every scan of the table failed with "Iceberg
equality-delete fields are absent from schema history". This happens in
steady state, not only during an upgrade.

Reproduce: create a table with column id, add column k, write data and an
equality delete keyed on k, drop k (the schema returns to the initial one
and reuses its ID), then query the table.

branch-4.1 (apache#66538) searches the scan snapshot's parent chain first, so the
definition recorded on that lineage wins, and falls back to the whole
metadata list, newest first, for schema-only changes and expired
ancestors. This change ports that order to IcebergScanPlanProvider. Each
lineage schema is indexed at most once and the walk stops as soon as every
key is resolved. It also ports the branch-4.1 tests for a reused schema ID
(with and without an expired parent) and for time travel past a later
rename. The existing test
equalityCarrierKeepsLatestDefinitionOfDroppedEqualityKey (apache#69394)
renamed and then dropped the key with no write in between. It now writes
a snapshot after each schema change, as the branch-4.1 test it was ported
from does, and still expects k2.

### Release note

Fix Iceberg queries that failed with "equality-delete fields are absent
from schema history" after a column used as an equality-delete key was
dropped and the table schema returned to an earlier one.

### Check List (For Author)

- Test: Unit Test
    - fe-connector-iceberg: 4 new cases and 1 adjusted case in
      IcebergScanPlanProviderTest; the two reused-ID cases fail without
      the fix; the whole module passes
- Behavior changed: Yes. Such queries no longer fail. When a dropped key
  has several historical definitions, the carrier now prefers the one
  recorded on the scan snapshot's lineage, as branch-4.1 does.
- Does this need documentation: No

Co-authored-by: daidai <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@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.

One P1 correctness regression is reported inline: resolving a dropped key solely from snapshot lineage can select a type older than a pending writer's schema, causing a widened equality-delete value to become NULL and match an unrelated NULL-key row. Changes are requested. No existing review threads or previously reported blockers were present.

Reviewed both changed files and the affected planning -> schema carrier -> scan-parameter -> native-reader flow at dc21748. Critical checkpoint conclusions:

  • Goal and proof: the real-table tests address schema-ID reuse and expired parents, including dictionary transport. The pending-writer promotion case in the inline finding remains incorrect and lacks coverage.
  • Scope and reuse: the change is focused on one lookup algorithm and its unit tests, reusing the existing historical-field merge helpers.
  • Concurrency: new collections are local and reads use statement-frozen table metadata; no new locks or worker threads. The finding concerns external writes committing after schema-only changes, which the snapshot schema does not fully describe.
  • Lifecycle: statement/table/FileIO ownership is unchanged. Null schema IDs and expired parents reach the schema-history fallback; SDK expiration retains schemas referenced by retained snapshots.
  • Configuration: no configuration changes.
  • Compatibility: no public SPI, storage-format or wire-format change. Existing backend-semantics fencing remains in use.
  • Thrift/protobuf IDs: no gensrc changes; branch field-ID comparison is inapplicable.
  • Parallel paths: production and testing callers pass the selected snapshot; normal, pinned/ref and streaming scans share the carrier. Both native reader generations were inspected; system-table handling is separate and unchanged.
  • Conditions and errors: repeated schema IDs are deduplicated, the no-missing-key path skips ancestry, and missing schemas/unresolved IDs fail loudly. Early completion after resolving an older type causes the reported defect.
  • Test coverage: four new JUnit tests and the amended rename/drop test were inspected alongside existing nested-key, default, reused-name and reader-promotion tests. Proxy histories establish local selection rules, not end-to-end reader results. No test directly covers the reported sequence or large missing-key ancestry.
  • Test results: no generated result files changed. No builds or tests were run under the static-only review contract; the author's reported test count was not independently validated.
  • Observability: existing error messages identify missing schema/equality IDs; no new logging or metrics requirement was substantiated for this local planning change.
  • Transactions and persistence: no Doris transaction, EditLog or replay changes. External snapshot commit semantics were checked against the pinned Iceberg SDK sources linked inline.
  • Writes and crash behavior: production changes only reconstruct read metadata; they introduce no writes, commit protocols or resource handles.
  • FE/BE variables: only a local Java Snapshot argument is added; dictionary fields and transport paths remain unchanged. Its historical type affects the existing BE comparison contract as described inline.
  • Performance: schema indexing is deduplicated and carrier size remains bounded by the current schema and relevant historical fields. Missing keys can require O(retained ancestry) lookups and REST refs-mode can load deferred snapshot history; no separate actionable performance regression was established or benchmarked.
  • Other concerns: nested field-ID merging, dropped/re-added names, stable initial defaults, schema-only updates, time travel, branches, expiry, and both reader generations were checked. No additional verified finding remains.

No additional user-provided focus was supplied. Review limits are static inspection only, unexecuted runtime tests, and unmeasured large-lineage/REST history cost. All identified candidates and risk focuses have explicit dispositions in the shared ledger. Final convergence is recorded separately by the submission helper after the unchanged-payload reviewer pass.

Review converged after 3 round(s).

throw new IllegalStateException(
"Iceberg snapshot schema " + schemaId + " is absent from table metadata");
}
addHistoricalEqualityFields(fields, missing, lineageSchema);

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.

[P1] Preserve promoted types when recovering a dropped equality key

Snapshot lineage can omit the type used by a pending writer. Start with optional k INT and an existing row where k IS NULL; promote k to LONG without a snapshot, prepare a LONG equality-delete file for k=2147483648, drop k, then commit the pending RowDelta. Use unpartitioned files without column metrics (including null counts and bounds). The SDK permits this: SnapshotProducer.apply() refreshes metadata and stamps the current schema, and BaseRowDelta does not reject the changed key schema. The delete snapshot therefore records the dropped schema while its parent still records INT.

This call resolves the ID from that INT ancestor and removes it from missing, so the retained schema-only LONG definition is never considered. V2 _resolve_equality_delete_fields takes the carrier's INT as its comparison type; _load_equality_delete_file then casts the LONG tombstone to INT. Overflow becomes NULL in non-strict mode, and NULL-safe equality incorrectly deletes the existing NULL-key row (strict mode fails the query). The previous metadata-history lookup selected LONG and preserved that row. Preserve the applicable promoted comparison type when choosing lineage definitions, and cover the promotion/drop/pending-delete sequence in tests.

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 71f51ed. With Iceberg 1.12 a pending RowDelta is stamped with the schema that is current when it commits (SnapshotProducer.apply → base.currentSchemaId()), and nothing validates its equality field IDs against that schema, so a key promoted by a schema-only change and dropped before the delete commits has no snapshot on the lineage that records the promoted type. FileScannerV2 compares equality keys in the carrier's type, so the INT carrier narrowed the LONG tombstone to NULL (non-strict) and matched NULL keys; the V1 reader compares in the delete file's own type and was not affected.

The carrier now keeps the lineage definition of a dropped key (name, optionality, initial default) but takes the widest type its field ID has in any retained schema, where wider means an Iceberg promotion (TypeUtil.isPromotionAllowed: INT→LONG, FLOAT→DOUBLE, decimal precision). Field IDs are never reused and promotion is monotonic and lossless, so that type covers every file written for the field, and widening is harmless when it comes from a schema after a time-travel target. equalityCarrierKeepsPromotedTypeOfKeyDroppedBeforeItsDeleteCommits replays the sequence with the SDK (promote, prepare the delete, drop, commit) and fails without the widening (expected: <long> but was: <int>).

branch-4.2 resolves keys the same way (IcebergScanNode.getSchemaFieldsForScan) and has the same FileScannerV2 comparison, so it carries the same defect; it will be passed to the 4.x maintainers. One residual case is shared by every version: expireSnapshots().cleanExpiredMetadata(true) can remove a schema-only schema that no retained snapshot references, after which no metadata records the promoted type; closing that needs a BE-side guard when a delete file's key type is wider than the carrier, which is outside this PR.

### What problem does this PR solve?

Issue Number: None

Related PR: apache#66538

Problem Summary: Review follow-up. The previous commit resolves an
equality-delete key that the scan schema no longer has from the scan
snapshot's parent chain first. With Iceberg 1.12 a pending RowDelta is
stamped with the schema that is current when it commits, and nothing checks
its equality field IDs against that schema. So for a key k promoted from INT
to LONG by a schema-only change (no snapshot), a writer that wrote LONG
tombstones, and k dropped before that delete commits, the delete snapshot
records the schema without k and its parent the INT one: no snapshot on the
lineage records LONG. The carrier then said INT, FileScannerV2 compares
equality keys in the carrier's type, and a tombstone such as 2147483648 was
cast to NULL in non-strict mode and deleted every row whose k is NULL
(strict mode failed the query). The previous metadata-list lookup found
LONG.

A dropped key now keeps its lineage definition (name, optionality, initial
default) but takes the widest type its field ID has in any retained schema.
Field IDs are never reused and promotion is monotonic and lossless, so that
type covers every file written for the field, also when it comes from a
schema after a time-travel target.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - IcebergScanPlanProviderTest#equalityCarrierKeepsPromotedTypeOfKeyDroppedBeforeItsDeleteCommits replays the sequence with the SDK; it fails without the widening
    - fe-connector-iceberg module tests
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage `` 🎉
Increment coverage report
Complete coverage report

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17591	5176	5079	5079
q2	2140	311	264	264
q3	10215	1367	822	822
q4	4676	460	340	340
q5	7423	794	525	525
q6	183	184	153	153
q7	747	799	600	600
q8	9271	1361	1102	1102
q9	5562	4207	4180	4180
q10	6837	1290	1040	1040
q11	425	249	223	223
q12	639	429	282	282
q13	18096	3641	3002	3002
q14	278	277	251	251
q15	q16	723	729	676	676
q17	1193	751	601	601
q18	7018	6185	6149	6149
q19	1120	964	585	585
q20	400	343	233	233
q21	5435	2481	2462	2462
q22	377	342	272	272
Total cold run time: 100349 ms
Total hot run time: 28841 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5912	5931	6045	5931
q2	744	600	571	571
q3	5570	5605	5324	5324
q4	2009	2153	1593	1593
q5	5755	5676	5733	5676
q6	238	190	143	143
q7	2304	1962	1822	1822
q8	3006	2620	2619	2619
q9	7969	7968	7750	7750
q10	3661	3567	3117	3117
q11	576	427	377	377
q12	666	671	509	509
q13	3263	3599	3055	3055
q14	286	303	268	268
q15	q16	660	663	621	621
q17	7702	7049	6887	6887
q18	12965	12194	12912	12194
q19	893	767	768	767
q20	2153	2146	1914	1914
q21	5614	4612	4676	4612
q22	496	455	390	390
Total cold run time: 72442 ms
Total hot run time: 66140 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150849 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 dc217482a94425cabd30b16bba72fe70c8cf784e, data reload: false

query5	4308	586	444	444
query6	422	209	183	183
query7	4803	438	226	226
query8	323	163	151	151
query9	8765	3041	3054	3041
query10	476	310	246	246
query11	5957	3859	3566	3566
query12	146	91	90	90
query13	1264	451	317	317
query14	6144	3902	3657	3657
query14_1	3412	3400	3435	3400
query15	209	201	192	192
query16	943	427	410	410
query17	867	655	538	538
query18	2422	425	327	327
query19	200	168	137	137
query20	87	84	84	84
query21	207	123	107	107
query22	12950	13041	12739	12739
query23	13550	12857	12525	12525
query23_1	12517	12575	12607	12575
query24	7390	832	464	464
query24_1	471	495	465	465
query25	538	426	359	359
query26	1270	254	144	144
query27	2756	438	271	271
query28	4617	1947	1919	1919
query29	1569	600	457	457
query30	298	221	185	185
query31	993	925	829	829
query32	142	93	94	93
query33	523	290	256	256
query34	894	845	493	493
query35	730	747	647	647
query36	816	823	737	737
query37	122	104	93	93
query38	1817	1727	1653	1653
query39	793	788	749	749
query39_1	734	729	738	729
query40	223	120	100	100
query41	72	70	67	67
query42	88	86	84	84
query43	345	369	310	310
query44	1295	699	684	684
query45	175	172	171	171
query46	823	980	573	573
query47	3022	2974	2956	2956
query48	290	318	215	215
query49	573	408	312	312
query50	686	267	205	205
query51	10658	10431	10185	10185
query52	78	76	74	74
query53	177	206	149	149
query54	230	208	171	171
query55	68	65	62	62
query56	221	206	203	203
query57	1682	1608	1591	1591
query58	284	266	259	259
query59	2089	2160	1949	1949
query60	289	243	218	218
query61	149	147	148	147
query62	393	335	306	306
query63	184	159	160	159
query64	2717	960	741	741
query65	3311	3278	3247	3247
query66	1787	420	305	305
query67	20300	20133	20190	20133
query68	2979	988	607	607
query69	385	291	248	248
query70	849	765	770	765
query71	296	228	211	211
query72	2746	2576	2281	2281
query73	511	528	291	291
query74	4647	4524	4317	4317
query75	2338	2281	1903	1903
query76	2314	1017	573	573
query77	334	375	273	273
query78	9748	9759	9197	9197
query79	933	897	523	523
query80	842	417	350	350
query81	583	322	271	271
query82	575	130	107	107
query83	283	192	181	181
query84	302	115	98	98
query85	974	421	386	386
query86	364	236	234	234
query87	2015	1966	1813	1813
query88	3629	2654	2629	2629
query89	337	283	242	242
query90	1706	167	165	165
query91	162	155	127	127
query92	91	88	85	85
query93	1002	995	580	580
query94	511	307	275	275
query95	563	330	354	330
query96	662	471	233	233
query97	2437	2449	2333	2333
query98	161	150	145	145
query99	765	768	659	659
Total cold run time: 234437 ms
Total hot run time: 150849 ms

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17593	5134	5049	5049
q2	2117	300	241	241
q3	10285	1369	793	793
q4	4674	458	330	330
q5	7430	788	529	529
q6	177	187	153	153
q7	753	794	591	591
q8	9255	1328	1013	1013
q9	5483	4374	4149	4149
q10	6822	1281	1024	1024
q11	425	247	228	228
q12	635	393	273	273
q13	18099	3521	2980	2980
q14	267	273	248	248
q15	q16	718	716	639	639
q17	1178	756	555	555
q18	6985	6150	6123	6123
q19	1132	952	587	587
q20	368	327	227	227
q21	4651	2132	2292	2132
q22	352	288	250	250
Total cold run time: 99399 ms
Total hot run time: 28114 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5610	5605	5605	5605
q2	689	564	536	536
q3	5197	5417	4922	4922
q4	2009	2139	1525	1525
q5	5263	5148	5400	5148
q6	261	199	160	160
q7	2328	2009	1837	1837
q8	3047	2706	2700	2700
q9	7963	7965	7793	7793
q10	3746	3694	3202	3202
q11	603	428	402	402
q12	731	794	545	545
q13	3586	3948	3212	3212
q14	300	313	289	289
q15	q16	701	732	652	652
q17	8292	7503	6893	6893
q18	13004	12196	12889	12196
q19	909	807	797	797
q20	2146	2153	1927	1927
q21	5641	4568	4669	4568
q22	497	467	400	400
Total cold run time: 72523 ms
Total hot run time: 65309 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150394 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 71f51edf754b1d8088aa6a7cacb547bbf54b1e8d, data reload: false

query5	4308	585	445	445
query6	447	206	195	195
query7	4816	431	222	222
query8	328	160	148	148
query9	8758	3051	3085	3051
query10	440	312	261	261
query11	6060	3846	3560	3560
query12	156	90	86	86
query13	1258	444	349	349
query14	6068	3935	3636	3636
query14_1	3452	3435	3404	3404
query15	208	198	182	182
query16	937	437	412	412
query17	874	669	540	540
query18	2422	434	317	317
query19	202	167	138	138
query20	90	83	84	83
query21	212	125	111	111
query22	12978	12979	12808	12808
query23	13656	12822	12496	12496
query23_1	12609	12541	12609	12541
query24	7365	816	473	473
query24_1	471	470	475	470
query25	552	422	365	365
query26	1260	261	140	140
query27	2766	457	278	278
query28	5124	1932	1966	1932
query29	1576	625	472	472
query30	301	218	184	184
query31	1016	928	829	829
query32	137	122	91	91
query33	528	303	250	250
query34	905	849	496	496
query35	747	752	649	649
query36	802	807	711	711
query37	127	103	83	83
query38	1775	1732	1709	1709
query39	788	791	749	749
query39_1	737	736	740	736
query40	221	116	97	97
query41	71	69	70	69
query42	88	85	84	84
query43	351	369	314	314
query44	1301	679	675	675
query45	177	215	159	159
query46	837	960	552	552
query47	2973	2947	2752	2752
query48	291	303	216	216
query49	574	399	287	287
query50	680	262	206	206
query51	10437	10286	10265	10265
query52	76	79	77	77
query53	178	209	154	154
query54	237	192	169	169
query55	70	67	70	67
query56	213	225	219	219
query57	1662	1640	1553	1553
query58	283	268	257	257
query59	2172	2173	1984	1984
query60	274	247	233	233
query61	148	148	144	144
query62	386	328	279	279
query63	187	158	151	151
query64	2785	935	768	768
query65	3314	3286	3238	3238
query66	1783	413	301	301
query67	20233	19999	19743	19743
query68	2961	1008	602	602
query69	376	296	254	254
query70	863	775	776	775
query71	285	227	206	206
query72	2707	2827	2246	2246
query73	507	518	289	289
query74	4604	4501	4337	4337
query75	2311	2265	1903	1903
query76	2302	1015	598	598
query77	347	384	270	270
query78	9801	9830	9204	9204
query79	998	853	511	511
query80	701	422	354	354
query81	541	324	275	275
query82	573	133	111	111
query83	286	196	176	176
query84	306	124	99	99
query85	873	454	383	383
query86	345	238	228	228
query87	2030	1922	1815	1815
query88	3628	2687	2624	2624
query89	324	285	242	242
query90	1799	168	165	165
query91	159	154	134	134
query92	99	84	96	84
query93	986	997	570	570
query94	488	313	279	279
query95	555	343	368	343
query96	659	485	234	234
query97	2431	2423	2310	2310
query98	158	151	146	146
query99	773	766	660	660
Total cold run time: 234739 ms
Total hot run time: 150394 ms

@hello-stephen

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

query1	0.01	0.00	0.00
query2	0.09	0.04	0.05
query3	0.26	0.08	0.09
query4	1.60	0.10	0.09
query5	0.26	0.24	0.27
query6	1.17	0.69	0.68
query7	0.04	0.01	0.00
query8	0.05	0.03	0.03
query9	0.40	0.35	0.35
query10	0.61	0.57	0.59
query11	0.21	0.14	0.13
query12	0.18	0.15	0.15
query13	0.49	0.49	0.51
query14	1.05	1.01	1.02
query15	0.63	0.60	0.63
query16	0.35	0.35	0.36
query17	1.18	1.17	1.16
query18	0.21	0.19	0.18
query19	2.08	1.98	2.03
query20	0.02	0.02	0.01
query21	15.50	0.20	0.13
query22	4.76	0.05	0.05
query23	16.24	0.34	0.12
query24	2.95	0.49	0.42
query25	0.08	0.06	0.06
query26	1.10	0.22	0.16
query27	0.05	0.04	0.04
query28	3.67	0.85	0.36
query29	12.54	4.15	3.23
query30	0.28	0.13	0.11
query31	2.78	0.57	0.32
query32	3.22	0.60	0.49
query33	3.19	3.18	3.21
query34	15.41	3.98	3.40
query35	3.34	3.35	3.33
query36	0.53	0.46	0.40
query37	0.10	0.06	0.07
query38	0.05	0.04	0.04
query39	0.03	0.03	0.03
query40	0.16	0.14	0.14
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: 97.04 s
Total hot run time: 24.3 s

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

Request changes for two verified regressions in dropped equality-key recovery: v3 temporal promotion can produce incorrect deleted rows, and schema-only nullability relaxation can make valid scans fail. Both are reported inline.

The existing P1 4359014047 is fixed for its reported INT-to-LONG scenario: retained promoted types now override a stale lineage type. It is not reposted or counted as an existing blocker. The temporal finding is distinct: the new helper overwrites an already-correct promoted lineage definition. No additional user focus was supplied.

Static review covered both changed files, their callers, carrier serialization, snapshot/schema selection, relevant Iceberg 1.12.0 contracts, and both native reader paths. No builds or tests were run, as required by this review environment.

Critical checkpoint conclusions:

  • Goal and proof: lineage recovery and whole-history fallback address reused schema IDs and expired parents. Added unit tests assert these metadata carriers and the pending INT-to-LONG delete case; the two inline cases remain incorrect.
  • Scope and reuse: the change is confined to the carrier builder, its call sites and tests. Existing historical-field merging and encoding are reused. The promotion helper's supported-type assumptions need correction.
  • Concurrency and locks: new collections are invocation-local; production statement tables retain one metadata generation. No locks, worker threads or lock ordering change. Concurrent external schema evolution is relevant to the nullability finding's pending writer.
  • Lifecycle and initialization: no new owned resources, static initialization dependencies or cleanup paths. Ancestor traversal stops at a missing/expired parent and falls back to retained schemas.
  • Configuration: no added settings or dynamic-configuration behavior.
  • Compatibility: no SPI, storage-format or symbol changes. Existing scan capability fencing remains; legal v3 temporal evolution exposes the reported comparison-domain regression.
  • Thrift/protobuf: no definition or field-ID changes; cross-release ID comparison is not applicable.
  • Parallel paths: synchronous/streaming native scans share the scan-node carrier. V2 uses the carrier comparison type; V1 takes physical delete-file types. Requiredness reaches missing-field construction in both readers. System-table paths are unchanged.
  • Conditions and error handling: missing-schema/key failures remain explicit and documented; the nullability finding instead rejects valid missing-key data using stale requiredness. No new exception is silently ignored.
  • Test coverage: new real-table tests cover ID reuse, parent expiration and a pending promoted delete; existing tests cover nested defaults and bounded carriers. Synthetic time-travel histories test ordering, not realistic writer reachability. Physical-file row results, temporal promotion and schema-only optionality are not exercised by the added cases.
  • Test results: no regression output files changed; new assertions are metadata/schema assertions. Execution results reported in the PR description were not independently reproduced.
  • Observability: existing metadata failures and reader errors remain available; no new distributed operation or logging/metric requirement was identified.
  • Transactions and persistence: Doris write, EditLog and failover paths are unchanged. External snapshot/schema commit behavior was traced to validate the reported scenarios.
  • Data writes and crash behavior: the patch only changes read planning; it adds no data write, atomicity, crash cleanup or write-lock obligation.
  • FE-to-BE values: the existing dictionary and encoding carry the changed key type/requiredness to the readers; no new transmitted variable or send path is introduced.
  • Performance and memory: missing-key recovery now scans retained schema history and snapshot ancestry, with each lineage schema indexed once; it does not enumerate data-file splits and carrier size stays bounded by the full selected schema plus referenced historical keys and their ancestors. No benchmark was run and no separate actionable performance regression was established.
  • Further review: default conversion, current-field precedence, nested identity matching, name reuse, time travel and fallback were rechecked. No additional distinct defect was verified beyond the two inline findings.

Coverage limits: this is a static review, without physical-file execution or an external writer run. The author's already-disclosed loss of an unreferenced schema-only promotion after cleanExpiredMetadata(true) is a pre-existing limitation, not a new finding in this invocation.

Review converged after 3 round(s).

continue;
}
Type widest = widestTypes.get(fieldId);
if (widest == null || TypeUtil.isPromotionAllowed(widest, type.asPrimitiveType())) {

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.

[P1] Preserve v3 temporal promotions when selecting the comparison type

Iceberg v3 permits DATE to unzoned TIMESTAMP/TIMESTAMP_NS promotion, but the pinned 1.12.0 TypeUtil.isPromotionAllowed only recognizes INT/LONG, FLOAT/DOUBLE and decimal precision (spec, helper). Iterating retained schemas oldest-first therefore keeps DATE, and withWidestTypes replaces even a correctly selected TIMESTAMP lineage field with DATE.

For an unpartitioned v3 table, retain a row with k=DATE '2024-01-02', promote k to TIMESTAMP, commit an equality delete for 2024-01-02 12:00:00, then drop k. Omit column metrics so the delete reaches the reader. V2 _resolve_equality_delete_fields chooses carrier DATE and _load_equality_delete_file casts the noon tombstone to DATE: it incorrectly deletes the original row, whose promoted value is midnight. The previous history lookup retained TIMESTAMP. Handle the v3 temporal comparison domains explicitly, avoid narrowing the lineage type, and cover this sequence. This differs from the existing INT/LONG thread because the promoted schema is present on the lineage.

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 cbf21fb0585. The widening added in 71f51ed relied on TypeUtil.isPromotionAllowed, which in Iceberg 1.12.0 (and on main) knows only INT→LONG, FLOAT→DOUBLE and decimal precision. For a v3 key promoted from DATE to TIMESTAMP, the first retained type therefore won and replaced the TIMESTAMP definition the lineage had already found. FileScannerV2 then cast the noon tombstone to DATE with an ordinary truncating cast and deleted the midnight row. V1 compares in the delete file's own type and was not affected.

Each field of the selected definition now starts from its own type and moves only along a recognized promotion: the SDK rules plus DATE→unzoned TIMESTAMP/TIMESTAMP_NS. It can no longer narrow a key or switch it to an unrelated type, and the result does not depend on the order of the retained schemas. equalityCarrierKeepsLineageTimestampOfPromotedDateKey replays this sequence, and equalityCarrierWidensDateKeyPromotedWithoutASnapshot covers a schema-only DATE→TIMESTAMP_NS promotion followed by a pending delete and a drop. Both publish the promotion with TableMetadata.Builder.setCurrentSchema, as promotedDateBoundsDoNotPruneNanosecondMatches does, because SchemaUpdate.updateColumn in 1.12.0 still rejects it; PyIceberg and iceberg-go reject it too unless incompatible changes are allowed, while a REST catalog's add-schema update only checks the new schema on its own.

Master before this PR and branch-4.2 do not widen, so neither has this regression. branch-4.2 does share the schema-only variant, because its lineage yields DATE; that case goes into the note for the 4.x maintainers.

throw new IllegalStateException(
"Iceberg snapshot schema " + schemaId + " is absent from table metadata");
}
addHistoricalEqualityFields(fields, missing, lineageSchema, widestTypes);

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] Retain schema-only optionality when recovering a dropped key

This consumes a key from missing using its snapshot-lineage definition before the metadata fallback can observe a compatible required-to-optional change. Consider a v2 unpartitioned table with required k INT and no initial default: append S1, make k optional without a snapshot, prepare a NULL equality-delete RowDelta, drop k, append an id-only file as S2, then commit the pending delete as S3. The SDK allows this pending commit and stamps the current dropped schema (SnapshotProducer, BaseRowDelta). Use files without column metrics so pruning does not exclude the delete.

S2/S3 omit k; recovery reaches S1 and restores REQUIRED, while the previous metadata lookup selected the intervening OPTIONAL definition. BE then rejects S2 with Missing required field: k instead of materializing NULL for the equality comparison. Type widening does not address this unchanged-INT case. Preserve the compatible schema-only optionality transition and add this pending-delete/drop/append test; globally relaxing still-required fields would hide real errors.

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 cbf21fb0585. The pending delete and the id-only append both commit with the post-drop schema, so the lineage walk reaches the original REQUIRED definition and never sees the schema-only makeColumnOptional. Both readers then reject the id-only file: FileScannerV2 with Missing required field: k, and V1 with Required Iceberg field 'k' is missing from the data file and has no initial default. Master's lookup found OPTIONAL and read NULL. Column metrics do not prevent this: Iceberg's own DeleteFileIndex resolves k from the highest schema ID, where it is optional, and attaches the NULL tombstone because the new file has no stats for k.

A dropped key is now optional when any retained schema has it optional. Required→optional is the compatible direction (the reverse needs allowIncompatibleChanges), and reading NULL for the missing key is safe either way. A key that is required in every retained schema stays required, so the missing-required check does not change for it. equalityCarrierKeepsSchemaOnlyOptionalityOfDroppedKey replays the pending-delete/drop/append sequence, and equalityCarrierKeepsAlwaysRequiredDroppedKeyRequired checks that an always-required dropped key stays required.

branch-4.2 also resolves keys lineage-first in IcebergScanNode.getSchemaFieldsForScan, and its readers reject the file the same way, so it has the same defect; it goes into the note for the 4.x maintainers.

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17595	5160	5057	5057
q2	2120	310	253	253
q3	10260	1378	813	813
q4	4686	466	341	341
q5	7444	802	524	524
q6	177	189	151	151
q7	751	801	590	590
q8	9253	1380	1081	1081
q9	5511	4219	4193	4193
q10	6828	1289	1028	1028
q11	439	254	231	231
q12	646	397	276	276
q13	18110	3677	2991	2991
q14	268	282	249	249
q15	q16	755	715	653	653
q17	1195	742	566	566
q18	7018	6185	6142	6142
q19	1111	966	584	584
q20	384	331	221	221
q21	5619	2236	2286	2236
q22	351	311	248	248
Total cold run time: 100521 ms
Total hot run time: 28428 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5593	5625	5634	5625
q2	685	589	550	550
q3	5211	5400	4919	4919
q4	1990	2135	1533	1533
q5	5265	5193	5792	5193
q6	254	197	157	157
q7	2456	2070	1877	1877
q8	3045	2887	2736	2736
q9	8040	8114	8061	8061
q10	3699	3665	3283	3283
q11	665	454	405	405
q12	659	737	508	508
q13	3456	3982	3218	3218
q14	323	317	283	283
q15	q16	711	721	643	643
q17	8262	7518	7317	7317
q18	13049	12192	12984	12192
q19	902	775	776	775
q20	2143	2159	1924	1924
q21	5640	4573	4659	4573
q22	490	455	418	418
Total cold run time: 72538 ms
Total hot run time: 66190 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150560 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 71f51edf754b1d8088aa6a7cacb547bbf54b1e8d, data reload: false

query5	4302	595	446	446
query6	424	213	208	208
query7	4793	442	220	220
query8	325	166	157	157
query9	8755	3104	3094	3094
query10	475	316	259	259
query11	6074	3862	3550	3550
query12	149	90	88	88
query13	1249	452	340	340
query14	6104	3938	3635	3635
query14_1	3429	3411	3438	3411
query15	210	204	187	187
query16	943	439	411	411
query17	862	668	537	537
query18	2443	441	305	305
query19	206	158	132	132
query20	86	85	91	85
query21	215	130	109	109
query22	13068	13044	12913	12913
query23	13426	12740	12501	12501
query23_1	12538	12523	12380	12380
query24	7362	815	466	466
query24_1	469	480	474	474
query25	545	427	363	363
query26	1166	258	142	142
query27	2748	445	276	276
query28	4587	1973	1965	1965
query29	1525	598	455	455
query30	293	221	183	183
query31	1013	949	812	812
query32	140	95	93	93
query33	548	308	245	245
query34	896	843	497	497
query35	731	745	643	643
query36	831	820	719	719
query37	122	96	84	84
query38	1766	1742	1661	1661
query39	799	802	737	737
query39_1	739	730	736	730
query40	223	117	97	97
query41	71	73	67	67
query42	87	83	85	83
query43	342	363	324	324
query44	1293	686	692	686
query45	182	180	168	168
query46	876	983	576	576
query47	2951	3036	2837	2837
query48	285	306	221	221
query49	573	400	297	297
query50	678	269	206	206
query51	10391	10501	10269	10269
query52	79	78	75	75
query53	183	208	155	155
query54	250	190	185	185
query55	71	70	64	64
query56	241	208	202	202
query57	1657	1621	1556	1556
query58	286	257	253	253
query59	2138	2190	1945	1945
query60	294	256	231	231
query61	147	147	139	139
query62	380	334	285	285
query63	185	153	162	153
query64	2673	956	799	799
query65	3336	3281	3316	3281
query66	1766	433	290	290
query67	19968	20141	19884	19884
query68	2953	1001	602	602
query69	380	284	244	244
query70	864	764	799	764
query71	295	234	233	233
query72	2718	2568	2302	2302
query73	524	516	292	292
query74	4629	4525	4329	4329
query75	2309	2279	1895	1895
query76	2333	1010	581	581
query77	339	357	275	275
query78	9833	9708	9104	9104
query79	1004	856	518	518
query80	1065	428	336	336
query81	575	306	271	271
query82	568	139	107	107
query83	308	189	181	181
query84	308	117	98	98
query85	852	420	362	362
query86	354	241	264	241
query87	2012	1953	1804	1804
query88	3604	2662	2612	2612
query89	332	279	242	242
query90	1812	165	164	164
query91	156	149	123	123
query92	95	91	91	91
query93	985	994	579	579
query94	556	315	282	282
query95	579	332	370	332
query96	670	478	232	232
query97	2430	2462	2347	2347
query98	178	149	164	149
query99	770	768	647	647
Total cold run time: 233974 ms
Total hot run time: 150560 ms

@hello-stephen

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

query1	0.01	0.01	0.00
query2	0.09	0.05	0.05
query3	0.26	0.09	0.09
query4	1.61	0.11	0.10
query5	0.23	0.24	0.25
query6	1.17	0.69	0.68
query7	0.04	0.00	0.00
query8	0.06	0.04	0.04
query9	0.41	0.35	0.35
query10	0.58	0.57	0.57
query11	0.19	0.15	0.15
query12	0.18	0.14	0.15
query13	0.50	0.49	0.50
query14	1.03	1.02	1.02
query15	0.63	0.61	0.60
query16	0.35	0.35	0.35
query17	1.18	1.14	1.14
query18	0.21	0.19	0.20
query19	2.08	1.99	1.96
query20	0.02	0.02	0.01
query21	15.49	0.18	0.13
query22	5.02	0.05	0.05
query23	16.30	0.32	0.12
query24	2.88	0.44	0.40
query25	0.13	0.07	0.04
query26	1.11	0.22	0.16
query27	0.05	0.05	0.04
query28	3.48	0.71	0.36
query29	12.50	4.10	3.24
query30	0.30	0.14	0.11
query31	2.78	0.55	0.32
query32	3.22	0.59	0.51
query33	3.17	3.27	3.15
query34	15.34	3.96	3.38
query35	3.33	3.35	3.32
query36	0.55	0.43	0.43
query37	0.10	0.06	0.06
query38	0.05	0.05	0.04
query39	0.03	0.03	0.03
query40	0.18	0.16	0.14
query41	0.08	0.04	0.03
query42	0.04	0.03	0.03
query43	0.05	0.04	0.03
Total cold run time: 97.01 s
Total hot run time: 24.29 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage `` 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 100% (0/0) 🎉
Increment coverage report
Complete coverage report

…d compatible changes

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69536

Problem Summary: A dropped Iceberg equality-delete key takes its
definition from the scan's snapshot lineage, falling back to the whole
schema list. The previous commit then replaced the key's type with the
widest type it has in any retained schema, comparing types with
TypeUtil.isPromotionAllowed. In Iceberg 1.12.0 that helper knows only
INT to LONG, FLOAT to DOUBLE and decimal precision, so for a v3 table
whose key was promoted from DATE to an unzoned TIMESTAMP the first
retained type, DATE, won and replaced the TIMESTAMP definition the
lineage had found. FileScannerV2 compares equality keys in the carrier's
type, so it cast a noon TIMESTAMP tombstone to DATE and deleted the
midnight row of that day. A schema-only DATE to TIMESTAMP_NS promotion
of a key dropped before its delete committed kept DATE as well.

The lineage also lost a schema-only change of a required key to
optional: a pending delete and an append without the key both commit
with the post-drop schema, so the lineage reaches the original REQUIRED
definition, and both readers reject the appended file with a missing
required field, where the lookup before this PR found OPTIONAL.

Each field of the selected definition now starts from its own type and
optionality and takes, from every retained schema, only a compatible
relaxation: a promotion recognized by TypeUtil.isPromotionAllowed or the
v3 promotion of DATE to an unzoned TIMESTAMP or TIMESTAMP_NS, and
optional when any retained schema has the field optional. A key cannot
be narrowed or switched to an unrelated type, and a key required in
every retained schema stays required.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - IcebergScanPlanProviderTest: a v3 DATE key promoted to TIMESTAMP keeps the lineage's TIMESTAMP; a DATE key promoted to TIMESTAMP_NS without a snapshot and dropped before its delete commits widens to TIMESTAMP_NS; a key made optional without a snapshot and dropped before its delete commits stays optional; a key required in every schema stays required
    - fe-connector-iceberg module tests
- Behavior changed: No (fixes a regression of this PR's previous commits)
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@morningman

Copy link
Copy Markdown
Contributor Author

Addressed the second review round in cbf21fb0585. Per finding:

  • 4363903193 (P1, v3 DATE→TIMESTAMP narrowed): fixed. The widening now starts from the selected definition and follows only recognized promotions, the SDK rules plus DATE→unzoned TIMESTAMP/TIMESTAMP_NS, so it can no longer narrow a key.
  • 4363903205 (P2, schema-only optionality lost): fixed. A dropped key is optional when any retained schema has it optional; a key required in every retained schema stays required.

Four new tests in IcebergScanPlanProviderTest; all 1698 tests of fe-connector-iceberg pass. The PR description is updated.

@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 #69536 at cbf21fb against the supplied aggregate diff. Two new P2 findings remain: temporally promoted DATE defaults can abort FE planning, and a future nanosecond promotion can make a historical DATE equality-delete scan fail. Both are inline with concrete histories and static call-chain evidence.

The original scenarios in comments 4359014047 (INT/LONG), 4363903193 (temporal narrowing), and 4363903205 (schema-only optionality) are fixed at this head. They were independently revalidated and are not reposted; no existing P0/P1 comment remains blocking for its reported scenario.

Reviewed flow: statement-frozen table -> exact snapshot/ref scan -> live equality-delete field IDs -> scan schema and ancestry -> retained-schema fallback/relaxations -> field-ID dictionary -> V1/V2 native readers.

Critical checkpoint conclusions:

  1. Goal/data correctness: ancestry plus whole-history fallback resolves reused schema IDs and expired parents in the inspected SDK histories. The two temporal failures prevent an unconditional correctness conclusion; the added tests do not cover them.
  2. Scope/reuse: the change is limited to the existing provider and unit suite, reusing schema selection, merging and dictionary encoding.
  3. Concurrency/locks: no threads or locks are added. Local collections operate on statement-frozen metadata; schema and snapshot lookups share that generation and preserve the selected snapshot's visibility.
  4. Lifecycle/static initialization: no new external resources, shared mutable state or static initialization dependency. Existing table leases own metadata/FileIO lifetime.
  5. Configuration: no settings or dynamic-update propagation changes.
  6. Compatibility: no SPI API, function symbol or storage-format changes. Existing current-backend scan-semantics fencing still covers equality-delete scans.
  7. Thrift/Protobuf: no definitions or field IDs change.
  8. Parallel paths: traced dictionary consumers in V1 and V2, including Parquet/ORC. Default reconstruction fails before either reader; the future-range finding specifically follows V2's carrier-domain cast. System/position-delete and no-equality branches retain their existing paths.
  9. Conditions/error handling: empty-missing fast path, repeated lineage schemas, legacy null schema IDs and expired-parent fallback are consistent. Unresolved IDs and malformed retained-snapshot schema references fail explicitly. Compatible nested ancestor/child restoration was checked without a separate defect.
  10. Test coverage: real SDK histories cover reused IDs, expiration and pending promotion/optionality/drop; doubles cover historical precedence. The reused-ID test round-trips the dictionary. Temporal default-bearing and future-range cases need coverage. Added file helpers are metadata-only, so these additions do not execute row/delete semantics.
  11. Results/framework: additions follow existing JUnit conventions; no regression output files are changed. No builds or tests were run, as required by this static-only review contract.
  12. Observability: missing schema/field errors remain explicit; no new distributed operation or metric requirement was identified.
  13. Transactions/persistence: no Doris transaction, EditLog, failover or persistent-state write path changes. External pending commits were traced through the pinned Iceberg SDK.
  14. Writes/atomicity: production changes are read-only planning; no data mutation or crash-cleanup path is added. Doris MoW bitmap/version logic is unaffected.
  15. FE-BE transport: existing TField type/default/optionality fields reach both readers; no new transmitted variable or sending point is introduced.
  16. Performance/memory: missing-key recovery walks retained ancestry and schemas, and SDK field lookup can populate schema-wide ID indexes. No new data-file enumeration or remote scan is introduced; carrier output stays limited to current fields plus referenced keys. No material performance defect was substantiated, and no benchmark was run.
  17. Other concerns/user focus: no additional user focus was supplied. Full changed-file and related-path coverage is recorded in the shared ledger; no other verified issue or unresolved candidate remains.

Validation limits: static source/spec/test inspection only; no physical Parquet/ORC execution, cluster validation or performance measurements. The findings are supported by explicit code paths, not claimed runtime reproductions.

Review converged after 2 round(s).

type = Types.StructType.of(withRetainedRelaxations(type.asStructType().fields(), schemas));
}
relaxed.add(type.equals(field.type()) && optional == field.isOptional()
? field : Types.NestedField.from(field).ofType(type).isOptional(optional).build());

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] Convert DATE defaults before rebuilding a promoted carrier

A non-null DATE default makes this rebuild fail. Iceberg 1.12.0's NestedField.from(field) copies the typed default literals, and the constructor converts them to the new type; DateLiteral.to accepts only DATE, so TIMESTAMP/TIMESTAMP_NS produces Cannot cast default value to timestamp.... See the pinned builder/default conversion and DateLiteral.

For an unpartitioned v3 table, give optional k DATE initial default 2024-01-02, append S1, publish a schema-only TIMESTAMP promotion with the equivalent midnight default, prepare an equality delete, drop k, then commit the pending delete. As in the new temporal tests, publish the promotion through TableMetadata.Builder. The lineage selects S1's DATE definition; retained widening then reaches this builder and aborts FE planning before either reader runs. The previous metadata lookup selected the already-typed TIMESTAMP definition. A copied DATE write default also triggers this.

Convert both defaults into the promoted temporal type before rebuilding and add a default-bearing test. This differs from the existing temporal thread's fixed no-default comparison failure.

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 36bac832997. NestedField's constructor casts both defaults to the field type (castDefault → Literal.to), and in Iceberg 1.12.0 DateLiteral.to returns null for anything but DATE. Rebuilding a dropped DATE key whose lineage definition carries an initial or write default as TIMESTAMP/TIMESTAMP_NS therefore threw Cannot cast default value to timestamp and failed planning for every scan the delete applies to. Master took the already-typed TIMESTAMP definition and never rebuilt it, and branch-4.2 does not widen, so neither fails this way. The SDK promotions (INT→LONG, FLOAT→DOUBLE, decimal precision) convert their literals already.

The relaxation now maps a DATE default to midnight of that day in the promoted type and then rebuilds the field. A date outside the TIMESTAMP_NS range still fails, as the spec requires for this promotion. equalityCarrierConvertsDefaultsOfDateKeyPromotedToTimestamp and equalityCarrierConvertsDefaultsOfDateKeyPromotedToTimestampNs replay your sequence with both defaults set, for TIMESTAMP and TIMESTAMP_NS, and check the converted default in the scan dictionary too.

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.

You're right about the copied write default; fixed in 9affde5af66. The rebuilt carrier field took the write default of the lineage definition. Converting a stale one outside the TIMESTAMP_NS range (1000-01-01 in your history) overflowed in DateTimeUtil.microsToNanos before any reader ran, so planning failed with ArithmeticException: long overflow. The carrier only feeds the scan dictionary, which carries the initial default and never a write default, so a rebuilt field now drops its write default instead of converting it. The initial default is still converted: it cannot change once the field is added, so the retained promoted definition already holds the same value. equalityCarrierDropsStaleWriteDefaultOfPromotedDateKey replays your history (write default cleared with updateColumnDefault, TIMESTAMP_NS published without a snapshot, the delete pending across the drop), and the two earlier default tests now expect no write default on the carrier.

optional |= retained.isOptional();
if (type.isPrimitiveType() && retained.type().isPrimitiveType()
&& isWideningPromotion(type.asPrimitiveType(), retained.type().asPrimitiveType())) {
type = retained.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 the historical DATE domain across future nanos promotions

Scanning every retained schema also imports promotions committed after a time-travel target. DATE-to-TIMESTAMP_NS has a smaller representable range: the v3 specification requires out-of-range values to fail at runtime, so this promotion is not harmless for an earlier DATE read.

Use an unpartitioned v3 table with optional k DATE, a row at 1000-01-01, and a DATE equality delete for 1001-01-01, without column metrics. Drop k and append an id-only file to establish target T. Later restore k's old nullable schema with TableMetadata.Builder.setCurrentSchema(existingId) and publish a TIMESTAMP_NS schema. Nullable-field deletion rollback is allowed by the same spec, and the metadata builder accepts this history.

Reading T should still compare DATEs and retain both rows. Instead, its lineage's DATE carrier is widened by that future schema. V2 _resolve_equality_delete_fields chooses TIMESTAMP_NS and _load_equality_delete_file uses EvolutionCast, whose DATE-to-nanos strict conversion rejects the year-1001 tombstone. The previous lookup stopped at T's schema and kept DATE. Preserve historical comparison domains for range-restricting promotions and add this pinned-read case. This is distinct from the existing temporal thread: the problematic promoted schema is after T, and no defaults are needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The mechanism is right. If a dropped key is restored with set-current-schema and then promoted to TIMESTAMP_NS, a read pinned before the promotion now compares in TIMESTAMP_NS. FileScannerV2's strict DATE→nanos EvolutionCast then rejects any date outside 1677-09-21..2262-04-11 with TIMESTAMP_NS overflow when casting row …. The query fails closed; no row is dropped or kept by mistake.

I'm keeping the widening, because the FE cannot tell whether a retained schema was added before or after the scan target. Schemas carry no timestamp, schema IDs are reused (the defect this PR fixes), and the list position only records when a schema was first added. If we stopped widening along DATE→TIMESTAMP_NS for a schema that might be later than the target, the case covered by equalityCarrierWidensDateKeyPromotedWithoutASnapshot would come back: a delete prepared after a schema-only promotion holds nanosecond tombstones, and a DATE carrier makes FileScannerV2 truncate them to their date and delete rows silently. A fail-closed error is the safer of the two outcomes. Your history also needs a rollback of the drop through set-current-schema, a DATE→TIMESTAMP_NS promotion that the 1.12.0 schema-update API rejects, and dates outside the nanosecond range. After that promotion the table's current read fails on the same values anyway, as the spec requires.

The complete fix belongs in FileScannerV2, which compares every delete file in the one carrier type. Choosing the comparison type per delete file, from its own key type and the data file's (V1 already uses the delete file's type), would handle this case. It would also handle the related one where expireSnapshots().cleanExpiredMetadata(true) removes a schema-only promotion and leaves the carrier too narrow. That is a BE change outside this FE-only PR; I'll track the two together.

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17603	5128	5072	5072
q2	2139	312	249	249
q3	10252	1355	823	823
q4	4680	459	333	333
q5	7437	786	519	519
q6	184	187	150	150
q7	727	785	622	622
q8	9285	1304	1032	1032
q9	5403	4182	4142	4142
q10	6821	1282	1017	1017
q11	430	248	231	231
q12	642	399	289	289
q13	18059	3475	3024	3024
q14	267	273	246	246
q15	q16	714	707	669	669
q17	1187	742	569	569
q18	6947	6138	6109	6109
q19	1096	957	581	581
q20	396	340	231	231
q21	5032	2147	2272	2147
q22	343	305	259	259
Total cold run time: 99644 ms
Total hot run time: 28314 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5576	5588	5598	5588
q2	673	566	533	533
q3	5198	5356	4896	4896
q4	1994	2122	1550	1550
q5	5256	5191	5398	5191
q6	246	203	158	158
q7	2377	2026	1858	1858
q8	2977	2689	2736	2689
q9	7958	7966	7932	7932
q10	3728	3650	3277	3277
q11	601	464	500	464
q12	691	688	538	538
q13	3493	3909	3296	3296
q14	322	325	303	303
q15	q16	721	700	645	645
q17	8266	7539	7019	7019
q18	13017	12185	12927	12185
q19	907	804	807	804
q20	2139	2142	1914	1914
q21	5644	4561	4733	4561
q22	504	450	398	398
Total cold run time: 72288 ms
Total hot run time: 65799 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 cbf21fb05857492ae739b2f8cf412fc8335c4a0e, data reload: false

query5	4324	595	446	446
query6	428	204	192	192
query7	4800	436	229	229
query8	322	164	144	144
query9	8775	3110	3120	3110
query10	483	307	257	257
query11	6018	3841	3595	3595
query12	144	89	86	86
query13	1266	475	356	356
query14	6051	3858	3615	3615
query14_1	3402	3426	3369	3369
query15	204	193	180	180
query16	952	438	417	417
query17	829	635	521	521
query18	2423	420	298	298
query19	193	158	129	129
query20	84	83	81	81
query21	204	126	102	102
query22	13060	13134	12776	12776
query23	13378	12754	12476	12476
query23_1	12586	12519	12435	12435
query24	7363	852	476	476
query24_1	461	455	475	455
query25	542	422	363	363
query26	1259	259	141	141
query27	2789	441	274	274
query28	4598	1938	2028	1938
query29	1726	585	446	446
query30	299	216	183	183
query31	998	920	815	815
query32	143	86	92	86
query33	516	289	233	233
query34	898	861	517	517
query35	726	770	655	655
query36	836	823	751	751
query37	128	102	91	91
query38	1847	1741	1720	1720
query39	821	800	758	758
query39_1	727	743	748	743
query40	217	115	97	97
query41	65	63	63	63
query42	93	87	89	87
query43	351	369	323	323
query44	1368	679	700	679
query45	171	179	167	167
query46	816	959	568	568
query47	2888	2901	2815	2815
query48	300	341	216	216
query49	586	387	285	285
query50	651	269	218	218
query51	10318	10492	10514	10492
query52	83	83	71	71
query53	188	217	169	169
query54	234	202	177	177
query55	72	72	64	64
query56	221	211	215	211
query57	1665	1620	1552	1552
query58	281	258	259	258
query59	2121	2062	1947	1947
query60	284	254	234	234
query61	172	169	160	160
query62	392	332	315	315
query63	194	163	167	163
query64	2888	1082	900	900
query65	3340	3260	3260	3260
query66	1810	431	319	319
query67	20346	20191	20055	20055
query68	3033	1045	644	644
query69	400	310	257	257
query70	881	807	777	777
query71	303	260	214	214
query72	2868	2685	2295	2295
query73	516	527	300	300
query74	4648	4514	4354	4354
query75	2305	2274	1947	1947
query76	2308	1000	584	584
query77	343	395	274	274
query78	9841	9674	9150	9150
query79	995	880	528	528
query80	1240	438	339	339
query81	589	321	275	275
query82	591	133	103	103
query83	303	196	192	192
query84	322	119	99	99
query85	890	424	378	378
query86	399	281	233	233
query87	2002	1912	1802	1802
query88	3691	2665	2613	2613
query89	336	282	240	240
query90	1916	175	176	175
query91	169	150	129	129
query92	100	90	86	86
query93	999	1017	574	574
query94	625	292	265	265
query95	570	387	317	317
query96	654	482	231	231
query97	2456	2440	2354	2354
query98	164	156	147	147
query99	765	752	652	652
Total cold run time: 235760 ms
Total hot run time: 151199 ms

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17597	5147	5042	5042
q2	2088	315	298	298
q3	10219	1443	807	807
q4	4679	466	340	340
q5	7440	808	527	527
q6	176	187	155	155
q7	756	792	599	599
q8	9238	1375	1077	1077
q9	5505	4191	4161	4161
q10	6819	1293	1029	1029
q11	430	250	225	225
q12	641	395	275	275
q13	18090	3868	3026	3026
q14	274	269	239	239
q15	q16	730	718	654	654
q17	1197	749	575	575
q18	7005	6168	6116	6116
q19	1114	960	590	590
q20	374	337	220	220
q21	4783	2525	2423	2423
q22	358	349	269	269
Total cold run time: 99513 ms
Total hot run time: 28647 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5940	5928	6417	5928
q2	740	635	547	547
q3	5625	5724	5361	5361
q4	2044	2192	1559	1559
q5	5786	5765	5655	5655
q6	238	184	144	144
q7	2228	2029	1846	1846
q8	2968	2646	2652	2646
q9	7969	7882	7459	7459
q10	3590	3581	3116	3116
q11	581	427	379	379
q12	663	660	509	509
q13	3250	3609	3054	3054
q14	294	297	280	280
q15	q16	650	697	622	622
q17	7777	7017	6867	6867
q18	12985	12215	12843	12215
q19	901	783	765	765
q20	2147	2144	1917	1917
q21	5607	4654	4752	4654
q22	503	447	403	403
Total cold run time: 72486 ms
Total hot run time: 65926 ms

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

Request changes: one new P1 was verified at head 36bac83. Recovery of an UNKNOWN-origin equality key can ignore valid v3 promotions and make FileScannerV2 retain a row covered by a committed equality delete. The inline comment gives the complete triggering history.

Reviewed both changed files and their upstream/downstream paths with two complete-review agents and a separate risk reviewer. This is static inspection only: no builds, tests, or runtime reproduction were executed. No additional user focus was supplied.

Existing-thread dispositions:

  • P1 4359014047 and 4363903193 are fixed for their reported INT/LONG and DATE-origin temporal scenarios. No existing P0/P1 remains independently confirmed in its reported form, so existing_blocking_comment_ids is empty.
  • P2 4363903205 is fixed: retained optionality is preserved, with an always-required control.
  • P2 4366960335: the reported in-range DATE initial/write literals now convert correctly, including nanosecond scaling. A residual copied-write-default failure remains: S1 has optional k DATE, initial default NULL, write default 1000-01-01, and explicitly written in-range data. Clear the write default without a snapshot, publish DATE-to-TIMESTAMP_NS with null defaults, prepare a matching in-range delete, drop k, then commit the pending delete. The recovered S1 definition still carries the obsolete write default; converting it at line 2376 overflows before either reader, although readers never use write defaults. Dropping that obsolete write default from the read carrier avoids this failure. This remains under the existing copied-DATE-write-default concern; no duplicate inline is posted.
  • P2 4366960337 still applies: a later retained nanos schema can impose a smaller comparison range on a historical dropped DATE key, causing a strict overflow error. This known fail-closed time-travel limitation is not reposted. Metadata cleanup that removes the only promoted schema is also already discussed in existing replies.

The reviewed runtime chain is:

getScanNodeProperties -> frozen table + pinned scan/schema
  -> selected-snapshot equality IDs -> lineage + metadata fallback
  -> retained relaxations -> history_schema_info
  -> V1 Parquet/ORC defaults and V2 equality comparisons

Individual code-review skill Part 1.3 conclusions:

  1. Goal and proof: the lineage walk and full fallback address reused schema IDs and expired ancestors in the added metadata tests. The new UNKNOWN case remains a correctness gap.
  2. Scope: changes are focused on two Java files; no unrelated SPI or architecture change.
  3. Concurrency and locks: immutable statement metadata supplies one generation; all new collections are invocation-local. No new threads, locks, lock ordering, or work under locks.
  4. Lifecycle and static initialization: fields are immutable request carriers; existing table owners retain FileIO. No new resource handles, ownership cycles, or static initialization dependency.
  5. Configuration: no new settings; existing type-mapping flags still reach serialization.
  6. Compatibility: existing dictionary shape and scan-semantics gates are reused. Equality-delete scans already require current backend semantics during rolling upgrades.
  7. Thrift/protobuf: no definitions or IDs changed.
  8. Parallel paths: V1 Parquet and ORC share the carrier/default handling; V2 uses the carrier comparison type. The new silent-mismatch finding is on V2. Metadata system tables do not use this data-carrier path.
  9. Special conditions: repeated schema IDs are indexed once; null/expired ancestry falls back; missing metadata and invalid nonprimitive keys fail explicitly. Nested valid keys preserve stable-ID struct ancestors and current fields first.
  10. Test coverage: added FE tests cover reused IDs, expired ancestry, lineage preference, pending promotions, optionality and temporal defaults. UNKNOWN-origin recovery and physical-row equality results are not covered. End-to-end Parquet/ORC and BE execution remain validation limits.
  11. Test results: no recorded result artifacts changed; assertions were inspected and no execution is claimed.
  12. Observability: existing metadata, requiredness and cast errors cover failure paths; no new metric or INFO logging is needed for this metadata-selection change.
  13. Persistence, transactions and failover: production changes only plan reads from committed external snapshots; no EditLog or commit mutation. Snapshot/schema selection stays within the frozen statement generation.
  14. Writes, atomicity and crashes: no production write path changes. The P1 violates committed-delete visibility on reads; no new crash cleanup or write-atomicity obligation was introduced.
  15. FE-BE variables: existing type, optionality and initial-default fields carry the changes through shared serialization; no new transport variable or sending point.
  16. Performance: metadata-only ancestry/history work; no data-file enumeration added. Carrier size stays limited to current fields plus referenced keys/ancestors. No measured performance claim; node-scoped delete caching does not mix schemas.
  17. Other checks/core invariants: committed-delete correctness requires the inline fix. Internal partition versions and MoW bitmaps are not involved; BE allocation/ownership is unchanged. Missing/inconsistent metadata fails loudly. All candidates and risk focuses were accepted, deduplicated, or dismissed with evidence; the known P2 limitations and static validation scope remain explicit.

The review converged after two rounds on the unchanged declared finding set; convergence does not mean the PR is ready to merge.

Review converged after 2 round(s).

if (TypeUtil.isPromotionAllowed(from, to)) {
return true;
}
if (from.typeId() != Type.TypeID.DATE) {

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.

[P1] Follow UNKNOWN promotions when recovering a dropped equality key

V3 also permits UNKNOWN to become any concrete type (promotion table). The pinned SDK helper does not recognize that edge, and this non-DATE return leaves a recovered UNKNOWN unchanged.

Use an unpartitioned v3 table with optional k UNKNOWN and commit S1 with k omitted. Publish schema-only UNKNOWN→DATE, prepare a DATE append containing k=2024-01-02, publish schema-only DATE→TIMESTAMP_NS, prepare a matching midnight nanos equality delete, then drop k and commit the pending append followed by the delete. Publish the schemas through TableMetadata.Builder, as the temporal tests do, and omit all column metrics so pruning retains the delete. Both pending snapshots record the dropped schema, so lineage recovery finds only S1's UNKNOWN.

Retained DATE/NANO definitions never replace UNKNOWN. IcebergSchemaUtils encodes it as STRING; V2 restores that carrier type and casts both keys to strings. The DATE becomes 2024-01-02, while the matching tombstone becomes 2024-01-02 00:00:00.000000000, so the deleted row remains visible. The previous reverse metadata lookup selected the retained nanos definition and matched correctly.

Recognize UNKNOWN→concrete promotions and cover this pending-writer history. This differs from the existing DATE-origin thread: the new helper never reaches its DATE handling when the lineage definition is UNKNOWN.

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 9affde5af66. The v3 promotion table lets unknown become any type, but Iceberg 1.12.0's TypeUtil.isPromotionAllowed and SchemaUpdate do not implement it, and isWideningPromotion only added DATE on top. In your history the only snapshot that records k is S1, written while k was unknown, so the carrier kept unknown. The scan dictionary encodes unknown as STRING, and FileScannerV2 then casts both keys to STRING: the DATE key becomes 2024-01-02 and the TIMESTAMP_NS tombstone 2024-01-02 00:00:00.000000000, so the deleted row stays visible. Master's reverse lookup took the retained TIMESTAMP_NS definition, so this PR introduced the regression. V1 compares in the delete file's own key type and was not affected.

isWideningPromotion now treats unknown as promotable to any primitive type, so the recovered definition takes the widest retained promotion, TIMESTAMP_NS here. An unknown field only has null defaults, so no default conversion is involved. equalityCarrierFollowsUnknownKeyPromotedWithoutASnapshot replays your history (both promotions published as schema-only changes, a pending append and a pending delete committed after the drop) and checks the carrier and the scan dictionary; without the new rule the carrier stays unknown.

If metadata cleanup has removed every schema that gave k a concrete type, the carrier has nothing to promote to and stays unknown, as on master. That is the same limitation as the cleanup case discussed earlier: the comparison type then has to come from the files, which I'm tracking as the FileScannerV2 follow-up.

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150830 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 36bac83299707215437647401f4c739a2e885f8b, data reload: false

query5	4316	581	425	425
query6	431	201	194	194
query7	4816	449	220	220
query8	320	173	156	156
query9	8764	3045	3067	3045
query10	468	304	247	247
query11	5949	3834	3576	3576
query12	146	92	87	87
query13	1292	431	311	311
query14	6131	3898	3609	3609
query14_1	3421	3394	3407	3394
query15	208	194	184	184
query16	941	411	405	405
query17	855	688	542	542
query18	2406	427	306	306
query19	197	170	139	139
query20	88	84	86	84
query21	217	125	104	104
query22	12951	12982	12726	12726
query23	13521	12747	12492	12492
query23_1	12488	12675	12615	12615
query24	7374	824	473	473
query24_1	482	481	464	464
query25	531	433	370	370
query26	1264	257	139	139
query27	2751	436	275	275
query28	4609	1957	1934	1934
query29	1561	601	457	457
query30	296	221	186	186
query31	1022	924	805	805
query32	138	96	92	92
query33	547	306	248	248
query34	881	840	483	483
query35	733	740	633	633
query36	837	812	760	760
query37	133	103	85	85
query38	1772	1749	1673	1673
query39	788	778	766	766
query39_1	736	730	738	730
query40	224	114	99	99
query41	74	71	68	68
query42	86	81	82	81
query43	331	364	309	309
query44	1283	676	684	676
query45	177	180	167	167
query46	820	973	585	585
query47	2946	2943	2805	2805
query48	287	302	213	213
query49	565	399	294	294
query50	659	265	208	208
query51	10793	10590	10445	10445
query52	77	80	69	69
query53	180	203	151	151
query54	252	191	197	191
query55	69	64	61	61
query56	245	227	224	224
query57	1660	1602	1548	1548
query58	276	260	251	251
query59	2186	2170	1982	1982
query60	291	244	224	224
query61	146	146	149	146
query62	384	325	279	279
query63	191	153	154	153
query64	2744	937	793	793
query65	3306	3285	3261	3261
query66	1770	412	302	302
query67	20104	20095	20039	20039
query68	3256	980	600	600
query69	394	276	243	243
query70	829	747	750	747
query71	298	230	202	202
query72	2772	2562	2315	2315
query73	529	525	290	290
query74	4647	4492	4352	4352
query75	2334	2286	1902	1902
query76	2391	999	590	590
query77	338	368	267	267
query78	9931	9764	9211	9211
query79	979	828	506	506
query80	822	429	339	339
query81	572	324	275	275
query82	574	143	106	106
query83	295	194	190	190
query84	303	121	104	104
query85	871	437	384	384
query86	355	241	227	227
query87	1992	1941	1811	1811
query88	3650	2633	2624	2624
query89	337	282	240	240
query90	1780	165	165	165
query91	158	156	131	131
query92	100	86	90	86
query93	991	980	566	566
query94	516	300	265	265
query95	584	327	302	302
query96	669	473	228	228
query97	2429	2436	2341	2341
query98	158	148	147	147
query99	773	762	673	673
Total cold run time: 234906 ms
Total hot run time: 150830 ms

@hello-stephen

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

query1	0.01	0.01	0.00
query2	0.10	0.05	0.04
query3	0.26	0.09	0.08
query4	1.62	0.10	0.10
query5	0.24	0.24	0.25
query6	1.16	0.68	0.68
query7	0.04	0.01	0.01
query8	0.05	0.04	0.04
query9	0.41	0.35	0.35
query10	0.61	0.62	0.58
query11	0.20	0.14	0.14
query12	0.18	0.15	0.15
query13	0.49	0.49	0.50
query14	1.04	1.02	1.02
query15	0.63	0.60	0.61
query16	0.35	0.36	0.36
query17	1.17	1.16	1.11
query18	0.21	0.20	0.20
query19	2.11	1.92	2.01
query20	0.02	0.02	0.01
query21	15.48	0.20	0.14
query22	4.82	0.06	0.05
query23	16.29	0.33	0.13
query24	3.02	0.45	0.37
query25	0.09	0.06	0.06
query26	0.84	0.22	0.15
query27	0.05	0.04	0.05
query28	3.57	0.77	0.37
query29	12.54	4.09	3.26
query30	0.27	0.14	0.10
query31	2.77	0.57	0.31
query32	3.21	0.60	0.51
query33	3.13	3.27	3.23
query34	15.36	3.97	3.38
query35	3.29	3.33	3.34
query36	0.56	0.42	0.43
query37	0.09	0.07	0.06
query38	0.05	0.04	0.04
query39	0.03	0.02	0.02
query40	0.17	0.15	0.14
query41	0.09	0.04	0.03
query42	0.04	0.03	0.02
query43	0.04	0.04	0.03
Total cold run time: 96.7 s
Total hot run time: 24.31 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage `` 🎉
Increment coverage report
Complete coverage report

…s of a dropped equality-delete key

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69536

Problem Summary: A dropped Iceberg equality-delete key takes its
definition from the scan's snapshot lineage and then takes the widest
promotion its field has in any retained schema. The promotion check was
TypeUtil.isPromotionAllowed plus the v3 promotion of DATE to an unzoned
TIMESTAMP or TIMESTAMP_NS. It missed the v3 promotion of unknown to any
type, which Iceberg 1.12.0's schema update does not implement but the
format allows (PyIceberg's update_column and a REST add-schema do it).
On a v3 table whose key was created as unknown, a snapshot committed
while it was unknown, and the key was promoted to DATE and then
TIMESTAMP_NS without snapshots and dropped before a pending append and a
pending equality delete committed, the only snapshot that records the
key has it as unknown. The carrier kept unknown, the scan dictionary
sent it as STRING, and FileScannerV2 cast both keys to STRING: a DATE
key became 2024-01-02 while its TIMESTAMP_NS tombstone became
2024-01-02 00:00:00.000000000, so the deleted row stayed visible. The
lookup before this PR took the retained TIMESTAMP_NS definition. V1
compares in the delete file's own key type and was not affected.

The previous commit also converted the write default of a rebuilt key
to the promoted type. A stale DATE write default outside the
TIMESTAMP_NS range, recorded by the snapshot the lineage selects and
cleared later without a snapshot, overflowed in DateTimeUtil.microsToNanos
and failed planning with "long overflow". The carrier only feeds the
scan dictionary, which carries the initial default and never a write
default.

unknown now promotes to any primitive type when the carrier takes a
key's retained promotions, and a rebuilt key drops its write default
instead of converting it. The initial default is still converted: it
cannot change once the field is added, so the retained promoted
definition holds the same value.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - IcebergScanPlanProviderTest: a v3 key created as unknown, promoted to DATE and then TIMESTAMP_NS without snapshots and dropped before a pending append and a pending delete commit, takes TIMESTAMP_NS in the carrier and the scan dictionary; a DATE key whose recorded write default is outside the TIMESTAMP_NS range, cleared and promoted to TIMESTAMP_NS without snapshots, plans without converting it; a rebuilt DATE key keeps only its converted initial default
    - fe-connector-iceberg module tests
- Behavior changed: No (fixes regressions of this PR's previous commits)
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@morningman

Copy link
Copy Markdown
Contributor Author

Addressed the fourth review round in 9affde5af66. Per finding:

  • 4367404920 (P1, a dropped key recovered as unknown): fixed. unknown now promotes to any primitive type when the carrier takes a key's retained promotions, so the key keeps the type its tombstones were written in.
  • 4366960335 (residual, copied DATE write default): fixed. A rebuilt carrier field drops its write default, which no reader uses, instead of converting it.

Two new tests in IcebergScanPlanProviderTest; all 1702 tests of fe-connector-iceberg pass. The PR description is updated.

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

Request changes for one new P1: retained TIMESTAMP and TIMESTAMP_NS alternatives can make a dropped equality key use a lossy comparison type after a schema-only rollback, incorrectly filtering a committed row. The inline finding gives the complete trigger and FE-to-BE path.

The schema-ID reuse fix itself is supported by the lineage-first lookup and full-history fallback tests. I independently revalidated the existing P1 threads 4359014047 (INT/LONG), 4363903193 (direct DATE promotions), and 4367404920 (UNKNOWN): their reported triggers are fixed at this head, so none is carried forward. The optionality, DATE-default and stale write-default fixes are also present. Existing P2 thread 4366960337 still describes the acknowledged future DATE-to-nanos range failure; it is already covered and is not reposted. Loss of schema-only type history after metadata cleanup is also an already-known reader limitation.

Critical checkpoint conclusions:

Checkpoint Conclusion
Goal and evidence Dropped-key recovery now handles reused schema IDs and expired parents; the new competing-temporal-target case remains incorrect. Added tests prove FE carrier/dictionary expectations, not final BE rows.
Scope and clarity Changes stay within equality-carrier planning and focused JUnit tests. The claim that relaxation is independent of schema order is contradicted by the inline case.
Concurrency and locks Recovery uses local collections over statement-frozen metadata. No new thread, lock, lock-order or critical-section work.
Lifecycle and initialization No new static initialization, external ownership or closeable resource; existing table/FileIO lifetime is preserved.
Configuration No new configuration or dynamic-setting behavior.
Compatibility and upgrades Existing dictionary format and plugin API are retained. The existing current-backend-semantics fence covers equality-delete scans.
Thrift/protobuf IDs No IDL change; cross-branch field-ID audit is not applicable.
Parallel paths Ordinary, streaming and cached scans share this dictionary. Both readers consume it; V2 uses the carrier comparison type, whereas V1 compares in the delete-file type.
Conditions and failures Missing-key, expired-parent and primitive-key checks follow explicit metadata cases; unresolved IDs fail planning. No ignored status or silent exception fallback added.
Test coverage New histories cover reuse, expiry, lineage preference, promotions, optionality and defaults. Existing tests cover nested recovery and fast paths. The competing temporal alternatives need a new case.
Test results No expected-result files changed. No builds or tests were run under this static-only contract; the author's reported test execution was not independently reproduced.
Observability Existing schema/key-ID errors and scan diagnostics are adequate for the added planning logic; no new per-row logging or metric is needed.
Transactions and persistence No Doris transaction state, EditLog or failover protocol changed; scan metadata remains pinned.
Data writes and crashes No production writer or atomicity change, native allocation ownership or new crash-recovery obligation. The reported defect affects read correctness.
FE/BE propagation Existing TField type, optionality and initial-default members carry the result through the shared encoder/apply path; no new wire field.
Performance No added per-data-file work. Parent traversal skips repeated schema indexing; recovered fields scan retained schemas. Full SDK schema-ID maps are already initialized by existing DeleteFileIndex planning. No separate performance defect verified.
Other concerns and limits Nested struct recovery, name/ID precedence, defaults, null schema IDs and expiration were reviewed. No other new verified finding or unresolved candidate remains.

User focus: no additional focus was supplied. Both changed files and their upstream/downstream consumers were reviewed. Validation remains static: the pending-writer and v3 temporal/UNKNOWN histories lack newly added physical row-level execution evidence.

Review converged after 2 round(s).

}
optional |= retained.isOptional();
if (type.isPrimitiveType() && retained.type().isPrimitiveType()
&& isWideningPromotion(type.asPrimitiveType(), retained.type().asPrimitiveType())) {

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.

[P1] Resolve competing temporal targets before choosing the carrier type

This single pass can keep an abandoned TIMESTAMP definition and reject the TIMESTAMP_NS definition actually used by the delete. On an unpartitioned v3 table, append S1 with optional k DATE = 2024-01-02; publish a schema-only DATE→TIMESTAMP promotion; restore the original DATE schema with TableMetadata.Builder.setCurrentSchema(existingId) before any new snapshot or data is written; then publish DATE→TIMESTAMP_NS. Both forward promotions are allowed by the v3 promotion table, and setCurrentSchema accepts the retained ID. Prepare a nanos equality delete for 2024-01-02T00:00:00.000000001, drop k, and commit the pending delete. Omit all column metrics so the delete reaches BE.

The lineage supplies DATE and retained order DATE/TIMESTAMP/TIMESTAMP_NS/dropped first selects TIMESTAMP. TIMESTAMP→TIMESTAMP_NS is not a recognized promotion, so the carrier stays DATETIMEV2(6). V2 _resolve_equality_delete_fields uses that carrier type, and _load_equality_delete_file casts the nanos tombstone to it; round_timestamp_ns_fraction rounds 1ns to zero. The original midnight row is incorrectly deleted. The base reverse lookup selected TIMESTAMP_NS and kept it.

Handle competing retained temporal domains without a lossy comparison (or fail explicitly), and cover this rollback history. This differs from the existing DATE thread: both DATE promotion edges are implemented here, but the first retained alternative blocks the other; it is also a current, in-range wrong-result case rather than the existing time-travel range 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 b4bb074bfed. The relaxation compared each retained type with the type it had reached so far, so its result depended on the order of the schema list; my earlier statement that it did not was wrong. This only matters when a key has two retained promotions that do not promote to each other: in v3, DATE to both TIMESTAMP and TIMESTAMP_NS, or unknown to two types, and both can be retained only after the current schema is set back to an earlier one (or after a type change that is not a valid promotion). Your history lists DATE, TIMESTAMP, TIMESTAMP_NS, so the loop stopped at TIMESTAMP, and FileScannerV2 rounded the 1 ns tombstone to midnight in DATETIMEV2(6) and deleted the DATE row. Master's reverse lookup took TIMESTAMP_NS, so this PR introduced the regression; V1 compares in the delete file's own type and was not affected. The unknown rule from the previous round has the same flaw: after unknown→INT, a rollback and unknown→STRING, the carrier was INT and turned a non-numeric STRING tombstone into NULL, which matches every row written while k was unknown.

No single carrier type is lossless for every such history, and the metadata does not record which type a delete file was written in. TIMESTAMP drops nanoseconds, and TIMESTAMP_NS cannot hold DATE or TIMESTAMP values outside 1677-09-21..2262-04-11, which FileScannerV2 rejects for DATE and turns into NULL for TIMESTAMP. The carrier now takes the promotion that every other retained promotion of the key promotes to, and planning fails with Iceberg equality-delete field 2 (k) was promoted to both timestamp and timestamp_ns in schema history when there is none. Only promotions of the definition the lineage selected count, so a delete committed while TIMESTAMP_NS was current still resolves TIMESTAMP_NS next to the abandoned TIMESTAMP schema. New tests replay your history in both orders and the unknown variant, and cover that lineage case.

One related history stays as on master and branch-4.2: if a delete is prepared under a TIMESTAMP_NS schema that is then abandoned, and a snapshot is committed while TIMESTAMP is current, the lineage records TIMESTAMP and the carrier keeps it. Telling those apart needs the delete file's own key type, which is the FileScannerV2 follow-up I'm tracking with the cleanup case.

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17600	5146	5077	5077
q2	2118	319	289	289
q3	10200	1353	812	812
q4	4687	456	337	337
q5	7419	773	520	520
q6	179	187	150	150
q7	747	804	596	596
q8	9264	1337	1021	1021
q9	5442	4166	4148	4148
q10	6821	1279	1036	1036
q11	424	239	224	224
q12	640	398	280	280
q13	18078	3541	3026	3026
q14	278	273	244	244
q15	q16	712	718	683	683
q17	1178	739	579	579
q18	6982	6178	6100	6100
q19	1097	961	602	602
q20	371	331	236	236
q21	4691	2130	2288	2130
q22	350	295	247	247
Total cold run time: 99278 ms
Total hot run time: 28337 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5606	5568	5600	5568
q2	665	577	525	525
q3	5188	5416	4942	4942
q4	2029	2143	1543	1543
q5	5221	5148	5414	5148
q6	251	210	151	151
q7	2350	2051	1913	1913
q8	2957	2703	2657	2657
q9	8023	8024	7950	7950
q10	3726	3667	3221	3221
q11	605	420	408	408
q12	716	860	545	545
q13	3539	3927	3206	3206
q14	322	305	293	293
q15	q16	701	738	656	656
q17	8323	7444	7229	7229
q18	12994	12196	12852	12196
q19	908	805	790	790
q20	2150	2169	1904	1904
q21	5741	4638	4774	4638
q22	503	479	401	401
Total cold run time: 72518 ms
Total hot run time: 65884 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150473 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 9affde5af66c32f657dd32549c1d1bc97d9ca630, data reload: false

query5	4312	581	445	445
query6	432	199	197	197
query7	4808	436	253	253
query8	320	167	148	148
query9	8780	3105	3137	3105
query10	478	324	261	261
query11	6033	3842	3553	3553
query12	158	90	87	87
query13	1245	431	311	311
query14	6116	3970	3701	3701
query14_1	3499	3462	3447	3447
query15	216	194	184	184
query16	962	465	408	408
query17	865	642	556	556
query18	2416	416	303	303
query19	200	155	137	137
query20	84	81	83	81
query21	208	126	105	105
query22	12939	12981	12878	12878
query23	13374	12866	12456	12456
query23_1	12550	12481	12408	12408
query24	7385	808	448	448
query24_1	456	454	453	453
query25	512	399	334	334
query26	1260	254	137	137
query27	2767	435	272	272
query28	4657	1926	1961	1926
query29	1560	646	441	441
query30	308	219	183	183
query31	1001	925	807	807
query32	145	91	87	87
query33	512	302	227	227
query34	974	834	490	490
query35	729	759	634	634
query36	819	844	699	699
query37	120	97	81	81
query38	1765	1751	1672	1672
query39	788	803	743	743
query39_1	737	727	730	727
query40	205	112	91	91
query41	66	63	63	63
query42	86	83	91	83
query43	339	366	302	302
query44	1275	676	671	671
query45	176	177	165	165
query46	827	958	560	560
query47	2987	2790	2766	2766
query48	297	322	225	225
query49	590	421	312	312
query50	718	279	213	213
query51	10405	10349	10313	10313
query52	86	79	73	73
query53	187	211	152	152
query54	259	221	191	191
query55	71	67	64	64
query56	265	244	223	223
query57	1674	1594	1541	1541
query58	326	260	252	252
query59	2119	2181	1953	1953
query60	282	246	223	223
query61	142	149	145	145
query62	385	332	277	277
query63	180	151	162	151
query64	2728	924	743	743
query65	3304	3263	3261	3261
query66	1780	425	297	297
query67	20034	20139	19959	19959
query68	2921	1024	608	608
query69	391	280	248	248
query70	846	769	752	752
query71	284	231	200	200
query72	2757	2574	2338	2338
query73	544	531	289	289
query74	4626	4535	4349	4349
query75	2362	2277	1893	1893
query76	2302	1001	570	570
query77	338	409	277	277
query78	9582	9615	9222	9222
query79	876	842	517	517
query80	474	410	339	339
query81	507	324	267	267
query82	203	128	110	110
query83	193	195	188	188
query84	288	128	104	104
query85	771	420	365	365
query86	256	242	225	225
query87	1948	1935	1812	1812
query88	3576	2674	2664	2664
query89	314	289	243	243
query90	2110	162	164	162
query91	164	152	128	128
query92	99	83	91	83
query93	976	1003	565	565
query94	441	303	272	272
query95	568	402	317	317
query96	659	492	234	234
query97	2459	2443	2323	2323
query98	163	152	146	146
query99	755	771	639	639
Total cold run time: 232844 ms
Total hot run time: 150473 ms

@hello-stephen

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

query1	0.01	0.01	0.00
query2	0.10	0.05	0.05
query3	0.26	0.08	0.08
query4	1.65	0.10	0.09
query5	0.24	0.24	0.25
query6	1.18	0.68	0.67
query7	0.04	0.01	0.01
query8	0.06	0.04	0.04
query9	0.41	0.36	0.33
query10	0.58	0.57	0.56
query11	0.21	0.15	0.14
query12	0.17	0.15	0.15
query13	0.48	0.47	0.49
query14	1.02	1.01	1.01
query15	0.64	0.60	0.62
query16	0.34	0.35	0.35
query17	1.14	1.13	1.18
query18	0.22	0.20	0.21
query19	2.11	2.02	1.91
query20	0.02	0.01	0.01
query21	15.50	0.19	0.14
query22	4.78	0.06	0.06
query23	16.27	0.32	0.13
query24	3.10	0.65	0.39
query25	0.10	0.07	0.04
query26	1.30	0.23	0.17
query27	0.03	0.04	0.04
query28	2.93	0.77	0.36
query29	12.50	4.12	3.24
query30	0.28	0.13	0.10
query31	2.77	0.55	0.32
query32	3.23	0.60	0.50
query33	3.15	3.24	3.16
query34	15.30	4.00	3.37
query35	3.31	3.35	3.38
query36	0.54	0.44	0.44
query37	0.09	0.07	0.06
query38	0.05	0.03	0.04
query39	0.04	0.03	0.03
query40	0.18	0.15	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.49 s
Total hot run time: 24.2 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage `` 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 100% (0/0) 🎉
Increment coverage report
Complete coverage report

… key regardless of schema order

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69536

Problem Summary: A dropped Iceberg equality-delete key takes its
definition from the scan's snapshot lineage and then the widest
promotion its field has in any retained schema. The relaxation compared
each retained type with the type it had reached so far and switched
whenever that was a promotion, so the result depended on the order of
the metadata schema list. The v3 promotions added by this PR give a
type two successors that do not promote to each other: DATE to both
TIMESTAMP and TIMESTAMP_NS, and unknown to any two types. Both are
retained once the current schema is set back to an earlier one (a REST
set-current-schema, or republishing an identical schema, which reuses
its ID) and the key is promoted again.

On a v3 table whose DATE key was promoted to TIMESTAMP without a
snapshot, set back to DATE, promoted to TIMESTAMP_NS and dropped before
a pending equality delete written under TIMESTAMP_NS committed, the
schema list holds DATE, TIMESTAMP, TIMESTAMP_NS and the loop stopped at
TIMESTAMP. FileScannerV2 compares keys in the carrier's type,
DATETIMEV2(6), so the tombstone 2024-01-02 00:00:00.000000001 rounded
to midnight and deleted the DATE row. The lookup before this PR took
TIMESTAMP_NS. The unknown promotion added by the previous commit has
the same flaw: after unknown to INT, a rollback and unknown to STRING,
the carrier was INT, and a non-numeric STRING tombstone cast to NULL,
which matched every row written while the key was unknown. V1 compares
in the delete file's own key type and was not affected.

No single carrier type is lossless for these histories: TIMESTAMP drops
nanoseconds, TIMESTAMP_NS cannot hold values outside 1677-09-21 to
2262-04-11, and the metadata does not record which type a delete file
was written in. The carrier now collects the retained types that the
selected definition promotes to and takes the one every other one
promotes to, whatever the schema order. When there is none, planning
fails with "Iceberg equality-delete field <id> (<name>) was promoted to
both <type> and <type> in schema history". Only promotions of the
lineage's definition count, so a delete committed while TIMESTAMP_NS
was current still resolves TIMESTAMP_NS beside an abandoned TIMESTAMP
schema.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - IcebergScanPlanProviderTest: a v3 DATE key promoted to TIMESTAMP and to TIMESTAMP_NS around a rollback of the current schema, in either order, and an unknown key promoted to INT and to STRING the same way, fail planning with an error that names both types; a delete committed while TIMESTAMP_NS is current keeps TIMESTAMP_NS beside the abandoned TIMESTAMP schema
    - fe-connector-iceberg module tests
- Behavior changed: Yes (when retained schemas promote a dropped equality-delete key to two types that do not promote to each other, planning fails with an explicit error instead of taking whichever type the schema list holds first)
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@morningman

Copy link
Copy Markdown
Contributor Author

Addressed the fifth review round in b4bb074bfed. Per finding:

  • 4369032165 (P1, TIMESTAMP and TIMESTAMP_NS competing after a schema rollback): fixed. A dropped key's carrier type no longer depends on the order of the schema list: it is the retained promotion that every other one promotes to, and planning fails with an explicit error when a rollback left two unrelated promotions (DATE to both TIMESTAMP and TIMESTAMP_NS, or unknown to two types). This also covers the unknown variant of the same history.

Four new tests in IcebergScanPlanProviderTest; all 1706 tests of fe-connector-iceberg pass. The PR description is updated.

@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 exact head b4bb074bfed7b9bb6337254b29a20ddde76ce683 against the supplied merge-base diff. No additional verified finding warrants a new inline comment.

The four earlier P1 findings are addressed at this head: retained numeric promotions (4359014047), v3 DATE promotions (4363903193), UNKNOWN promotions (4367404920), and competing retained promotion targets (4369032165). I independently checked the implementation and corresponding regression histories; none remains a P0/P1 blocker to carry forward. The optionality and DATE-default threads are also addressed, including discarding stale write defaults.

The existing historical DATE-domain finding still applies: a later retained TIMESTAMP_NS schema can make an earlier DATE read fail for out-of-range keys. This is already reported and intentionally retained as a fail-closed limitation, so it is not reposted. The earlier replies also identify the remaining reader-level limits when metadata cleanup removes the only promoted definition or a pending delete was written under an abandoned type. A carrier derived only from metadata cannot distinguish those file types; this review does not claim those known limits are resolved.

Critical checkpoint conclusions:

  1. Goal and data correctness: lineage-first lookup recovers live dropped equality keys when schema IDs are reused; retained relaxations preserve supported promotions, optionality and initial defaults. Incomparable retained targets fail explicitly. Tests cover reused IDs, expired ancestors and these recovery cases.
  2. Scope and reuse: the change is confined to one Iceberg scan-planning class and its FE unit tests, reusing existing manifest discovery, TypeUtil projection and schema-dictionary encoding.
  3. Concurrency and locks: reconstruction adds method-local state, no threads or locks, and performs no refresh or shared mutation. Existing statement/cache metadata and FileIO ownership mechanisms are unchanged; no new lock-order or concurrency defect was found.
  4. Lifecycle and memory: no new persistent owner, resource handle, callback or static initializer. Temporary collections remain planning-scoped and existing statement/FileIO ownership is preserved.
  5. Configuration: no settings or dynamic-update behavior are added.
  6. Compatibility: no exported connector SPI, storage-format or function-symbol change. Existing backend scan-semantics capability checks continue to apply.
  7. Thrift and protobuf: no definitions, IDs or enums change.
  8. Parallel paths: the existing dictionary reaches both V1 and V2; V1 Parquet/ORC hidden-key resolution and V2 comparison-domain selection were traced. System-table handling is unchanged.
  9. Conditional checks and errors: schema reuse, null schema IDs, expired parents, missing key definitions and competing promotions have concrete paths. Ambiguous domains raise a field-specific exception rather than selecting a lossy type.
  10. Test coverage: added JUnit5 metadata histories cover numeric/DATE/UNKNOWN promotions, initial and stale write defaults, optionality, incomparable targets in both orders, time-travel names and expired lineage. Existing nested-key, drop/re-add and carrier-size cases remain applicable. These FE fixtures validate metadata and dictionary behavior, not complete file-value interoperability.
  11. Test-result changes: no regression output files change. Assertions and expected errors were inspected; no build or test was run, as required by this static-only review contract.
  12. Observability: new ambiguity errors include field ID, name and competing types. No new distributed operation or per-row logging needs instrumentation.
  13. Transactions and persistence: production changes only read committed external snapshot metadata; Doris EditLog, replay and write-transaction handling are unchanged.
  14. Writes, atomicity and crash cleanup: no production write path or new cleanup obligation is introduced; metadata commits in the diff are test fixtures.
  15. FE/BE propagation: recovered fields use the existing type/nullability/initial-default dictionary representation; there is no new transmitted variable or serialization route.
  16. Performance: current equality keys keep the fast exit. Dropped keys walk retained parents, index each distinct lineage schema once and inspect retained definitions per recovered field. No data-file/split enumeration is added, and output is limited to current fields plus referenced historical keys and ancestors. No new actionable performance defect was verified; this was not benchmarked.
  17. Other invariants and user focus: no additional user focus was supplied. Internal rowset versions/MoW delete bitmaps are outside this external-table planning change. No further verified regression was found in the reviewed scope.

Promotion and literal semantics were checked against the pinned Iceberg 1.12 specification and literal conversion implementation. Review coverage is static; runtime, build/style execution and external-writer interoperability remain unexecuted validation limits.

Review converged after 2 round(s).

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17624	5121	5043	5043
q2	2129	314	257	257
q3	10221	1361	794	794
q4	4691	467	333	333
q5	7420	801	529	529
q6	173	181	148	148
q7	741	801	603	603
q8	9240	1326	1054	1054
q9	5474	4190	4161	4161
q10	6817	1284	1039	1039
q11	430	250	223	223
q12	632	395	273	273
q13	18131	3556	3013	3013
q14	286	269	246	246
q15	q16	724	755	678	678
q17	1211	754	568	568
q18	7009	6205	6100	6100
q19	1106	961	590	590
q20	416	352	239	239
q21	4718	2122	2419	2122
q22	348	301	247	247
Total cold run time: 99541 ms
Total hot run time: 28260 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5594	5530	5557	5530
q2	653	557	519	519
q3	5231	5364	4966	4966
q4	1994	2142	1533	1533
q5	5268	5099	5469	5099
q6	275	208	162	162
q7	2385	2105	1917	1917
q8	3011	2926	2665	2665
q9	8044	7889	8112	7889
q10	3727	3648	3295	3295
q11	604	435	414	414
q12	653	702	508	508
q13	3533	3969	3252	3252
q14	317	326	278	278
q15	q16	673	691	630	630
q17	8362	7418	7342	7342
q18	12913	12141	12868	12141
q19	879	768	757	757
q20	2133	2157	1893	1893
q21	5545	4588	4721	4588
q22	495	448	394	394
Total cold run time: 72289 ms
Total hot run time: 65772 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150538 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 b4bb074bfed7b9bb6337254b29a20ddde76ce683, data reload: false

query5	4331	580	457	457
query6	426	208	192	192
query7	4811	446	228	228
query8	319	168	151	151
query9	8789	3118	3115	3115
query10	487	300	276	276
query11	5977	3881	3565	3565
query12	146	95	98	95
query13	1287	460	327	327
query14	6108	3930	3612	3612
query14_1	3419	3431	3415	3415
query15	207	193	176	176
query16	956	476	447	447
query17	871	661	548	548
query18	2420	432	306	306
query19	212	161	137	137
query20	85	86	83	83
query21	211	127	108	108
query22	12930	13076	12781	12781
query23	13511	12931	12495	12495
query23_1	12517	12602	12456	12456
query24	7382	821	467	467
query24_1	471	471	480	471
query25	537	420	355	355
query26	1270	260	142	142
query27	2768	438	267	267
query28	4650	1955	1939	1939
query29	1595	606	447	447
query30	300	219	183	183
query31	997	925	813	813
query32	144	95	93	93
query33	537	302	246	246
query34	912	854	489	489
query35	737	747	638	638
query36	815	828	702	702
query37	130	97	82	82
query38	1761	1722	1670	1670
query39	798	801	747	747
query39_1	745	727	736	727
query40	217	131	95	95
query41	71	67	68	67
query42	86	82	85	82
query43	343	363	314	314
query44	1292	677	680	677
query45	179	184	169	169
query46	831	965	557	557
query47	2882	2785	2699	2699
query48	287	301	210	210
query49	576	390	281	281
query50	689	280	213	213
query51	10914	10582	10548	10548
query52	89	81	69	69
query53	182	200	144	144
query54	237	194	185	185
query55	71	68	62	62
query56	246	205	209	205
query57	1657	1611	1566	1566
query58	276	257	236	236
query59	2149	2150	1948	1948
query60	285	239	209	209
query61	152	149	147	147
query62	378	329	292	292
query63	186	156	162	156
query64	2798	1018	771	771
query65	3338	3274	3284	3274
query66	1784	409	306	306
query67	20184	20184	19864	19864
query68	3229	988	609	609
query69	385	298	256	256
query70	857	761	759	759
query71	296	239	199	199
query72	2846	2597	2303	2303
query73	526	519	288	288
query74	4620	4516	4333	4333
query75	2340	2271	1907	1907
query76	2394	1014	581	581
query77	355	366	277	277
query78	9736	9687	9167	9167
query79	963	842	510	510
query80	857	424	378	378
query81	582	319	276	276
query82	578	129	108	108
query83	300	191	179	179
query84	310	122	94	94
query85	873	429	372	372
query86	352	239	227	227
query87	1990	1959	1809	1809
query88	3581	2653	2614	2614
query89	321	284	243	243
query90	1875	168	163	163
query91	163	148	123	123
query92	101	89	91	89
query93	976	988	577	577
query94	517	298	264	264
query95	561	321	360	321
query96	655	477	226	226
query97	2477	2423	2317	2317
query98	161	146	147	146
query99	770	771	648	648
Total cold run time: 235210 ms
Total hot run time: 150538 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 b4bb074bfed7b9bb6337254b29a20ddde76ce683, data reload: false

query1	0.01	0.01	0.01
query2	0.09	0.05	0.05
query3	0.26	0.09	0.09
query4	1.61	0.10	0.10
query5	0.23	0.23	0.23
query6	1.18	0.71	0.68
query7	0.04	0.01	0.01
query8	0.05	0.04	0.04
query9	0.39	0.34	0.35
query10	0.62	0.61	0.62
query11	0.21	0.14	0.16
query12	0.18	0.15	0.15
query13	0.48	0.48	0.50
query14	1.01	1.02	1.00
query15	0.64	0.59	0.59
query16	0.35	0.31	0.34
query17	1.16	1.18	1.13
query18	0.20	0.17	0.18
query19	2.10	2.00	2.05
query20	0.01	0.01	0.02
query21	15.50	0.19	0.14
query22	4.89	0.06	0.05
query23	16.28	0.31	0.12
query24	2.88	0.45	0.35
query25	0.09	0.07	0.07
query26	0.98	0.21	0.16
query27	0.05	0.04	0.04
query28	3.23	0.76	0.37
query29	12.50	4.13	3.27
query30	0.28	0.13	0.10
query31	2.79	0.55	0.31
query32	3.22	0.59	0.51
query33	3.24	3.25	3.33
query34	15.35	4.00	3.39
query35	3.32	3.34	3.31
query36	0.55	0.46	0.42
query37	0.09	0.07	0.06
query38	0.05	0.04	0.04
query39	0.03	0.03	0.03
query40	0.17	0.14	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.48 s
Total hot run time: 24.36 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 0.00% (0/331) 🎉
Increment coverage report
Complete coverage report

@morningman
morningman merged commit 3d000b4 into apache:master Oct 26, 2026
45 checks passed
morningman added a commit to morningman/doris that referenced this pull request Oct 28, 2026
…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 <[email protected]>
Gabriel39 pushed a commit that referenced this pull request Oct 29, 2026
…lete and data file types (#69711)

### 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 <!-- At least one of them must be included. -->
    - [ ] Regression test
    - [x] 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`: #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.
    - [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. <!-- 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 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants