Skip to content

[fix](iceberg) Preserve nested field case in created schemas - #67166

Merged
Gabriel39 merged 9 commits into
apache:masterfrom
Gabriel39:fix/preserve-iceberg-nested-field-case
Sep 3, 2026
Merged

[fix](iceberg) Preserve nested field case in created schemas#67166
Gabriel39 merged 9 commits into
apache:masterfrom
Gabriel39:fix/preserve-iceberg-nested-field-case

Conversation

@Gabriel39

@Gabriel39 Gabriel39 commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: N/A

Problem Summary:

Doris used the lowercase runtime name of nested STRUCT fields when building connector schemas. Creating an Iceberg table through Doris therefore changed persisted mixed-case field names such as CaseSensitive to casesensitive, causing schema compatibility checks from engines such as Trino and Spark to fail.

The same identity mismatch also affected schema evolution, query execution, and pruning:

  • complex MODIFY COLUMN could interpret a case-only spelling difference as a nested rename;
  • flat Iceberg MODIFY COLUMN and its AFTER reference used caller spelling even though Iceberg update paths are case-sensitive;
  • nested MODIFY COLUMN error upgrading resolved only the leaf name and could inspect an unrelated same-named top-level column;
  • runtime struct lookup depended on the JVM default locale, so locales such as Turkish could map distinct field names to the same key;
  • a Unicode field spelling displayed by DESCRIBE, such as Σ or , could fail at execution because FE passed that external spelling to BE while the thrift struct descriptor contained the normalized runtime name (σ or ß);
  • cast-aware nested pruning used equalsIgnoreCase, which could map distinct ROOT-normalized siblings such as i and ı to the wrong source field.

This change keeps two explicit field identities: a locale-independent lowercase key for Doris runtime lookup and the original spelling for connector/external metadata. Connector create-table paths, including FILE TVF CTAS, use the original spelling. Iceberg schema evolution resolves existing fields case-insensitively and then stages type, comment, nullability, and position changes with the persisted canonical path. Only an explicit rename operation changes field spelling. Nested modify error upgrading resolves the complete ConnectorColumnPath and remains best-effort so it cannot replace the original build error for a missing target.

After Nereids successfully resolves a STRUCT selector, it replaces the external spelling with the resolved field's normalized runtime name for thrift/BE execution. This canonicalization is applied to directly analyzed ElementAt nodes, ElementAt produced while binding SQL function syntax from UnboundFunction, and ElementAt created for dotted access on a computed base such as CAST(... AS STRUCT<...>).field. The latter two paths construct and return a new node without revisiting visitElementAt. Consequently, element_at(struct, 'field') and dotted dereference both work while DESCRIBE and connector metadata continue to expose the original external spelling. Cast-aware pruning compares exact ROOT-normalized keys so that distinct siblings remain distinct.

Metadata and rolling-upgrade compatibility

StructField.name, StructType.fields, and the current fieldMap lookup index can be present in FE image metadata. Before this change, an FE running with a locale such as tr-TR could therefore persist I as the runtime key ı. A new FE using Locale.ROOT produces the lookup key i, so exact ROOT lookup alone cannot read that pre-ROOT image.

For fields replayed from metadata that predates originalName, this PR records a runtime-only legacy marker through Catalog-to-Nereids conversion. Current fields use the exact ROOT lookup key. Legacy fields first accept an exact persisted runtime spelling; broader case matching is used only when it identifies a single legacy field. If multiple legacy runtime names match, lookup rejects the ambiguous selector instead of silently returning the wrong sibling. Newly created metadata always has originalName, so valid ROOT-distinct names such as i and ı are not merged by the compatibility path.

This covers the supported rolling-upgrade direction where upgraded Followers/Observers replay metadata written by an older Master, followed by upgrading the Master. It does not make an old FE understand metadata first written by a new FE. It also cannot reconstruct original spelling that an old FE already discarded; it only preserves unambiguous lookup compatibility for the persisted runtime name. Rebuilding fieldMap during deserialization would not recover the old FE locale or discarded spelling because Doris replays these objects through Gson and that information was never persisted.

Fix boundary

The fix is limited to nested field identity preservation, Iceberg schema evolution, STRUCT selector canonicalization in FE, unambiguous legacy pre-ROOT struct lookup, and consistent cast-pruning identity. Doris runtime lookup remains case-insensitive for current metadata. BE continues to receive and compare normalized thrift names; this PR does not add Unicode case folding to BE. Current metadata continues to persist the existing fieldMap; rebuilding or removing that lookup index is a separate metadata-format change and is intentionally outside this PR. Removed code paths such as StructElement and IcebergScanNode are not reintroduced; their current replacements already route through the fixed lookup or preserve partition-column case.

Release note

Preserve mixed-case nested field names when creating and evolving Iceberg schemas, allow displayed Unicode nested field names to be queried, keep nested pruning correct for locale-sensitive Unicode names, and retain safe lookup compatibility with pre-ROOT FE metadata.

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test
    • No need to test or manual test.

    Added coverage for mixed-case nested fields in create-table and FILE TVF CTAS paths, STRUCT/ARRAY/MAP evolution, locale-independent runtime lookup and pruning, case-insensitive flat Iceberg MODIFY COLUMN/AFTER resolution, full-path nested modify error handling, pre-ROOT Turkish metadata replay across Catalog and Nereids, ambiguous legacy-field rejection, cast pruning with ROOT-distinct sibling names, and execution of exact displayed Unicode names (Σ and ). The ExpressionAnalyzer coverage constructs an UnboundFunction to exercise the same SQL function-binding path as element_at(...), and covers direct ElementAt, ordinary dotted dereference, and computed-base dotted dereference.

    Latest local validation passed 23 Iceberg connector column-evolution tests, all 8 ExpressionAnalyzer tests, and all 5 ColumnGsonSerializationTest tests. Full FE Checkstyle passed all 74 modules. The external Spark/Iceberg regression requires the CI test environment and was added for CI execution.

  • Behavior changed:

    • No.
    • Yes. External Iceberg schemas retain their original nested-field spelling, displayed Unicode nested field names remain executable, unambiguous legacy struct metadata remains queryable after upgrade, ambiguous legacy selectors are rejected instead of reading the wrong field, and pruning keeps ROOT-distinct field identities separate.
  • 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

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

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

@Gabriel39

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.

Requesting changes for two blocking issues.

  • Correctness: CREATE/readback now preserve nested external spelling while runtime lookup and Thrift remain normalized, but Iceberg complex MODIFY regresses when an existing mixed-case field is redeclared with different case. The same root cause affects type-, comment-, and nullability-only changes under STRUCT/ARRAY/MAP.
  • CI: the new public Nereids constructor fails the required CheckStyle gate because it lacks Javadoc.

Checkpoint conclusions: the dual-name design is otherwise focused; recursive Iceberg/Paimon CREATE paths, catalog display, runtime lookup, Gson backward fallback, and FE/BE serialization use the intended identity. No separate concurrency, lifecycle, configuration, transaction/partial-write, observability, storage-format, or material performance issue survived review. The added tests cover the converter boundary but not the failing real Iceberg evolution case. CheckStyle is failed; compile and FE UT are still pending, and no local build/test was run because the review bundle prohibits it. No additional user focus was supplied.

The review converged after two fresh rounds: all final full/risk reviewers returned NO_NEW_VALUABLE_FINDINGS after revalidating these two findings and their anchors.

### What problem does this PR solve?

Problem Summary: Complex Iceberg MODIFY COLUMN operations treated a case-only spelling difference as a forbidden nested-field rename. Match existing fields case-insensitively, keep the persisted Iceberg field path spelling, reject case-insensitive append collisions, and document the dual-name StructField contract.

### Release note

Fix mixed-case nested Iceberg field evolution while preserving external schema spelling.
@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

Automated review completed after two rounds; every Round 2 normal and risk-focused reviewer returned NO_NEW_VALUABLE_FINDINGS. Two distinct P1 correctness gaps remain and are described inline.

Critical checkpoint conclusions:

  • Goal and proof: the patch preserves nested spelling on direct structured CREATE/MODIFY and ordinary ASCII Iceberg evolution, but the goal is incomplete for Unicode field identity and Parquet FILE-TVF CTAS.
  • Scope and clarity: the production changes are small and focused; the dual-name split is clear once each producer/consumer is traced.
  • Concurrency, locking, and lifecycle: no Doris shared mutable state, thread, lock, static initialization, callback, or teardown path is added. Iceberg owns optimistic conflict detection for the single metadata commit.
  • Configuration and compatibility: no configuration, storage-format, function-symbol, connector-SPI, or FE-BE protocol change is introduced. Old Gson records safely fall back from missing originalName to the historical name; BE Thrift intentionally retains normalized runtime names.
  • Parallel paths and conditions: direct parsed CREATE/ADD/MODIFY, connector-imported schemas, plugin-table CTAS, ARRAY/MAP recursion, canonical existing Iceberg paths, appended-field collision checks, and dedicated rename were traced. FILE-TVF CTAS is the missing producer path, while the Unicode comparator uses a different identity from Iceberg and its sibling collision set.
  • Error handling and data correctness: unsupported category, narrowing, MAP-key, rename/reorder, nullability, and append-collision cases fail before commit(). The two findings instead allow successful operations that persist the wrong schema or mutate the wrong nested field.
  • Tests and results: the added unit/InMemoryCatalog tests correctly cover direct export plus ASCII type, comment, nullability, ARRAY/MAP, and collision behavior. They omit the Unicode comparator boundary and the Parquet FILE-TVF to external Iceberg CTAS path. No local build or test was run because the authoritative review prompt forbids it; live CheckStyle passes, while FE UT/compile/performance were still pending at submission time.
  • Observability: existing field/path exceptions are sufficient for this metadata operation; no new metric or log is required.
  • Persistence and writes: this is one atomic Iceberg metadata commit rather than a Doris transaction/EditLog change, but atomicity cannot correct the wrong field mapping identified inline.
  • Performance: recursive diffing remains linear in complex-type size with a small set per STRUCT; no material performance or memory issue was found.
  • Other review points: the Nereids toSql() spelling discrepancy was dismissed because plugin external CREATE/ALTER use structured objects and never consume or reparse that diagnostic text. No other correctness, compatibility, lifecycle, or coverage issue survived convergence.

User focus: no additional review focus was supplied.

Review status: complete for the supplied head; all candidates were accepted, deduplicated, or dismissed before submission.

Comment thread fe/fe-type/src/main/java/org/apache/doris/catalog/StructField.java
@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17581	3061	3075	3061
q2	2334	254	218	218
q3	10000	896	534	534
q4	4690	255	201	201
q5	7707	583	390	390
q6	139	120	93	93
q7	533	508	387	387
q8	9329	977	978	977
q9	3474	2391	2404	2391
q10	6532	882	705	705
q11	457	200	181	181
q12	732	266	204	204
q13	18052	1551	1178	1178
q14	166	142	135	135
q15	q16	435	399	369	369
q17	1457	875	873	873
q18	3073	2245	2253	2245
q19	1108	915	786	786
q20	367	282	210	210
q21	5257	1747	1829	1747
q22	345	269	237	237
Total cold run time: 93768 ms
Total hot run time: 17122 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3505	3361	3312	3312
q2	518	400	375	375
q3	2222	2392	2140	2140
q4	1216	1183	893	893
q5	2198	2109	2107	2107
q6	181	130	88	88
q7	1072	909	835	835
q8	1633	1427	1440	1427
q9	3153	3106	3079	3079
q10	1931	1849	1658	1658
q11	362	273	255	255
q12	471	439	344	344
q13	1504	1575	1151	1151
q14	169	161	155	155
q15	q16	395	404	360	360
q17	3601	3292	3250	3250
q18	4907	4387	4829	4387
q19	5338	875	850	850
q20	997	968	852	852
q21	3808	3104	3188	3104
q22	416	342	331	331
Total cold run time: 39597 ms
Total hot run time: 30953 ms

@hello-stephen

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

query5	4264	404	328	328
query6	442	130	121	121
query7	4930	419	229	229
query8	305	123	116	116
query9	8661	2858	2877	2858
query10	389	220	182	182
query11	5384	1038	910	910
query12	125	71	77	71
query13	1211	456	329	329
query14	6086	2197	2071	2071
query14_1	1953	1952	1918	1918
query15	181	119	110	110
query16	929	375	355	355
query17	821	448	383	383
query18	2331	314	231	231
query19	171	131	105	105
query20	70	67	69	67
query21	212	103	85	85
query22	5354	5216	5208	5208
query23	6642	6039	6039	6039
query23_1	5901	5956	6065	5956
query24	7267	1098	784	784
query24_1	754	755	786	755
query25	547	284	226	226
query26	1226	237	134	134
query27	2787	419	259	259
query28	4689	1504	1481	1481
query29	926	419	340	340
query30	252	156	123	123
query31	850	390	321	321
query32	132	72	82	72
query33	471	214	167	167
query34	1035	823	472	472
query35	401	431	336	336
query36	576	579	531	531
query37	141	76	71	71
query38	1001	840	817	817
query39	483	493	470	470
query39_1	451	434	446	434
query40	204	89	72	72
query41	52	49	49	49
query42	70	69	74	69
query43	243	237	208	208
query44	1029	541	550	541
query45	118	102	96	96
query46	782	828	506	506
query47	743	744	712	712
query48	320	315	235	235
query49	554	243	185	185
query50	731	271	186	186
query51	8298	8232	8273	8232
query52	68	77	57	57
query53	200	209	151	151
query54	232	188	170	170
query55	84	60	53	53
query56	236	196	157	157
query57	711	661	671	661
query58	209	173	184	173
query59	1204	1231	1089	1089
query60	243	179	177	177
query61	135	134	133	133
query62	359	215	187	187
query63	168	136	138	136
query64	2740	738	643	643
query65	1583	1509	1516	1509
query66	1793	269	211	211
query67	9594	9834	9628	9628
query68	2788	1168	760	760
query69	372	217	203	203
query70	665	632	619	619
query71	263	186	175	175
query72	2397	1704	1553	1553
query73	680	529	336	336
query74	1565	1222	1120	1120
query75	1173	1088	957	957
query76	2292	748	553	553
query77	252	258	205	205
query78	3784	3574	3182	3182
query79	2808	846	592	592
query80	1555	316	279	279
query81	489	159	132	132
query82	699	137	94	94
query83	279	208	188	188
query84	293	113	92	92
query85	813	344	287	287
query86	407	198	171	171
query87	1003	971	881	881
query88	2777	2092	2090	2090
query89	291	196	178	178
query90	1940	120	128	120
query91	128	120	102	102
query92	81	81	69	69
query93	1461	1089	703	703
query94	641	256	225	225
query95	534	325	224	224
query96	803	609	258	258
query97	1087	1031	981	981
query98	167	137	137	137
query99	423	344	320	320
Total cold run time: 177420 ms
Total hot run time: 81641 ms

@hello-stephen

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

query1	0.01	0.00	0.00
query2	0.07	0.04	0.03
query3	0.24	0.11	0.12
query4	1.61	0.10	0.10
query5	0.18	0.16	0.17
query6	1.23	0.67	0.69
query7	0.04	0.01	0.00
query8	0.04	0.03	0.03
query9	0.29	0.22	0.21
query10	0.35	0.34	0.35
query11	0.16	0.12	0.11
query12	0.15	0.12	0.12
query13	0.30	0.31	0.31
query14	0.44	0.45	0.44
query15	0.36	0.34	0.34
query16	0.24	0.23	0.24
query17	0.68	0.69	0.65
query18	0.17	0.17	0.18
query19	1.23	1.14	1.13
query20	0.02	0.02	0.01
query21	15.43	0.18	0.11
query22	5.06	0.04	0.05
query23	16.18	0.26	0.10
query24	3.03	0.30	0.27
query25	0.11	0.03	0.03
query26	0.81	0.17	0.12
query27	0.04	0.03	0.03
query28	3.62	0.52	0.29
query29	12.56	3.08	2.55
query30	0.25	0.12	0.12
query31	2.75	0.38	0.17
query32	3.53	0.32	0.22
query33	1.43	1.52	1.44
query34	15.30	2.28	1.75
query35	1.72	1.72	1.68
query36	0.46	0.30	0.29
query37	0.06	0.04	0.04
query38	0.04	0.03	0.03
query39	0.04	0.03	0.02
query40	0.11	0.07	0.07
query41	0.08	0.03	0.02
query42	0.04	0.02	0.02
query43	0.03	0.02	0.02
Total cold run time: 90.49 s
Total hot run time: 14.59 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 90.00% (9/10) 🎉
Increment coverage report
Complete coverage report

### What problem does this PR solve?

Issue Number: None

Related PR: apache#67166

Problem Summary: The mixed-case Iceberg schema evolution fixture drops a lowercase nested field and re-adds it as uppercase DROP_AND_ADD. After preserving external nested field spelling, DESC correctly reports the uppercase name, but the regression baseline still expected the old normalized spelling. Update the Parquet and ORC schema expectations and document that runtime lookup continues to use the normalized field name.

### Release note

None

### Check List (For Author)

- Test: Regression test
    - Built the regression framework and ran its four unit tests with ./run-regression-test.sh --compile
    - Compiled test_iceberg_struct_schema_evolution.groovy with the Groovy compiler
    - Verified both mixed-case DESC baselines retain six columns and preserve DROP_AND_ADD
- Behavior changed: No
- Does this need documentation: No
@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

Automated review result: changes requested.

Two blocking correctness issues remain on the current head; both already have complete live inline threads, so this review deliberately adds no duplicate inline comments:

  1. Use Iceberg's ROOT-lowercase name identity. equalsIgnoreCase can accept a positional match whose lowercase keys differ, then stage type/comment/nullability updates on the wrong canonical old field path.
  2. Preserve FILE-TVF spelling before constructing the field. FILE-TVF decoding lowercases PStructField names before the new dual-name carrier sees them, so S3/FILE CTAS to Iceberg still persists lowercase nested names.

Critical checkpoint conclusions:

  • Goal and proof: the change correctly separates original external spelling from normalized runtime lookup for direct DDL, ordinary catalog/Nereids/connector round trips, display, and normalized BE thrift. The unit tests, real Iceberg InMemoryCatalog commits, and Parquet/ORC regression expectations prove the covered ASCII paths, but the two blockers above mean the full stated goal is not yet achieved.
  • Scope and parallel paths: the implementation is focused and reuses the existing conversion flow. Direct CREATE, ordinary connector CTAS, nested ARRAY/MAP recursion, complex MODIFY, constant-folding constraints, and nested-pruning reachability were traced. FILE-TVF is the missed functional parallel path.
  • Concurrency and lifecycle: no new threads, locks, mutable shared state, static initialization, configuration, or dynamic-config behavior are introduced. Complex Iceberg changes remain staged in one UpdateSchema and committed atomically.
  • Compatibility and persistence: the catalog field is an additive Gson member with an old-image fallback to normalized name; equality/runtime maps and FE-to-BE thrift remain normalized, so no new BE protocol or rolling-BE payload is introduced. No new FE/BE variable requires propagation.
  • Data correctness and errors: ordinary validation fails loud and canonical old paths prevent case-only renames, but the Unicode identity mismatch can commit changes to the wrong field, which is blocking.
  • Tests and results: the changed expected results are consistent with preserving external spelling while retaining lowercase runtime access. Please add the Unicode identity InMemoryCatalog regression and an end-to-end FILE-TVF-to-Iceberg CTAS regression requested in the existing threads. No builds or tests were run by this automated review because the review environment explicitly prohibits them.
  • Performance and observability: the extra immutable spelling field and lowercase collision keys have negligible cost; no new logging, metrics, or observability surface is required.
  • User focus: no additional user-provided focus was supplied.

Review completion: the authoritative 11-file diff was fully covered. One normal FE/lifecycle review, one normal Iceberg/test review, and a separate risk-focused ingress/identity review all converged with NO_NEW_VALUABLE_FINDINGS; every candidate was independently validated and deduplicated. No additional inline finding is warranted beyond the two live blockers above.

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

Review result: request changes

Two P1 correctness gaps remain in the new case-preservation behavior.

Checkpoint conclusions

  • Goal and proof: The patch otherwise preserves nested spelling through direct CREATE, source CTAS, FILE-TVF CTAS, and complex MODIFY, including STRUCT under ARRAY/MAP. The two inline issues show that runtime identity is still JVM-locale-dependent and flat Iceberg MODIFY still fails before the corrected child matcher when the top-level stored spelling differs.
  • Scope and parallel paths: All 14 changed paths were reviewed across parser/type conversion, catalog and Nereids representations, connector export, FILE protobuf decoding, flat/nested MODIFY, rendering, runtime lookup/pruning, and the changed tests/results. No additional user focus was supplied.
  • Concurrency, lifecycle, and configuration: No new threads, locks, background/static lifecycle, configuration, or FE/BE variable behavior is introduced. Existing connector session/authentication scope and table cleanup remain unchanged.
  • Compatibility and persistence: originalName is additive Gson metadata with an old-image fallback to name; thrift/protobuf execution shape is unchanged. Iceberg schema changes remain staged in one UpdateSchema and committed atomically. Locale-dependent runtime keys still make metadata and lookup behavior inconsistent across FE locales, as noted inline.
  • Special conditions and errors: ROOT-lowercase child identity, Unicode collision handling, canonical nested paths, and fail-before-commit validation otherwise hold. The flat top-level root and AFTER reference still need canonical stored spellings.
  • Tests and result files: The changed unit/regression coverage exercises real Iceberg commits and real Parquet FILE-TVF-to-Iceberg CTAS, and the modified result rows match the new rendering. Missing boundaries are a Turkish-default-locale uppercase I/dotless ı case and a case-mismatched mixed-case top-level MODIFY root. Per the review runner instructions, no local build or test command was run.
  • Observability and performance: These are synchronous DDL/type-conversion paths with existing error context; no new observability requirement was found. The added per-field string state and DDL-time normalization cost are negligible.

All three final convergence reviewers returned no new valuable findings after second rechecks. Live head/base still match the authoritative bundle. CheckStyle is passing; FE UT, COMPILE, and performance are currently pending.

Comment thread fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructField.java Outdated
@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17592	3055	3075	3055
q2	2168	270	223	223
q3	10184	874	500	500
q4	4669	242	196	196
q5	7687	565	382	382
q6	139	119	94	94
q7	530	498	384	384
q8	9230	864	890	864
q9	3450	2386	2386	2386
q10	6517	852	704	704
q11	390	205	179	179
q12	614	260	197	197
q13	18146	1552	1194	1194
q14	163	156	141	141
q15	q16	441	400	366	366
q17	1347	893	813	813
q18	3011	2240	2208	2208
q19	1118	890	774	774
q20	355	282	202	202
q21	5220	1805	1887	1805
q22	330	271	228	228
Total cold run time: 93301 ms
Total hot run time: 16895 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3410	3331	3365	3331
q2	510	416	371	371
q3	2224	2310	2188	2188
q4	1188	1171	883	883
q5	2163	2083	2114	2083
q6	168	121	85	85
q7	1032	937	848	848
q8	1605	1391	1421	1391
q9	3105	3097	3095	3095
q10	1851	1820	1677	1677
q11	357	269	250	250
q12	451	435	345	345
q13	1493	1560	1152	1152
q14	177	168	162	162
q15	q16	390	400	350	350
q17	3577	3204	3137	3137
q18	4821	4384	4753	4384
q19	2864	889	873	873
q20	964	941	811	811
q21	3729	3039	3246	3039
q22	389	325	307	307
Total cold run time: 36468 ms
Total hot run time: 30762 ms

@hello-stephen

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

query5	4300	403	320	320
query6	383	139	129	129
query7	4980	437	238	238
query8	303	121	132	121
query9	8684	2873	2865	2865
query10	364	222	198	198
query11	5399	1028	907	907
query12	130	75	69	69
query13	1187	455	324	324
query14	6052	2173	2094	2094
query14_1	2000	2029	1968	1968
query15	181	119	108	108
query16	946	380	360	360
query17	932	458	379	379
query18	2328	336	241	241
query19	170	142	109	109
query20	70	69	71	69
query21	211	99	86	86
query22	5472	5449	5337	5337
query23	6609	6101	5971	5971
query23_1	6144	6212	6005	6005
query24	7311	1109	783	783
query24_1	785	781	787	781
query25	441	311	264	264
query26	1243	231	131	131
query27	2766	421	253	253
query28	4703	1496	1482	1482
query29	975	447	361	361
query30	255	157	133	133
query31	833	403	329	329
query32	131	76	75	75
query33	468	234	180	180
query34	997	801	479	479
query35	408	404	337	337
query36	583	575	515	515
query37	132	80	70	70
query38	1008	850	826	826
query39	508	510	471	471
query39_1	466	454	466	454
query40	208	94	79	79
query41	59	58	55	55
query42	74	72	73	72
query43	240	242	210	210
query44	1018	547	569	547
query45	109	104	101	101
query46	806	858	554	554
query47	765	778	714	714
query48	323	308	267	267
query49	526	246	181	181
query50	772	264	187	187
query51	8165	8130	8121	8121
query52	69	66	63	63
query53	185	214	142	142
query54	234	181	251	181
query55	71	59	54	54
query56	221	170	163	163
query57	691	653	655	653
query58	202	161	156	156
query59	1222	1251	1080	1080
query60	236	177	168	168
query61	115	118	117	117
query62	361	207	180	180
query63	173	143	139	139
query64	2638	707	583	583
query65	1599	1597	1602	1597
query66	1874	285	199	199
query67	10188	9686	9432	9432
query68	2785	1142	753	753
query69	346	228	195	195
query70	674	624	643	624
query71	249	169	163	163
query72	2342	1723	1598	1598
query73	667	595	345	345
query74	1575	1226	1125	1125
query75	1173	1091	958	958
query76	2290	701	551	551
query77	257	256	200	200
query78	3863	3736	3288	3288
query79	2500	822	573	573
query80	1594	338	280	280
query81	498	153	130	130
query82	658	126	103	103
query83	295	216	189	189
query84	268	108	91	91
query85	800	357	296	296
query86	399	175	165	165
query87	1017	964	881	881
query88	2771	2137	2096	2096
query89	298	195	177	177
query90	1968	129	130	129
query91	133	120	100	100
query92	74	72	71	71
query93	1513	1109	646	646
query94	683	250	201	201
query95	527	336	259	259
query96	776	571	274	274
query97	1065	1065	1038	1038
query98	166	140	129	129
query99	429	345	304	304
Total cold run time: 178057 ms
Total hot run time: 81927 ms

@hello-stephen

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

query1	0.01	0.00	0.00
query2	0.08	0.04	0.04
query3	0.26	0.11	0.11
query4	1.62	0.10	0.11
query5	0.18	0.16	0.16
query6	1.19	0.66	0.70
query7	0.03	0.00	0.00
query8	0.05	0.04	0.03
query9	0.29	0.23	0.22
query10	0.34	0.34	0.35
query11	0.17	0.12	0.11
query12	0.15	0.12	0.12
query13	0.29	0.30	0.29
query14	0.44	0.45	0.44
query15	0.37	0.34	0.34
query16	0.21	0.24	0.22
query17	0.64	0.66	0.66
query18	0.18	0.17	0.16
query19	1.13	1.09	1.20
query20	0.02	0.01	0.01
query21	15.45	0.15	0.13
query22	5.08	0.04	0.05
query23	16.13	0.25	0.11
query24	3.00	0.32	0.27
query25	0.10	0.05	0.03
query26	0.73	0.17	0.13
query27	0.03	0.04	0.02
query28	3.63	0.57	0.28
query29	12.46	3.19	2.55
query30	0.27	0.12	0.12
query31	2.78	0.39	0.18
query32	3.50	0.32	0.23
query33	1.46	1.70	1.59
query34	15.34	2.17	1.77
query35	1.76	1.75	1.71
query36	0.47	0.29	0.28
query37	0.06	0.04	0.03
query38	0.04	0.03	0.03
query39	0.03	0.03	0.02
query40	0.12	0.08	0.08
query41	0.08	0.02	0.02
query42	0.04	0.02	0.02
query43	0.03	0.02	0.02
Total cold run time: 90.24 s
Total hot run time: 14.76 s

### What problem does this PR solve?

Issue Number: None

Related PR: apache#67166

Problem Summary: Struct runtime lookup keys still depended on the JVM default locale, which could collapse distinct field names under locales such as Turkish. Flat Iceberg MODIFY COLUMN also resolved and staged updates with the caller spelling even though Iceberg update paths are case-sensitive. Use Locale.ROOT throughout the struct identity pipeline, resolve top-level Iceberg fields case-insensitively, and stage all updates and positions with persisted names.

### Release note

Preserve mixed-case Iceberg field names reliably across locales and allow case-insensitive Doris MODIFY COLUMN references without losing the stored spelling.

### Check List (For Author)

- Test: Unit Test
    - 38 targeted FE core tests
    - 44 Iceberg catalog-backed column evolution tests
    - Full FE Checkstyle
- Behavior changed: Yes. Iceberg schema changes keep persisted field spelling while Doris lookup remains case-insensitive and locale-independent.
- Does this need documentation: No.
@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

Review summary

Requesting changes for one P1 correctness gap: Unicode runtime-name canonicalization still misses dotted dereferences whose base is a computed expression. The patch otherwise consistently preserves external struct spelling through Catalog/Nereids conversions, connector requests, FILE TVF ingestion, and Iceberg schema evolution while keeping normalized names on thrift/BE runtime paths.

Key checkpoints

  • Goal and parallel paths: explicit element_at and bare dotted UnboundSlot access are canonicalized, but a non-slot base is parsed and analyzed through the separate DereferenceExpression path; this prevents the runtime-name contract from being complete.
  • Scope/minimality: the production changes are focused on the dual-name type contract, conversion boundaries, Iceberg mutation paths, analyzer/pruning alignment, and their tests.
  • Compatibility/persistence: originalName is additive persisted metadata and the thrift shape remains unchanged. I did not repeat the already-threaded pre-ROOT legacy lookup concern.
  • Iceberg writes/errors: flat and nested mutations resolve persisted Iceberg identities and remain staged on one UpdateSchema with one final commit; no separate wrong-target, partial-commit, or swallowed-error defect survived review.
  • Concurrency/lifecycle/configuration: no new production thread, lock, resource lifetime, static-initialization, or configuration behavior is introduced.
  • FE-to-BE/performance/observability: normalized names continue over the existing wire representation; the inline finding is a selector that fails to adopt that identity before BE lookup. Added schema-width work is confined to analysis/DDL paths, and no new observability surface is needed.
  • Tests: the added tests cover conversion, replay, explicit and bare selectors, pruning, Iceberg evolution, and the FILE-TVF-to-Iceberg boundary, but not the computed-base dotted case. Per the review instructions I did not run builds or tests locally; live CheckStyle is green, while skipped or absent heavier jobs are not treated as proof.

No additional user review focus was provided, so I reviewed the full authoritative diff. Existing inline discussions were treated as duplicate fences.

lastFieldName = fieldName;
expression = new ElementAt(expression, new StringLiteral(fieldName));
// Dereference-created selectors also cross the thrift boundary and must use runtime identity.
expression = new ElementAt(expression, new StringLiteral(field.getName()));

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] Canonicalize computed dotted dereferences too

This line fixes only the UnboundSlot path handled by bindNestedFields. A computed base such as (CAST(payload AS STRUCT<`Σ`:BIGINT>)).`Σ` is parsed as DereferenceExpression; visitDereferenceExpression resolves the field but returns new ElementAt(expression, dereferenceExpression.child(1)), and that newly created node is not revisited by visitElementAt. The selector therefore remains Σ while the cast/thrift struct field is σ, so BE's bytewise iequal lookup still reports the field missing. This is a separate analyzer branch from the existing direct-selector thread. Please canonicalize the resolved field in visitDereferenceExpression too (or share one helper) and add a computed-base dotted-access test.

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.

Fixed in 458e465. The struct branch in visitDereferenceExpression now canonicalizes the newly constructed ElementAt before returning, because that node is not revisited by visitElementAt. A regression test covers dotted access on a computed CAST base with a Unicode selector.

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

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.

Review result: request changes

Two P1 correctness gaps remain on exact head 6f1193d7f63f92948be69f0c62d6a643ad8be848. Both already have live inline anchors, so this review deliberately adds no duplicate root comments.

  1. Canonicalize computed dotted dereferences. The parser-shaped path below resolves the struct field but returns a fresh ElementAt with selector Σ; it is not revisited by the branch that canonicalizes to runtime key σ, so FE analysis and the normalized thrift/BE lookup disagree.

    Project[(CAST(payload AS STRUCT<Σ:BIGINT>)).Σ]
    └─ child producing payload
    
  2. Preserve lookup for pre-ROOT serialized structs. I continued that thread with a two-field pre-change tr-TR counterexample: old keys ı and i can coexist, and selector I now exact-hits the wrong i sibling before the compatibility fallback runs.

Key checkpoint conclusions

  • Goal and proof: External spelling and ROOT-normalized runtime identity are preserved across the covered Catalog/Nereids/connector/FILE-TVF/Iceberg paths, but the two blockers mean selector correctness and old-image compatibility are incomplete.
  • Scope and parallel paths: The implementation is focused. Direct and function element_at, simple dotted slots, computed-base dereference, CTAS, thrift translation, pruning, flat/nested Iceberg evolution, STRUCT under ARRAY/MAP, and external spelling round trips were traced. The missed computed-base branch is the first blocker.
  • Concurrency, lifecycle, and configuration: No new shared mutable state, lock, callback/resource lifetime, static initialization, configuration, or dynamic-reload behavior is introduced.
  • Compatibility and persistence: originalName is additive metadata and current runtime/thrift naming is consistent. The exact-first legacy lookup can nevertheless rebind a persisted pre-ROOT two-field schema, which is a blocking compatibility/data-correctness issue. No new FE/BE variable or wire-shape change was found.
  • Conditions, errors, transactions, and writes: Iceberg validation and canonical target paths stage mutations in one UpdateSchema and one atomic commit; no separate partial-write or wrong-Iceberg-target issue survived review. The remaining failures occur in analyzer/runtime identity and legacy binding.
  • Tests and results: Added tests cover current-schema conversion, direct/function/simple-slot selectors, single-field legacy replay, pruning, real Iceberg evolution, and FILE-TVF CTAS. They omit a parser-shaped computed-base dotted expression and the two-field legacy collision. No local build or test was run because the review runner prohibits it. Live CheckStyle and lightweight checks pass; FE UT, COMPILE, and performance are pending at submission time.
  • Observability and performance: Existing analysis/DDL errors are sufficient once identity is corrected. Added normalization and schema scans are outside scan hot paths; no distinct logging, metrics, memory, or performance issue was found.
  • Other review points: Existing review threads were hard duplicate fences. No additional correctness, security, documentation, or user-focus concern survived the full changed-file sweep; no additional user focus was supplied.

Review status: complete for the exact supplied head. One normal FE review, one normal Iceberg/integration review, and a separate risk-focused review all returned NO_NEW_VALUABLE_FINDINGS after applying the existing-thread fences; every candidate was verified, deduplicated, or dismissed before submission.

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17607	3072	3058	3058
q2	2080	270	234	234
q3	10230	852	506	506
q4	4666	247	201	201
q5	7682	548	389	389
q6	136	112	92	92
q7	521	489	379	379
q8	9227	840	940	840
q9	3469	2399	2377	2377
q10	6526	840	687	687
q11	407	196	176	176
q12	633	253	201	201
q13	18128	1500	1151	1151
q14	155	149	137	137
q15	q16	445	399	369	369
q17	1410	948	845	845
q18	3088	2247	2238	2238
q19	1275	858	781	781
q20	388	287	192	192
q21	5610	1726	1869	1726
q22	326	268	225	225
Total cold run time: 94009 ms
Total hot run time: 16804 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3446	3360	3347	3347
q2	520	396	370	370
q3	2199	2395	2274	2274
q4	1167	1161	886	886
q5	2135	2133	2083	2083
q6	164	124	88	88
q7	1032	955	874	874
q8	1589	1404	1424	1404
q9	3110	3121	3046	3046
q10	1846	1776	1614	1614
q11	359	269	250	250
q12	461	426	349	349
q13	1482	1533	1157	1157
q14	183	171	161	161
q15	q16	393	387	364	364
q17	3570	3304	3214	3214
q18	4800	4420	4723	4420
q19	868	861	859	859
q20	982	960	831	831
q21	3779	3041	3226	3041
q22	409	347	332	332
Total cold run time: 34494 ms
Total hot run time: 30964 ms

@hello-stephen

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

query5	4281	421	321	321
query6	411	129	123	123
query7	4941	427	235	235
query8	309	123	114	114
query9	8695	2908	2887	2887
query10	407	224	173	173
query11	5360	1027	912	912
query12	134	70	72	70
query13	1204	436	339	339
query14	6138	2181	2085	2085
query14_1	1968	1953	1943	1943
query15	179	113	109	109
query16	943	373	360	360
query17	833	450	369	369
query18	2336	332	236	236
query19	169	139	110	110
query20	69	71	68	68
query21	203	99	91	91
query22	5383	5377	5244	5244
query23	6741	6248	5812	5812
query23_1	6043	5829	6136	5829
query24	7280	1107	765	765
query24_1	777	810	776	776
query25	424	302	269	269
query26	1221	231	131	131
query27	2801	413	245	245
query28	4707	1508	1487	1487
query29	968	446	354	354
query30	267	159	130	130
query31	840	398	334	334
query32	133	76	72	72
query33	475	227	194	194
query34	993	819	508	508
query35	412	402	336	336
query36	590	578	522	522
query37	115	84	71	71
query38	997	884	806	806
query39	494	461	475	461
query39_1	437	466	485	466
query40	196	87	78	78
query41	52	50	50	50
query42	71	69	72	69
query43	242	239	206	206
query44	1019	544	546	544
query45	107	102	97	97
query46	801	869	557	557
query47	762	757	729	729
query48	308	316	231	231
query49	530	232	176	176
query50	722	265	195	195
query51	7919	8028	7882	7882
query52	69	68	58	58
query53	196	191	150	150
query54	206	162	147	147
query55	81	64	55	55
query56	193	175	184	175
query57	686	740	663	663
query58	199	162	151	151
query59	1216	1225	1076	1076
query60	244	168	169	168
query61	123	136	113	113
query62	361	206	177	177
query63	170	138	139	138
query64	2875	674	598	598
query65	1606	1536	1621	1536
query66	1839	267	213	213
query67	9826	9640	9407	9407
query68	2762	1218	768	768
query69	327	233	201	201
query70	679	596	636	596
query71	246	180	165	165
query72	2328	1708	1560	1560
query73	645	623	344	344
query74	1557	1209	1128	1128
query75	1180	1089	932	932
query76	2286	735	532	532
query77	250	260	219	219
query78	3889	3820	3197	3197
query79	2288	803	572	572
query80	1577	324	266	266
query81	489	156	130	130
query82	622	124	93	93
query83	274	204	188	188
query84	296	107	89	89
query85	792	353	289	289
query86	393	175	165	165
query87	1000	954	878	878
query88	2754	2109	2095	2095
query89	272	191	172	172
query90	1979	128	129	128
query91	130	119	100	100
query92	79	67	62	62
query93	1423	1080	731	731
query94	630	273	205	205
query95	523	324	223	223
query96	802	582	294	294
query97	1038	1036	1005	1005
query98	144	138	128	128
query99	446	340	307	307
Total cold run time: 176852 ms
Total hot run time: 80910 ms

@hello-stephen

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

query1	0.01	0.00	0.01
query2	0.07	0.03	0.03
query3	0.25	0.11	0.11
query4	1.61	0.10	0.10
query5	0.18	0.16	0.15
query6	1.25	0.68	0.68
query7	0.03	0.00	0.01
query8	0.04	0.03	0.04
query9	0.29	0.22	0.21
query10	0.34	0.34	0.35
query11	0.17	0.12	0.12
query12	0.15	0.12	0.11
query13	0.30	0.31	0.30
query14	0.47	0.44	0.45
query15	0.36	0.33	0.33
query16	0.21	0.23	0.21
query17	0.67	0.72	0.73
query18	0.19	0.17	0.18
query19	1.13	1.20	1.18
query20	0.01	0.01	0.01
query21	15.44	0.16	0.12
query22	5.11	0.04	0.04
query23	16.19	0.24	0.10
query24	3.00	0.31	0.24
query25	0.11	0.03	0.04
query26	0.75	0.16	0.12
query27	0.04	0.04	0.04
query28	3.62	0.54	0.28
query29	12.42	3.16	2.54
query30	0.27	0.12	0.13
query31	2.75	0.36	0.17
query32	3.57	0.33	0.24
query33	1.42	1.50	1.42
query34	15.33	2.21	1.77
query35	1.73	1.75	1.71
query36	0.45	0.28	0.27
query37	0.06	0.04	0.04
query38	0.04	0.03	0.03
query39	0.03	0.02	0.02
query40	0.12	0.08	0.07
query41	0.08	0.02	0.02
query42	0.03	0.02	0.02
query43	0.03	0.03	0.03
Total cold run time: 90.32 s
Total hot run time: 14.68 s

@924060929

Copy link
Copy Markdown
Contributor

Compatibility Review: Struct Field Name Lookup Risks

Thanks for the detailed work on preserving nested field case. I ran targeted compatibility tests simulating locale-dependent metadata persistence and replay. Below are findings from code tracing and Java-level verification.

Issue 1 — catalog.StructType.getField() has no legacy fallback (P1)

The PR adds a legacyLocaleDependentName fallback in nereids.types.StructType.getField(), but the catalog StructType.getField() (which is the persistent type, deserialized via Gson with @SerializedName on fieldMap) does not get a similar fallback.

After Gson deserialization, fieldMap keys are whatever the old FE persisted — they are not rebuilt by the constructor. On a non-ROOT locale (e.g. Turkish tr-TR):

Old FE (Turkish): "I".toLowerCase() = "ı" (U+0131)  →  fieldMap key = "ı"
New FE (ROOT):    getField("I") → fieldMap.get("I".toLowerCase(ROOT)) = fieldMap.get("i")  →  MISS

I verified this with a standalone Java test — the catalog getField() returns null for fields persisted under a different locale. Any code path that goes through the catalog StructType.getField() (e.g. FunctionCallExpr in the old analyzer, connector metadata paths) will fail to find the field.

Suggestion: Either add the same legacy fallback to catalog.StructType.getField(), or add a readObject() that rebuilds fieldMap with Locale.ROOT keys after Gson deserialization:

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    in.defaultReadObject();
    fieldMap.clear();
    for (StructField field : fields) {
        fieldMap.put(field.getName().toLowerCase(Locale.ROOT), field);
    }
}

Issue 2 — StructElement.getSignatures() uses raw name for map lookup (P2)

StructElement.java:88 looks up the field name directly in nameToFields without any toLowerCase():

String name = ((StringLikeLiteral) child(1)).getStringValue();
if (!structArgType.getNameToFields().containsKey(name)) {  // raw name vs lowered keys

The map keys are lowered (via StructField constructor), so struct_element(s, 'FieldA') looks up "FieldA" against key "fielda"always misses for mixed-case names.

This is a pre-existing bug, but the PR's canonicalizeStructSelector() in ExpressionAnalyzer only handles ElementAt, not StructElement. I confirmed via grep that there is no visitStructElement in ExpressionAnalyzer and no rewrite rule that converts StructElement to ElementAt — so StructElement goes through its own binding path and never gets canonicalized.

Suggestion: Add StructElement canonicalization in ExpressionAnalyzer.visitUnboundFunction() alongside the existing ElementAt handling, or fix getSignatures() to lower the lookup key.

Issue 3 — IcebergScanNode.getPathPartitionKeys() uses String::toLowerCase without Locale.ROOT (P2)

// IcebergScanNode.java:385
return icebergTable.spec().fields().stream()
    .map(PartitionField::name).map(String::toLowerCase)  // ← no Locale.ROOT
    .collect(Collectors.toList());

This file is not in the PR's changed files. After the PR, struct field names in the Nereids layer use toLowerCase(Locale.ROOT), but partition key names from this path still use locale-dependent toLowerCase(). On a Turkish JVM, partition key "I" becomes "ı" while the corresponding struct field name becomes "i" — they won't match.

Issue 4 — catalog.StructType.addField() key generation may be inconsistent (P2)

public void addField(StructField field) {
    fieldMap.put(field.getName().toLowerCase(), field);  // ← no Locale.ROOT
}

If the PR changes getField() to use toLowerCase(Locale.ROOT) but addField() still uses toLowerCase() (default locale), then after an ALTER TABLE ADD COLUMN on a struct, the new field's key and the lookup key could diverge on non-ROOT locales.

What works correctly

  • ✅ Nereids StructType.getField() legacy fallback — verified it correctly resolves Turkish-persisted fields via equalsIgnoreCase
  • ✅ Pure ASCII field names — toLowerCase() and toLowerCase(Locale.ROOT) produce identical results for ASCII, so the vast majority of users are unaffected
  • ElementAt canonicalization in ExpressionAnalyzer — covers element_at() and dotted dereference paths
  • NestedColumnPruning / AccessPathExpressionCollector Locale.ROOT changes — consistent with Nereids struct keys

Test methodology

I wrote a standalone Java test that simulates:

  1. StructElement raw name lookup against lowered map keys → confirmed miss
  2. Turkish locale field persistence + ROOT locale lookup → confirmed catalog getField() miss
  3. Nereids legacy fallback → confirmed it resolves the miss
  4. addField() mixed-key scenario
  5. Gson deserialization with stale fieldMap keys → confirmed miss
  6. toLowerCase() vs toLowerCase(Locale.ROOT) divergence for "I", "Info" under Turkish locale

All 4 failure scenarios are specific to non-ASCII field names + non-ROOT JVM locale. For the common case (ASCII + English locale), there are no compatibility issues.

@Gabriel39

Copy link
Copy Markdown
Contributor Author

run buildall

@Gabriel39

Copy link
Copy Markdown
Contributor Author

/review

@Gabriel39

Copy link
Copy Markdown
Contributor Author

@924060929 Thanks for the compatibility review. I checked all four points against the current PR head:

  1. The Catalog StructType.getField compatibility fallback was already present. However, the review exposed a real edge case: a pre-ROOT image can contain two legacy runtime names that both match one selector under broad Unicode case folding. Commit 458e465 fixes this by accepting a legacy fallback only when it is unique; exact persisted runtime spellings still work, and ambiguous selectors are rejected instead of returning the wrong sibling. I did not add readObject: these types are replayed through Gson, and rebuilding fieldMap cannot recover the original locale or external spelling that old images never persisted.
  2. StructElement no longer exists on current master; it was merged into ElementAt. Both element_at and the struct_element alias therefore use the same StructType.getField path, so no separate fix is needed.
  3. IcebergScanNode no longer exists on current master. Identity partition columns are obtained through IcebergPartitionUtils.getIdentityPartitionColumns, which intentionally preserves source-column case and already has focused coverage, so changing partition-key case handling is outside this PR.
  4. StructType.addField already uses toLowerCase(Locale.ROOT) on the current PR head, so no additional change is needed there.

I also fixed the separate computed-base dereference gap in 458e465 and added regression coverage for both valid issues.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 87.27% (48/55) 🎉
Increment coverage report
Complete coverage report

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

Review result: request changes

Two new P1 correctness gaps remain on exact head 458e4651d95aad2ae1da42b665082ce4791e0462 and are described inline:

  1. Flat Iceberg ADD ... AFTER and full reorder still pass caller spellings to case-sensitive Iceberg move APIs, even though the new MODIFY path canonicalizes them.
  2. Bracket syntax retains a Variable wrapper through STRUCT-selector canonicalization, so a displayed Unicode selector can still reach BE with the external rather than runtime spelling.

The already-threaded pre-ROOT replay concern also remains incomplete for locale mappings that change code-point count: under lt-LT, capital I plus combining grave lowercases to i plus combining dot plus combining grave, which the new equalsIgnoreCase fallback cannot match to the ROOT-lowercased selector. I did not add a duplicate inline comment.

Reduced plan for the new analyzer gap:

Project[payload[@field]]
└─ Scan[payload STRUCT<σ:INT>]  (@field = "Σ")

Key checkpoint conclusions

  • Goal and proof: The patch preserves external STRUCT spelling while retaining stable runtime identity on the covered Catalog, Nereids, FILE-TVF, connector, pruning, and Iceberg MODIFY paths. The goal is incomplete at the two new inline paths and the existing legacy-replay thread. Current tests do not prove those boundaries.
  • Scope and clarity: The dual-name and canonical-path changes are focused. All 28 changed paths and the relevant parser, analyzer, conversion, replay, pruning, translation, BE lookup, connector, Iceberg mutation, and regression consumers were traced.
  • Concurrency: No new threads, locks, or shared mutable state are introduced. Iceberg schema mutation retains its existing optimistic commit-conflict boundary.
  • Lifecycle: No new callback, cache-handle, static-initialization, teardown, or non-intuitive ownership lifecycle is added.
  • Configuration: No configuration item or dynamic-reload behavior is added.
  • Compatibility: originalName is additive Gson metadata and the FE-to-BE thrift shape remains unchanged. Current-generation ROOT identities and supported conversion paths are consistent, but the existing pre-ROOT replay fallback still misses the length-expanding locale case described above.
  • Parallel paths: Direct/function literals, integer selectors, prepared placeholders, ordinary/computed dereferences, flat/nested MODIFY, STRUCT under ARRAY/MAP, and canonical error-upgrade paths are covered. The missed parallels are bracket-variable selectors and the flat ADD/reorder positioning paths reported inline.
  • Conditions and errors: Invalid Iceberg mutations fail before the single commit, and no swallowed status or misleading new error path survived review. The accepted DDL paths deterministically reject valid case-insensitive references; the accepted selector path passes FE analysis and fails at BE lookup.
  • Test coverage: Unit, InMemoryCatalog, protobuf, and external regression tests provide real conversion, metadata, pruning, and execution oracles. Missing regressions are mixed-case ADD ... AFTER/reorder, variable-backed bracket execution plus SQL-cache accounting, and the duplicate-fenced Lithuanian replay case.
  • Test results: The changed expected results are consistent with the intended spelling/runtime split. No local build or test was run because the authoritative review bundle prohibits it. At submission time CheckStyle and the applicable lightweight checks pass; COMPILE, FE UT, performance, and code-review are pending.
  • Observability: Existing analyzer and Iceberg exceptions provide adequate context once identity is corrected; no new log, metric, or tracing surface is needed.
  • Persistence and transactions: Gson replay and Iceberg schema metadata are the applicable persistence paths; no Doris EditLog/transaction path is changed. Each Iceberg evolution entry point stages one UpdateSchema and commits once.
  • Data writes: Only Iceberg metadata is mutated, atomically at commit. No partial schema publication, FE/BE crash leak, or wrong-field write survived beyond the rejected-name paths described above.
  • FE/BE variables and protocol: No new cross-process variable or wire field is added. The bracket issue concerns analyzer phase ordering for an existing SQL variable and the unchanged runtime name sent to BE; a repair must preserve SQL-cache variable accounting.
  • Memory and BE nullable handling: These Java metadata/analyzer changes add no BE buffer ownership, allocator, nullable-column, or const-nullable path.
  • Performance: New work is limited to planner/DDL-time field or path scans. No scan-hot-path, material allocation, or complexity regression was found.
  • Other: No other distinct correctness issue survived validation and live-thread duplicate fencing. No additional user-provided review focus was supplied.

Review completion: both Round 2 normal full-review agents and both dedicated risk agents returned NO_NEW_VALUABLE_FINDINGS; every candidate was accepted, duplicate-fenced, or dismissed with evidence. The live head/base and current GitHub patches were revalidated immediately before submission.

updateSchema.makeColumnOptional(currentName);
}
applyPosition(updateSchema, position, column.getName());
IcebergNestedColumnEvolution.applyTopLevelPosition(

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] Canonicalize the other top-level positioning paths too. This fixes MODIFY by resolving both the target and AFTER sibling to persisted Iceberg spellings, but ADD still routes through applyPosition, which passes position.getAfterColumn() verbatim, and reorderColumns still passes every caller-provided name directly to the case-sensitive moveFirst/moveAfter APIs. On a table with stored fields Id/Label, ADD ... AFTER id and a reorder using label/id therefore still fail even though Doris resolves external columns case-insensitively. Please reuse schema-backed canonicalization for ADD and resolve the complete reorder list before staging it, with mixed-case catalog-backed tests.

private ElementAt canonicalizeStructSelector(ElementAt elementAt) {
Expression left = elementAt.left();
Expression right = elementAt.right();
if (left.getDataType() instanceof StructType && right instanceof StringLikeLiteral) {

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] Unwrap bracket-expression variables before canonicalizing. The parser builds payload[@field] directly as ElementAt, and ElementAt legality/type resolution uses getArgument(1), so a user variable whose value is displayed Unicode Σ is accepted. This helper checks the raw right child, sees Variable, and skips the rewrite; VariableToLiteral later exposes Σ, but only integer selectors are normalized afterward. BE then receives Σ against the thrift runtime name σ and fails lookup. Please canonicalize the wrapper-aware argument while preserving variable-use/SQL-cache accounting, and add a variable-backed bracket execution test. Function syntax is already unwrapped by constructUnboundFunctionArguments.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 95.00% (57/60) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

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

------ Round 1 ----------------------------------
============================================
q1	17627	3138	3114	3114
q2	2103	278	246	246
q3	10203	863	523	523
q4	4670	254	208	208
q5	7678	538	385	385
q6	140	115	95	95
q7	542	496	384	384
q8	9246	875	864	864
q9	3430	2393	2405	2393
q10	6524	864	728	728
q11	416	196	186	186
q12	685	265	207	207
q13	18242	1538	1154	1154
q14	163	148	143	143
q15	q16	443	389	369	369
q17	1408	834	831	831
q18	2985	2235	2255	2235
q19	1136	900	742	742
q20	368	280	203	203
q21	5473	1793	1872	1793
q22	327	268	224	224
Total cold run time: 93809 ms
Total hot run time: 17027 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	3493	3429	3379	3379
q2	509	412	366	366
q3	2208	2298	2152	2152
q4	1203	1169	887	887
q5	2187	2086	2100	2086
q6	171	117	88	88
q7	1038	916	884	884
q8	1613	1437	1406	1406
q9	3134	3150	3135	3135
q10	1860	1797	1604	1604
q11	361	271	252	252
q12	453	433	349	349
q13	1478	1529	1173	1173
q14	179	179	154	154
q15	q16	397	392	358	358
q17	3537	3350	3156	3156
q18	4807	4420	4722	4420
q19	882	921	896	896
q20	1014	972	844	844
q21	3881	3260	3224	3224
q22	399	359	316	316
Total cold run time: 34804 ms
Total hot run time: 31129 ms

@hello-stephen

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

query5	4243	410	332	332
query6	389	132	126	126
query7	4958	416	208	208
query8	286	127	121	121
query9	8690	2900	2914	2900
query10	411	221	187	187
query11	5376	1041	916	916
query12	115	70	71	70
query13	1188	435	327	327
query14	6070	2206	2076	2076
query14_1	1994	1937	1963	1937
query15	177	123	119	119
query16	920	370	363	363
query17	800	466	370	370
query18	2327	318	242	242
query19	162	142	118	118
query20	72	69	71	69
query21	205	100	88	88
query22	5379	5430	5373	5373
query23	6781	6239	5961	5961
query23_1	5884	6029	5983	5983
query24	7307	1102	800	800
query24_1	768	783	787	783
query25	438	299	260	260
query26	1238	239	130	130
query27	2779	448	268	268
query28	4657	1505	1526	1505
query29	941	444	347	347
query30	258	160	129	129
query31	814	396	336	336
query32	154	81	78	78
query33	481	227	176	176
query34	1013	822	491	491
query35	418	409	346	346
query36	563	582	558	558
query37	124	82	72	72
query38	1009	863	822	822
query39	511	488	477	477
query39_1	467	461	443	443
query40	214	94	78	78
query41	60	57	57	57
query42	78	73	73	73
query43	244	247	218	218
query44	1039	551	576	551
query45	112	131	94	94
query46	767	855	543	543
query47	752	750	727	727
query48	321	306	220	220
query49	522	233	193	193
query50	727	249	196	196
query51	8292	8102	8189	8102
query52	79	74	60	60
query53	200	212	144	144
query54	208	181	153	153
query55	78	57	64	57
query56	214	313	155	155
query57	687	660	653	653
query58	220	159	168	159
query59	1244	1224	1112	1112
query60	238	184	185	184
query61	135	113	124	113
query62	385	204	181	181
query63	169	142	143	142
query64	2734	761	585	585
query65	1642	1610	1574	1574
query66	1816	258	200	200
query67	9555	9650	9559	9559
query68	3029	1150	726	726
query69	333	225	183	183
query70	681	641	615	615
query71	266	178	164	164
query72	2318	1820	1588	1588
query73	671	557	330	330
query74	1986	1212	1133	1133
query75	1156	1094	951	951
query76	2352	717	535	535
query77	257	245	212	212
query78	3867	3748	3192	3192
query79	2320	821	605	605
query80	1589	325	278	278
query81	496	154	136	136
query82	1015	120	98	98
query83	280	212	193	193
query84	294	115	87	87
query85	782	362	296	296
query86	392	185	174	174
query87	1031	964	897	897
query88	2767	2101	2093	2093
query89	292	196	175	175
query90	1973	131	131	131
query91	131	115	98	98
query92	79	70	62	62
query93	1595	1111	727	727
query94	661	260	245	245
query95	509	313	228	228
query96	800	594	264	264
query97	1058	1058	1027	1027
query98	167	139	131	131
query99	427	346	331	331
Total cold run time: 178090 ms
Total hot run time: 82056 ms

@hello-stephen

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

query1	0.00	0.00	0.00
query2	0.08	0.03	0.03
query3	0.26	0.11	0.11
query4	1.61	0.10	0.10
query5	0.18	0.17	0.17
query6	1.25	0.69	0.69
query7	0.03	0.00	0.01
query8	0.05	0.03	0.04
query9	0.29	0.21	0.21
query10	0.36	0.34	0.34
query11	0.16	0.11	0.12
query12	0.15	0.13	0.12
query13	0.32	0.30	0.31
query14	0.46	0.44	0.45
query15	0.36	0.35	0.35
query16	0.22	0.23	0.22
query17	0.70	0.72	0.71
query18	0.18	0.17	0.17
query19	1.14	1.17	1.14
query20	0.01	0.02	0.02
query21	15.45	0.15	0.11
query22	5.09	0.04	0.05
query23	16.18	0.26	0.11
query24	3.08	0.32	0.26
query25	0.12	0.05	0.04
query26	0.72	0.17	0.11
query27	0.03	0.03	0.02
query28	3.71	0.52	0.29
query29	12.48	3.18	2.61
query30	0.25	0.10	0.12
query31	2.75	0.37	0.18
query32	3.51	0.32	0.22
query33	1.34	1.42	1.39
query34	15.35	2.19	1.76
query35	1.71	1.70	1.71
query36	0.45	0.28	0.28
query37	0.06	0.04	0.04
query38	0.05	0.03	0.03
query39	0.03	0.02	0.02
query40	0.11	0.07	0.08
query41	0.07	0.02	0.02
query42	0.03	0.02	0.02
query43	0.04	0.03	0.03
Total cold run time: 90.42 s
Total hot run time: 14.71 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 56.32% (49/87) 🎉
Increment coverage report
Complete coverage report

@Gabriel39
Gabriel39 merged commit 219c619 into apache:master Sep 3, 2026
35 checks passed
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

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

@github-actions github-actions Bot added approved Indicates a PR has been approved by one committer. dev/4.1.x-conflict labels Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by one committer. dev/4.1.x dev/4.1.x-conflict

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants