This is an automated email from the ASF dual-hosted git repository.

Gabriel39 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 219c6193f24 [fix](iceberg) Preserve nested field case in created 
schemas (#67166)
219c6193f24 is described below

commit 219c6193f24ed6c028c5f5469f18681be7c0292c
Author: Gabriel <[email protected]>
AuthorDate: Thu Sep 3 20:02:55 2026 +0800

    [fix](iceberg) Preserve nested field case in created schemas (#67166)
    
    ### 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
        - [x] Regression test
        - [x] 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.
    - [x] 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?
        - [x] No.
        - [ ] Yes.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 .../doris/connector/iceberg/IcebergCatalogOps.java |  17 ++-
 .../connector/iceberg/IcebergComplexTypeDiff.java  |  13 +-
 .../iceberg/IcebergConnectorMetadata.java          |  11 +-
 .../iceberg/IcebergNestedColumnEvolution.java      |  20 +++
 ...BackedIcebergCatalogOpsColumnEvolutionTest.java | 144 +++++++++++++++++++++
 ...cebergConnectorMetadataColumnEvolutionTest.java |  47 +++++++
 .../converter/ConnectorColumnConverter.java        |   2 +-
 .../nereids/rules/analysis/ExpressionAnalyzer.java |  29 ++++-
 .../rewrite/AccessPathExpressionCollector.java     |   6 +-
 .../nereids/rules/rewrite/NestedColumnPruning.java |  14 +-
 .../functions/scalar/CreateNamedStruct.java        |   3 +-
 .../org/apache/doris/nereids/types/DataType.java   |   4 +-
 .../apache/doris/nereids/types/StructField.java    |  45 ++++++-
 .../org/apache/doris/nereids/types/StructType.java |  22 +++-
 .../ExternalFileTableValuedFunction.java           |  10 +-
 .../doris/catalog/ColumnGsonSerializationTest.java |  72 +++++++++++
 ...teTableInfoToConnectorRequestConverterTest.java |  16 +++
 .../converter/ConnectorColumnConverterTest.java    |  43 ++++++
 .../rules/analysis/ExpressionAnalyzerTest.java     | 101 +++++++++++++++
 .../rewrite/AccessPathExpressionCollectorTest.java | 102 +++++++++++++++
 .../expressions/literal/StructLiteralTest.java     |  20 +++
 .../ExternalFileTableValuedFunctionTest.java       |  42 ++++++
 .../java/org/apache/doris/catalog/StructField.java |  39 +++++-
 .../java/org/apache/doris/catalog/StructType.java  |  23 +++-
 .../main/java/org/apache/doris/catalog/Type.java   |   2 +-
 .../test_iceberg_struct_schema_evolution.out       |   4 +-
 .../test_iceberg_struct_schema_evolution.groovy    |   6 +-
 .../test_iceberg_write_ctas_format_boundary.groovy |  84 +++++++++++-
 28 files changed, 888 insertions(+), 53 deletions(-)

diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogOps.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogOps.java
index 91d6c9b1c93..3b55a98be7c 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogOps.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergCatalogOps.java
@@ -531,10 +531,14 @@ public interface IcebergCatalogOps {
         public void modifyColumn(String dbName, String tableName, 
IcebergColumnChange column,
                 boolean commentSpecified, ConnectorColumnPosition position) {
             withTable(dbName, tableName, table -> {
-                Types.NestedField current = 
table.schema().findField(column.getName());
+                Schema schema = table.schema();
+                Types.NestedField current = 
IcebergNestedColumnEvolution.findTopLevelField(
+                        schema, column.getName());
                 if (current == null) {
                     throw new DorisConnectorException("Column " + 
column.getName() + " does not exist");
                 }
+                // Iceberg update paths are case-sensitive, so stage every 
change with the persisted spelling.
+                String currentName = current.name();
                 // Iceberg can widen required -> optional but never optional 
-> required (existing data may hold
                 // nulls), so a NOT NULL request on an already-nullable column 
fails loud — legacy parity
                 // (IcebergMetadataOps.validateForModifyColumn / 
validateForModifyComplexColumn).
@@ -555,7 +559,7 @@ public interface IcebergCatalogOps {
                         throw new DorisConnectorException("Modify column type 
from complex to primitive is not"
                                 + " supported: " + column.getName());
                     }
-                    updateSchema.updateColumn(column.getName(), 
newType.asPrimitiveType(), targetComment);
+                    updateSchema.updateColumn(currentName, 
newType.asPrimitiveType(), targetComment);
                 } else {
                     // A complex (STRUCT/ARRAY/MAP) modify diffs the new type 
against the current one field-by-field
                     // (IcebergComplexTypeDiff); the top-level column doc is 
updated separately, as in legacy.
@@ -563,16 +567,17 @@ public interface IcebergCatalogOps {
                         throw new DorisConnectorException("Modify column type 
from non-complex to complex is not"
                                 + " supported: " + column.getName());
                     }
-                    IcebergComplexTypeDiff.apply(updateSchema, 
column.getName(), current.type(), newType,
+                    IcebergComplexTypeDiff.apply(updateSchema, currentName, 
current.type(), newType,
                             column.getSourceType());
                     if (!Objects.equals(current.doc(), targetComment)) {
-                        updateSchema.updateColumnDoc(column.getName(), 
targetComment);
+                        updateSchema.updateColumnDoc(currentName, 
targetComment);
                     }
                 }
                 if (column.isNullable()) {
-                    updateSchema.makeColumnOptional(column.getName());
+                    updateSchema.makeColumnOptional(currentName);
                 }
-                applyPosition(updateSchema, position, column.getName());
+                IcebergNestedColumnEvolution.applyTopLevelPosition(
+                        updateSchema, position, currentName, schema, "modify");
                 updateSchema.commit();
                 return null;
             });
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergComplexTypeDiff.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergComplexTypeDiff.java
index 36aa7cef28f..ad8ab08a917 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergComplexTypeDiff.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergComplexTypeDiff.java
@@ -172,10 +172,11 @@ public final class IcebergComplexTypeDiff {
             Types.NestedField oldField = oldFields.get(i);
             Types.NestedField newField = newFields.get(i);
             String fieldPath = path + "." + oldField.name();
-            existingNames.add(oldField.name());
+            existingNames.add(lowercaseName(oldField.name()));
 
-            // Legacy ColumnType rule: existing fields are matched by position 
and may not be renamed.
-            if (!oldField.name().equals(newField.name())) {
+            // Iceberg defines case-insensitive identity with ROOT-lowercase 
keys. Java equalsIgnoreCase is
+            // broader for some Unicode characters and could otherwise route 
an update to the wrong field.
+            if 
(!lowercaseName(oldField.name()).equals(lowercaseName(newField.name()))) {
                 throw new DorisConnectorException("Cannot rename struct field 
from '" + oldField.name()
                         + "' to '" + newField.name() + "'");
             }
@@ -216,7 +217,7 @@ public final class IcebergComplexTypeDiff {
         // Append the new fields (legacy parity: must be nullable and not 
clash with an existing name).
         for (int i = oldFields.size(); i < newFields.size(); i++) {
             Types.NestedField newField = newFields.get(i);
-            if (existingNames.contains(newField.name())) {
+            if (!existingNames.add(lowercaseName(newField.name()))) {
                 throw new DorisConnectorException("Added struct field '" + 
newField.name()
                         + "' conflicts with existing field");
             }
@@ -227,6 +228,10 @@ public final class IcebergComplexTypeDiff {
         }
     }
 
+    private static String lowercaseName(String name) {
+        return name.toLowerCase(Locale.ROOT);
+    }
+
     private static void applyListChange(UpdateSchema updateSchema, String path,
             Types.ListType oldList, Types.ListType newList, ConnectorType 
newConn) {
         String elementPath = path + "." + 
oldList.field(oldList.elementId()).name();
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
index bbb83894026..10318ee9ee1 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadata.java
@@ -1303,7 +1303,7 @@ public class IcebergConnectorMetadata implements 
ConnectorMetadata {
             // generic "Unsupported type for Iceberg: SMALLINT" here. Restore 
the legacy parity message ("Cannot
             // change int to smallint in nested types") by validating the 
requested nested type against the
             // CURRENT type — legacy validated in Doris type space, where the 
narrow target still exists.
-            throw upgradeNestedModifyError(iceHandle, column, buildError);
+            throw upgradeNestedModifyError(iceHandle, 
ConnectorColumnPath.of(column.getName()), column, buildError);
         }
         // Carry the neutral source type so a complex-type diff can read each 
STRUCT field's commentSpecified.
         IcebergColumnChange change = new IcebergColumnChange(column.getName(), 
icebergType,
@@ -1328,15 +1328,15 @@ public class IcebergConnectorMetadata implements 
ConnectorMetadata {
      * against the CURRENT column type. Best-effort: a scalar modify, a load 
failure, or no offending nested leaf
      * keeps the original build error — so no other modify path changes.
      */
-    private DorisConnectorException 
upgradeNestedModifyError(IcebergTableHandle handle, ConnectorColumn column,
-            DorisConnectorException buildError) {
+    private DorisConnectorException 
upgradeNestedModifyError(IcebergTableHandle handle, ConnectorColumnPath path,
+            ConnectorColumn column, DorisConnectorException buildError) {
         if (!isComplexType(column.getType())) {
             return buildError;
         }
         try {
             Types.NestedField current = executeAuthenticated(() ->
                     catalogOps.withTable(handle.getDbName(), 
handle.getTableName(),
-                            table -> 
table.schema().findField(column.getName())));
+                            table -> 
IcebergNestedColumnEvolution.findFieldForErrorUpgrade(table.schema(), path)));
             if (current != null && !current.type().isPrimitiveType()) {
                 
IcebergComplexTypeDiff.validateNestedModifyRepresentable(current.type(), 
column.getType());
             }
@@ -1487,7 +1487,8 @@ public class IcebergConnectorMetadata implements 
ConnectorMetadata {
         try {
             icebergType = 
IcebergSchemaBuilder.buildColumnType(column.getType());
         } catch (DorisConnectorException buildError) {
-            throw upgradeNestedModifyError(iceHandle, column, buildError);
+            // Preserve the complete target identity so error parity cannot 
bind a same-named top-level field.
+            throw upgradeNestedModifyError(iceHandle, path, column, 
buildError);
         }
         // Carry the neutral source type so the nested complex-type diff can 
read each STRUCT field's
         // commentSpecified (an omitted COMMENT on a sub-field must keep its 
current doc, not clear it).
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergNestedColumnEvolution.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergNestedColumnEvolution.java
index b4ca1c02c7d..7bc5a0e52b8 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergNestedColumnEvolution.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergNestedColumnEvolution.java
@@ -492,6 +492,19 @@ public final class IcebergNestedColumnEvolution {
         return new ResolvedColumnPath(ConnectorColumnPath.of(canonicalParts), 
currentType, currentField);
     }
 
+    static NestedField findTopLevelField(Schema schema, String columnName) {
+        return schema.asStruct().caseInsensitiveField(columnName);
+    }
+
+    static NestedField findFieldForErrorUpgrade(Schema schema, 
ConnectorColumnPath columnPath) {
+        try {
+            return resolveColumnPath(schema, columnPath, "modify").getField();
+        } catch (DorisConnectorException ignored) {
+            // Error-message upgrading is best-effort and must not replace the 
original type-build failure.
+            return null;
+        }
+    }
+
     /**
      * Resolves {@code columnPath}'s parent (which must be a struct) and its 
leaf within that struct
      * (case-insensitive). Used by nested DROP / RENAME, which target an 
existing struct field.
@@ -584,6 +597,13 @@ public final class IcebergNestedColumnEvolution {
         }
     }
 
+    static void applyTopLevelPosition(UpdateSchema updateSchema, 
ConnectorColumnPosition position,
+            String columnName, Schema schema, String operation) {
+        if (position != null) {
+            applyPosition(updateSchema, position, 
ConnectorColumnPath.of(columnName), schema, operation);
+        }
+    }
+
     private static String getPositionReferencePath(Schema schema, 
ConnectorColumnPath columnPath,
             ConnectorColumnPosition position, String operation) {
         if (position == null || position.isFirst()) {
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java
index c0b9089b6fd..450ae232695 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/CatalogBackedIcebergCatalogOpsColumnEvolutionTest.java
@@ -367,6 +367,32 @@ public class 
CatalogBackedIcebergCatalogOpsColumnEvolutionTest {
         return ConnectorType.structOf(names, types, nullable, comments);
     }
 
+    @Test
+    public void testModifyColumnsCanonicalizeMixedCaseRootAndPosition() {
+        createMixedCaseTable();
+        ops.modifyColumn("db1", "mixed", change("id", Types.LongType.get(), 
"identifier", true), true, null);
+
+        ConnectorType requestedType = structType(
+                Arrays.asList("metric"), 
Arrays.asList(ConnectorType.of("BIGINT")),
+                Arrays.asList(true), Arrays.asList((String) null));
+
+        ops.modifyColumn("db1", "mixed",
+                new IcebergColumnChange("info", 
IcebergSchemaBuilder.buildColumnType(requestedType),
+                        "updated", null, true, requestedType),
+                true, ConnectorColumnPosition.after("id"));
+
+        Schema schema = reload("mixed");
+        Assertions.assertEquals(Arrays.asList("Id", "Info", "Label"), 
schema.columns().stream()
+                .map(Types.NestedField::name).collect(Collectors.toList()));
+        Assertions.assertEquals(Type.TypeID.LONG, 
schema.findField("Id").type().typeId());
+        Assertions.assertEquals("identifier", schema.findField("Id").doc());
+        Types.NestedField info = schema.findField("Info");
+        Assertions.assertEquals("updated", info.doc());
+        Types.NestedField metric = info.type().asStructType().fields().get(0);
+        Assertions.assertEquals("Metric", metric.name());
+        Assertions.assertEquals(Type.TypeID.LONG, metric.type().typeId());
+    }
+
     @Test
     public void testModifyStructAddsNullableField() {
         createTable("s_add", new ConnectorColumn("st",
@@ -397,6 +423,124 @@ public class 
CatalogBackedIcebergCatalogOpsColumnEvolutionTest {
         Assertions.assertEquals("new", a.doc());
     }
 
+    @Test
+    public void testModifyStructMatchesExistingFieldCaseInsensitively() {
+        createTable("s_case", new ConnectorColumn("st",
+                structType(Arrays.asList("CaseSensitive"), 
Arrays.asList(ConnectorType.of("INT")),
+                        Arrays.asList(true), Arrays.asList((String) null)), 
"", true, null, false));
+
+        modifyComplex("s_case", "st",
+                structType(Arrays.asList("casesensitive"), 
Arrays.asList(ConnectorType.of("BIGINT")),
+                        Arrays.asList(true), Arrays.asList((String) null)), 
true);
+
+        Types.NestedField field = 
reload("s_case").findField("st").type().asStructType().fields().get(0);
+        Assertions.assertEquals("CaseSensitive", field.name());
+        Assertions.assertEquals(Type.TypeID.LONG, field.type().typeId());
+    }
+
+    @Test
+    public void testModifyStructCommentMatchesExistingFieldCaseInsensitively() 
{
+        createTable("s_case_doc", new ConnectorColumn("st",
+                structType(Arrays.asList("CaseSensitive"), 
Arrays.asList(ConnectorType.of("INT")),
+                        Arrays.asList(true), Arrays.asList("old")), "", true, 
null, false));
+
+        modifyComplex("s_case_doc", "st",
+                structType(Arrays.asList("casesensitive"), 
Arrays.asList(ConnectorType.of("INT")),
+                        Arrays.asList(true), Arrays.asList("new")), true);
+
+        Types.NestedField field = 
reload("s_case_doc").findField("st").type().asStructType().fields().get(0);
+        Assertions.assertEquals("CaseSensitive", field.name());
+        Assertions.assertEquals("new", field.doc());
+    }
+
+    @Test
+    public void 
testModifyStructNullabilityMatchesExistingFieldCaseInsensitively() {
+        createTable("s_case_null", new ConnectorColumn("st",
+                structType(Arrays.asList("CaseSensitive"), 
Arrays.asList(ConnectorType.of("INT")),
+                        Arrays.asList(false), Arrays.asList((String) null)), 
"", true, null, false));
+
+        modifyComplex("s_case_null", "st",
+                structType(Arrays.asList("casesensitive"), 
Arrays.asList(ConnectorType.of("INT")),
+                        Arrays.asList(true), Arrays.asList((String) null)), 
true);
+
+        Types.NestedField field = 
reload("s_case_null").findField("st").type().asStructType().fields().get(0);
+        Assertions.assertEquals("CaseSensitive", field.name());
+        Assertions.assertTrue(field.isOptional());
+    }
+
+    @Test
+    public void 
testModifyStructUnderArrayMatchesExistingFieldCaseInsensitively() {
+        ConnectorType oldStruct = structType(
+                Arrays.asList("CaseSensitive"), 
Arrays.asList(ConnectorType.of("INT")),
+                Arrays.asList(true), Arrays.asList((String) null));
+        createTable("a_case", new ConnectorColumn(
+                "arr", ConnectorType.arrayOf(oldStruct), "", true, null, 
false));
+        ConnectorType newStruct = structType(
+                Arrays.asList("casesensitive"), 
Arrays.asList(ConnectorType.of("BIGINT")),
+                Arrays.asList(true), Arrays.asList((String) null));
+
+        modifyComplex("a_case", "arr", ConnectorType.arrayOf(newStruct), true);
+
+        Types.NestedField field = 
reload("a_case").findField("arr").type().asListType()
+                .elementType().asStructType().fields().get(0);
+        Assertions.assertEquals("CaseSensitive", field.name());
+        Assertions.assertEquals(Type.TypeID.LONG, field.type().typeId());
+    }
+
+    @Test
+    public void 
testModifyStructUnderMapMatchesExistingFieldCaseInsensitively() {
+        ConnectorType oldStruct = structType(
+                Arrays.asList("CaseSensitive"), 
Arrays.asList(ConnectorType.of("INT")),
+                Arrays.asList(true), Arrays.asList((String) null));
+        createTable("m_case", new ConnectorColumn("m",
+                ConnectorType.mapOf(ConnectorType.of("STRING"), oldStruct), 
"", true, null, false));
+        ConnectorType newStruct = structType(
+                Arrays.asList("casesensitive"), 
Arrays.asList(ConnectorType.of("BIGINT")),
+                Arrays.asList(true), Arrays.asList((String) null));
+
+        modifyComplex("m_case", "m",
+                ConnectorType.mapOf(ConnectorType.of("STRING"), newStruct), 
true);
+
+        Types.NestedField field = 
reload("m_case").findField("m").type().asMapType()
+                .valueType().asStructType().fields().get(0);
+        Assertions.assertEquals("CaseSensitive", field.name());
+        Assertions.assertEquals(Type.TypeID.LONG, field.type().typeId());
+    }
+
+    @Test
+    public void testModifyStructRejectsCaseInsensitiveAppendedFieldCollision() 
{
+        createTable("s_case_collision", new ConnectorColumn("st",
+                structType(Arrays.asList("CaseSensitive"), 
Arrays.asList(ConnectorType.of("INT")),
+                        Arrays.asList(true), Arrays.asList((String) null)), 
"", true, null, false));
+
+        DorisConnectorException ex = 
Assertions.assertThrows(DorisConnectorException.class,
+                () -> modifyComplex("s_case_collision", "st",
+                        structType(Arrays.asList("casesensitive", 
"CASESENSITIVE"),
+                                Arrays.asList(ConnectorType.of("INT"), 
ConnectorType.of("STRING")),
+                                Arrays.asList(true, true), Arrays.asList(null, 
null)), true));
+
+        Assertions.assertTrue(ex.getMessage().contains("conflicts with 
existing field"), ex.getMessage());
+    }
+
+    @Test
+    public void testModifyStructUsesIcebergLowercaseIdentity() {
+        createTable("s_unicode_identity", new ConnectorColumn("st",
+                structType(Arrays.asList("Σ", "ς"),
+                        Arrays.asList(ConnectorType.of("INT"), 
ConnectorType.of("INT")),
+                        Arrays.asList(true, true), Arrays.asList(null, null)), 
"", true, null, false));
+
+        DorisConnectorException ex = 
Assertions.assertThrows(DorisConnectorException.class,
+                () -> modifyComplex("s_unicode_identity", "st",
+                        structType(Arrays.asList("ς", "Σ"),
+                                Arrays.asList(ConnectorType.of("BIGINT"), 
ConnectorType.of("INT")),
+                                Arrays.asList(true, true), Arrays.asList(null, 
null)), true));
+
+        Assertions.assertTrue(ex.getMessage().contains("Cannot rename struct 
field"), ex.getMessage());
+        Types.StructType fields = 
reload("s_unicode_identity").findField("st").type().asStructType();
+        Assertions.assertEquals(Type.TypeID.INTEGER, 
fields.field("Σ").type().typeId());
+        Assertions.assertEquals(Type.TypeID.INTEGER, 
fields.field("ς").type().typeId());
+    }
+
     @Test
     public void testModifyStructFieldWidensNotNullToNullable() {
         createTable("s_null", new ConnectorColumn("st",
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataColumnEvolutionTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataColumnEvolutionTest.java
index da1a8e3557b..c82513e421c 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataColumnEvolutionTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergConnectorMetadataColumnEvolutionTest.java
@@ -20,6 +20,7 @@ package org.apache.doris.connector.iceberg;
 import org.apache.doris.connector.spi.ConnectorColumn;
 import org.apache.doris.connector.spi.ConnectorType;
 import org.apache.doris.connector.spi.DorisConnectorException;
+import org.apache.doris.connector.spi.ddl.ConnectorColumnPath;
 import org.apache.doris.connector.spi.ddl.ConnectorColumnPosition;
 
 import org.apache.iceberg.Schema;
@@ -84,6 +85,21 @@ public class IcebergConnectorMetadataColumnEvolutionTest {
         return catalog.createTable(TableIdentifier.of("db1", "t1"), schema);
     }
 
+    /** A real iceberg table with a nested {@code Root.Leaf ARRAY<INT>} target 
and an unrelated
+     * top-level {@code LEAF ARRAY<FLOAT>} field sharing the target's leaf 
name. */
+    private static Table tableWithNestedArrayAndTopLevelDecoy() {
+        InMemoryCatalog catalog = new InMemoryCatalog();
+        catalog.initialize("test", Collections.emptyMap());
+        catalog.createNamespace(Namespace.of("db1"));
+        Schema schema = new Schema(
+                Types.NestedField.optional(1, "Root", Types.StructType.of(
+                        Types.NestedField.optional(2, "Leaf",
+                                Types.ListType.ofOptional(3, 
Types.IntegerType.get())))),
+                Types.NestedField.optional(4, "LEAF",
+                        Types.ListType.ofOptional(5, Types.FloatType.get())));
+        return catalog.createTable(TableIdentifier.of("db1", "t1"), schema);
+    }
+
     // ---------- addColumn ----------
 
     @Test
@@ -290,6 +306,37 @@ public class IcebergConnectorMetadataColumnEvolutionTest {
         Assertions.assertEquals("Cannot change int to smallint in nested 
types", ex.getMessage());
     }
 
+    @Test
+    public void testModifyNestedColumnBuildErrorUsesFullTargetPath() {
+        RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+        ops.table = tableWithNestedArrayAndTopLevelDecoy();
+        RecordingConnectorContext ctx = new RecordingConnectorContext();
+        ConnectorColumn leaf = new ConnectorColumn("leaf",
+                ConnectorType.arrayOf(ConnectorType.of("SMALLINT")), "", true, 
null, false);
+
+        DorisConnectorException ex = 
Assertions.assertThrows(DorisConnectorException.class,
+                () -> metadata(ops, ctx).modifyNestedColumn(null, HANDLE,
+                        ConnectorColumnPath.of(Arrays.asList("root", "leaf")), 
leaf, null));
+
+        // Error parity must use the resolved nested target, never a 
same-named top-level field.
+        Assertions.assertEquals("Cannot change int to smallint in nested 
types", ex.getMessage());
+    }
+
+    @Test
+    public void testModifyMissingComplexColumnKeepsBuildError() {
+        RecordingIcebergCatalogOps ops = new RecordingIcebergCatalogOps();
+        ops.table = tableWithArrayIntColumn();
+        RecordingConnectorContext ctx = new RecordingConnectorContext();
+        ConnectorColumn missing = new ConnectorColumn("missing",
+                ConnectorType.arrayOf(ConnectorType.of("SMALLINT")), "", true, 
null, false);
+
+        DorisConnectorException ex = 
Assertions.assertThrows(DorisConnectorException.class,
+                () -> metadata(ops, ctx).modifyColumn(null, HANDLE, missing, 
null));
+
+        // Error upgrading is best-effort; an unresolved target must retain 
the original build failure.
+        Assertions.assertEquals("Unsupported type for Iceberg: SMALLINT", 
ex.getMessage());
+    }
+
     @Test
     public void testModifyScalarColumnToUnrepresentableKeepsBuildError() {
         // A TOP-LEVEL (non-nested) modify to an iceberg-unrepresentable type 
keeps the generic build error: the
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/connector/converter/ConnectorColumnConverter.java
 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/connector/converter/ConnectorColumnConverter.java
index 8a2d4dd38c6..24986554c64 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/datasource/connector/converter/ConnectorColumnConverter.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/datasource/connector/converter/ConnectorColumnConverter.java
@@ -238,7 +238,7 @@ public final class ConnectorColumnConverter {
             // isCommentSpecified() so the diff can tell an omitted COMMENT 
(preserve the current doc) from
             // COMMENT '' (clear it) — the comment string is "" for both 
(#65329 omit-preserves-metadata).
             for (StructField f : struct.getFields()) {
-                names.add(f.getName());
+                names.add(f.getOriginalName());
                 types.add(toConnectorType(f.getType()));
                 nullables.add(f.getContainsNull());
                 comments.add(f.getComment());
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
index fd7db58c0b0..bf7e872de20 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzer.java
@@ -273,7 +273,9 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
             StructType structType = (StructType) dataType;
             StructField field = 
structType.getField(dereferenceExpression.fieldName);
             if (field != null) {
-                return new ElementAt(expression, 
dereferenceExpression.child(1));
+                // This newly constructed node returns directly and will not 
be revisited by visitElementAt.
+                return canonicalizeStructSelector(
+                        new ElementAt(expression, 
dereferenceExpression.child(1)));
             }
         } else if (dataType.isMapType()) {
             return new ElementAt(expression, dereferenceExpression.child(1));
@@ -299,6 +301,7 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
         }
         Expression right = elementAt.right().accept(this, context);
         elementAt = (ElementAt) elementAt.withChildren(left, right);
+        elementAt = canonicalizeStructSelector(elementAt);
         Expression coerced = TypeCoercionUtils.processBoundFunction(elementAt);
         if (isEnableVariantSchemaAutoCast(context)) {
             return wrapVariantElementAtWithCast(coerced);
@@ -617,7 +620,12 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
             // we do type coercion in build function in alias function, so 
it's ok to return directly.
             return buildResult.first;
         } else {
-            Expression castFunction = 
TypeCoercionUtils.processBoundFunction((BoundFunction) buildResult.first);
+            BoundFunction boundFunction = (BoundFunction) buildResult.first;
+            if (boundFunction instanceof ElementAt) {
+                // SQL function syntax binds here directly and therefore does 
not visit visitElementAt above.
+                boundFunction = canonicalizeStructSelector((ElementAt) 
boundFunction);
+            }
+            Expression castFunction = 
TypeCoercionUtils.processBoundFunction(boundFunction);
             if (castFunction instanceof RewriteWhenAnalyze) {
                 castFunction = ((RewriteWhenAnalyze) 
castFunction).rewriteWhenAnalyze();
             }
@@ -631,6 +639,20 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
         return TypeCoercionUtils.processBoundFunction(boundFunction);
     }
 
+    private ElementAt canonicalizeStructSelector(ElementAt elementAt) {
+        Expression left = elementAt.left();
+        Expression right = elementAt.right();
+        if (left.getDataType() instanceof StructType && right instanceof 
StringLikeLiteral) {
+            String selector = ((StringLikeLiteral) right).getStringValue();
+            StructField field = ((StructType) 
left.getDataType()).getField(selector);
+            if (field != null && !field.getName().equals(selector)) {
+                // BE struct names use the normalized thrift identity and 
cannot Unicode-fold external spelling.
+                return (ElementAt) elementAt.withChildren(left, new 
StringLiteral(field.getName()));
+            }
+        }
+        return elementAt;
+    }
+
     @Override
     public Expression visitWindow(WindowExpression windowExpression, 
ExpressionRewriteContext context) {
         windowExpression = (WindowExpression) 
super.visitWindow(windowExpression, context);
@@ -1286,7 +1308,8 @@ public class ExpressionAnalyzer extends 
SubExprAnalyzer<ExpressionRewriteContext
                     throw new AnalysisException("No such struct field '" + 
fieldName + "' in '" + lastFieldName + "'");
                 }
                 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()));
                 continue;
             } else if (dataType.isMapType()) {
                 expression = new ElementAt(expression, new 
StringLiteral(fieldName));
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AccessPathExpressionCollector.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AccessPathExpressionCollector.java
index 316a0f76833..f03047e6348 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AccessPathExpressionCollector.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/AccessPathExpressionCollector.java
@@ -74,6 +74,7 @@ import com.google.common.collect.Multimap;
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
@@ -148,7 +149,7 @@ public class AccessPathExpressionCollector extends 
DefaultExpressionVisitor<Void
             return null;
         }
         if (dataType instanceof NestedColumnPrunable) {
-            
context.accessPathBuilder.addPrefix(slotReference.getName().toLowerCase());
+            
context.accessPathBuilder.addPrefix(slotReference.getName().toLowerCase(Locale.ROOT));
             ImmutableList<String> path = 
Utils.fastToImmutableList(context.accessPathBuilder.accessPath);
             int slotId = slotReference.getExprId().asInt();
             slotToAccessPaths.put(slotId, new CollectAccessPathResult(path, 
context.bottomFilter, context.type));
@@ -359,7 +360,8 @@ public class AccessPathExpressionCollector extends 
DefaultExpressionVisitor<Void
                         return continueCollectAccessPath(first, context);
                     }
                 }
-                context.accessPathBuilder.addPrefix(((Literal) 
fieldName).getStringValue().toLowerCase());
+                context.accessPathBuilder.addPrefix(
+                        ((Literal) 
fieldName).getStringValue().toLowerCase(Locale.ROOT));
                 return continueCollectAccessPath(first, context);
             }
             return visit(elementAt, context);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NestedColumnPruning.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NestedColumnPruning.java
index 3faf0d581a5..64153355fc3 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NestedColumnPruning.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/NestedColumnPruning.java
@@ -56,6 +56,7 @@ import java.util.Collection;
 import java.util.Comparator;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
@@ -620,7 +621,8 @@ public class NestedColumnPruning implements CustomRewriter {
                 List<StructField> fields = ((StructType) 
cast.type).getFields();
                 for (int i = 0; i < fields.size(); i++) {
                     String castFieldName = path.get(index);
-                    if 
(fields.get(i).getName().equalsIgnoreCase(castFieldName)) {
+                    // Struct runtime keys are ROOT-normalized; broad folding 
can merge distinct siblings.
+                    if (fields.get(i).getName().equals(castFieldName)) {
                         String originFieldName = ((StructType) 
type).getFields().get(i).getName();
                         path.set(index, originFieldName);
                         return 
children.get(originFieldName).replacePathByAnotherTree(
@@ -662,7 +664,7 @@ public class NestedColumnPruning implements CustomRewriter {
             accessPartialChild = true;
 
             if (this.type.isStructType()) {
-                String fieldName = path.get(accessIndex).toLowerCase();
+                String fieldName = 
path.get(accessIndex).toLowerCase(Locale.ROOT);
                 DataTypeAccessTree child = children.get(fieldName);
                 if (child != null) {
                     child.setAccessByPath(path, accessIndex + 1, pathType);
@@ -730,7 +732,8 @@ public class NestedColumnPruning implements CustomRewriter {
                 accessAll = true;
                 return;
             } else if (isRoot) {
-                
children.get(path.get(accessIndex).toLowerCase()).setAccessByPath(path, 
accessIndex + 1, pathType);
+                children.get(path.get(accessIndex).toLowerCase(Locale.ROOT))
+                        .setAccessByPath(path, accessIndex + 1, pathType);
                 return;
             }
             throw new AnalysisException("unsupported data type: " + this.type);
@@ -739,7 +742,7 @@ public class NestedColumnPruning implements CustomRewriter {
         public static DataTypeAccessTree ofRoot(Slot slot, 
ColumnAccessPathType pathType) {
             DataTypeAccessTree child = of(slot.getDataType(), pathType);
             DataTypeAccessTree root = new DataTypeAccessTree(true, 
NullType.INSTANCE, pathType);
-            root.children.put(slot.getName().toLowerCase(), child);
+            root.children.put(slot.getName().toLowerCase(Locale.ROOT), child);
             return root;
         }
 
@@ -749,7 +752,8 @@ public class NestedColumnPruning implements CustomRewriter {
             if (type instanceof StructType) {
                 StructType structType = (StructType) type;
                 for (Entry<String, StructField> kv : 
structType.getNameToFields().entrySet()) {
-                    root.children.put(kv.getKey().toLowerCase(), 
of(kv.getValue().getDataType(), pathType));
+                    root.children.put(kv.getKey().toLowerCase(Locale.ROOT),
+                            of(kv.getValue().getDataType(), pathType));
                 }
             } else if (type instanceof ArrayType) {
                 root.children.put(AccessPathInfo.ACCESS_ALL, of(((ArrayType) 
type).getItemType(), pathType));
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CreateNamedStruct.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CreateNamedStruct.java
index c27b5cc94c9..49d089bd4a6 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CreateNamedStruct.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CreateNamedStruct.java
@@ -35,6 +35,7 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 
 import java.util.List;
+import java.util.Locale;
 import java.util.Set;
 
 /**
@@ -70,7 +71,7 @@ public class CreateNamedStruct extends ScalarFunction 
implements CustomSignature
                 throw new AnalysisException("named_struct only allows"
                         + " constant string parameter in odd position: " + 
this);
             } else {
-                String name = ((StringLikeLiteral) 
child(i)).getStringValue().toLowerCase();
+                String name = ((StringLikeLiteral) 
child(i)).getStringValue().toLowerCase(Locale.ROOT);
                 if (names.contains(name)) {
                     throw new AnalysisException("The name of the struct field 
cannot be repeated."
                             + " same name fields are " + name);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DataType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DataType.java
index d70079555c7..6c62dbc2bd0 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DataType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/DataType.java
@@ -467,9 +467,9 @@ public abstract class DataType {
 
         if (type.isStructType()) {
             List<StructField> structFields = 
((org.apache.doris.catalog.StructType) (type)).getFields().stream()
-                    .map(cf -> new StructField(cf.getName(), 
fromCatalogType(cf.getType()),
+                    .map(cf -> new StructField(cf.getName(), 
cf.getOriginalName(), fromCatalogType(cf.getType()),
                             cf.getContainsNull(), cf.getComment() == null ? "" 
: cf.getComment(),
-                            cf.isCommentSpecified()))
+                            cf.isCommentSpecified(), !cf.hasOriginalName()))
                     .collect(ImmutableList.toImmutableList());
             return new StructType(structFields);
         } else if (type.isMapType()) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructField.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructField.java
index aefcf20f227..7e8699293ae 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructField.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructField.java
@@ -22,6 +22,7 @@ import org.apache.doris.nereids.parser.NereidsParser;
 import org.apache.doris.nereids.util.SqlLiteralUtils;
 import org.apache.doris.nereids.util.Utils;
 
+import java.util.Locale;
 import java.util.Objects;
 
 /**
@@ -32,10 +33,12 @@ public class StructField {
     public static final String DEFAULT_FIELD_NAME = "col";
 
     private final String name;
+    private final String originalName;
     private final DataType dataType;
     private final boolean nullable;
     private final String comment;
     private final boolean commentSpecified;
+    private final boolean legacyLocaleDependentName;
 
     /**
      * StructField Constructor
@@ -49,17 +52,44 @@ public class StructField {
 
     public StructField(String name, DataType dataType, boolean nullable, 
String comment,
             boolean commentSpecified) {
-        this.name = Objects.requireNonNull(name, "name should not be 
null").toLowerCase();
+        this(name, name, dataType, nullable, comment, commentSpecified);
+    }
+
+    /**
+     * Creates a field with separate names for case-insensitive runtime lookup 
and external schema spelling.
+     *
+     * @param name field name normalized internally for runtime lookup
+     * @param originalName field spelling preserved for external schema 
metadata
+     * @param dataType field data type
+     * @param nullable whether the field accepts null values
+     * @param comment field comment
+     * @param commentSpecified whether the comment was explicitly specified
+     */
+    public StructField(String name, String originalName, DataType dataType, 
boolean nullable, String comment,
+            boolean commentSpecified) {
+        this(name, originalName, dataType, nullable, comment, 
commentSpecified, false);
+    }
+
+    StructField(String name, String originalName, DataType dataType, boolean 
nullable, String comment,
+            boolean commentSpecified, boolean legacyLocaleDependentName) {
+        // Runtime field identity must stay stable across FE locales and match 
external schema lookup keys.
+        this.name = Objects.requireNonNull(name, "name should not be 
null").toLowerCase(Locale.ROOT);
+        this.originalName = Objects.requireNonNull(originalName, "originalName 
should not be null");
         this.dataType = Objects.requireNonNull(dataType, "dataType should not 
be null");
         this.nullable = nullable;
         this.comment = Objects.requireNonNull(comment, "comment should not be 
null");
         this.commentSpecified = commentSpecified;
+        this.legacyLocaleDependentName = legacyLocaleDependentName;
     }
 
     public String getName() {
         return name;
     }
 
+    public String getOriginalName() {
+        return originalName;
+    }
+
     public DataType getDataType() {
         return dataType;
     }
@@ -76,6 +106,10 @@ public class StructField {
         return commentSpecified;
     }
 
+    boolean isLegacyLocaleDependentName() {
+        return legacyLocaleDependentName;
+    }
+
     public StructField conversion() {
         if (this.dataType.equals(dataType.conversion())) {
             return this;
@@ -84,16 +118,19 @@ public class StructField {
     }
 
     public StructField withDataType(DataType dataType) {
-        return new StructField(name, dataType, nullable, comment, 
commentSpecified);
+        return new StructField(name, originalName, dataType, nullable, 
comment, commentSpecified,
+                legacyLocaleDependentName);
     }
 
     public StructField withDataTypeAndNullable(DataType dataType, boolean 
nullable) {
-        return new StructField(name, dataType, nullable, comment, 
commentSpecified);
+        return new StructField(name, originalName, dataType, nullable, 
comment, commentSpecified,
+                legacyLocaleDependentName);
     }
 
     public org.apache.doris.catalog.StructField toCatalogDataType() {
         return new org.apache.doris.catalog.StructField(
-                name, dataType.toCatalogDataType(), comment, nullable, 
commentSpecified);
+                name, legacyLocaleDependentName ? null : originalName,
+                dataType.toCatalogDataType(), comment, nullable, 
commentSpecified);
     }
 
     public String toSql() {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructType.java 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructType.java
index 13f28c2e06e..8f2893ec55f 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructType.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/types/StructType.java
@@ -29,6 +29,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
@@ -60,7 +61,7 @@ public class StructType extends DataType implements 
ComplexDataType, NestedColum
         // ATTN: should use LinkedHashMap to keep order
         this.nameToFields = new LinkedHashMap<>();
         for (StructField field : this.fields) {
-            String fieldName = field.getName().toLowerCase();
+            String fieldName = field.getName().toLowerCase(Locale.ROOT);
             StructField existingField = this.nameToFields.put(fieldName, 
field);
             if (existingField != null) {
                 throw new AnalysisException("Duplicate field name found: " + 
fieldName);
@@ -76,8 +77,25 @@ public class StructType extends DataType implements 
ComplexDataType, NestedColum
         return nameToFields;
     }
 
+    /** Get a field by its case-insensitive runtime name. */
     public StructField getField(String name) {
-        return nameToFields.get(name.toLowerCase());
+        StructField field = nameToFields.get(name.toLowerCase(Locale.ROOT));
+        if (field != null && (!field.isLegacyLocaleDependentName() || 
field.getName().equals(name))) {
+            return field;
+        }
+        StructField legacyMatch = null;
+        for (int i = fields.size() - 1; i >= 0; i--) {
+            StructField legacyField = fields.get(i);
+            // Limit broad case folding to old replayed fields so new 
ROOT-distinct names remain distinct.
+            if (legacyField.isLegacyLocaleDependentName() && 
legacyField.getName().equalsIgnoreCase(name)) {
+                if (legacyMatch != null) {
+                    // The old locale was not persisted, so choosing either 
folded sibling could return wrong data.
+                    return null;
+                }
+                legacyMatch = legacyField;
+            }
+        }
+        return legacyMatch != null ? legacyMatch : field;
     }
 
     @Override
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/ExternalFileTableValuedFunction.java
 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/ExternalFileTableValuedFunction.java
index 01abd8b4821..eb21be855a8 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/tablefunction/ExternalFileTableValuedFunction.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/tablefunction/ExternalFileTableValuedFunction.java
@@ -98,6 +98,7 @@ import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -421,13 +422,16 @@ public abstract class ExternalFileTableValuedFunction 
extends TableValuedFunctio
             for (int i = 0; i < typeNodes.get(start).getStructFieldsCount(); 
++i) {
                 Pair<Type, Integer> fieldType = getColumnType(typeNodes, start 
+ parsedNodes);
                 PStructField structField = 
typeNodes.get(start).getStructFields(i);
-                String fieldName = structField.getName().toLowerCase();
+                String originalFieldName = structField.getName();
+                String fieldName = originalFieldName.toLowerCase(Locale.ROOT);
                 if (fieldLowerNames.contains(fieldName)) {
                     throw new NotSupportedException("Repeated lowercase field 
names: " + fieldName);
                 } else {
                     fieldLowerNames.add(fieldName);
-                    fields.add(new StructField(fieldName, fieldType.key(), 
structField.getComment(),
-                            structField.getContainsNull()));
+                    // File readers return the external schema spelling, which 
must survive CTAS metadata writes;
+                    // only the runtime lookup key and duplicate detection are 
normalized.
+                    fields.add(new StructField(fieldName, originalFieldName, 
fieldType.key(), structField.getComment(),
+                            structField.getContainsNull(), 
!structField.getComment().isEmpty()));
                 }
                 parsedNodes += fieldType.value();
             }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/catalog/ColumnGsonSerializationTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/ColumnGsonSerializationTest.java
index 6e81121ca04..dddc1a7ecf1 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/catalog/ColumnGsonSerializationTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/catalog/ColumnGsonSerializationTest.java
@@ -23,6 +23,10 @@ import org.apache.doris.common.io.Writable;
 import org.apache.doris.persist.gson.GsonUtils;
 
 import com.google.common.collect.Lists;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
 import com.google.gson.annotations.SerializedName;
 import org.junit.After;
 import org.junit.Assert;
@@ -121,4 +125,72 @@ public class ColumnGsonSerializationTest {
         in.close();
     }
 
+    @Test
+    public void testReplayPreRootTurkishStructFieldName() {
+        StructType structType = new StructType(new StructField("I", Type.INT));
+        JsonObject legacyJson = JsonParser.parseString(
+                GsonUtils.GSON.toJson(structType, 
Type.class)).getAsJsonObject();
+
+        JsonObject fieldMap = legacyJson.getAsJsonObject("fieldMap");
+        JsonElement legacyMapField = fieldMap.remove("i");
+        setLegacyTurkishFieldName(legacyMapField.getAsJsonObject());
+        fieldMap.add("ı", legacyMapField);
+        JsonArray fields = legacyJson.getAsJsonArray("fields");
+        setLegacyTurkishFieldName(fields.get(0).getAsJsonObject());
+
+        StructType replayed = (StructType) GsonUtils.GSON.fromJson(legacyJson, 
Type.class);
+        Assert.assertEquals("ı", replayed.getField("I").getName());
+
+        org.apache.doris.nereids.types.StructType nereidsType =
+                (org.apache.doris.nereids.types.StructType)
+                        
org.apache.doris.nereids.types.DataType.fromCatalogType(replayed);
+        Assert.assertEquals("ı", nereidsType.getField("I").getName());
+
+        StructType roundTrip = (StructType) nereidsType.toCatalogDataType();
+        Assert.assertEquals("ı", roundTrip.getField("I").getName());
+    }
+
+    @Test
+    public void testCurrentDotlessStructFieldDoesNotMatchAsciiI() {
+        StructType structType = new StructType(new StructField("ı", Type.INT));
+        Assert.assertNull(structType.getField("I"));
+
+        org.apache.doris.nereids.types.StructType nereidsType =
+                (org.apache.doris.nereids.types.StructType)
+                        
org.apache.doris.nereids.types.DataType.fromCatalogType(structType);
+        Assert.assertNull(nereidsType.getField("I"));
+    }
+
+    @Test
+    public void testReplayPreRootTurkishStructFieldCollisionIsAmbiguous() {
+        StructType structType = new StructType(
+                new StructField("ı", Type.INT), new StructField("i", 
Type.BIGINT));
+        JsonObject legacyJson = JsonParser.parseString(
+                GsonUtils.GSON.toJson(structType, 
Type.class)).getAsJsonObject();
+        legacyJson.getAsJsonObject("fieldMap").entrySet().forEach(
+                entry -> 
entry.getValue().getAsJsonObject().remove("originalName"));
+        legacyJson.getAsJsonArray("fields").forEach(
+                field -> field.getAsJsonObject().remove("originalName"));
+
+        StructType replayed = (StructType) GsonUtils.GSON.fromJson(legacyJson, 
Type.class);
+        Assert.assertNull(replayed.getField("I"));
+        Assert.assertEquals("ı", replayed.getField("ı").getName());
+        Assert.assertEquals("i", replayed.getField("i").getName());
+
+        org.apache.doris.nereids.types.StructType nereidsType =
+                (org.apache.doris.nereids.types.StructType)
+                        
org.apache.doris.nereids.types.DataType.fromCatalogType(replayed);
+        Assert.assertNull(nereidsType.getField("I"));
+        Assert.assertEquals("ı", nereidsType.getField("ı").getName());
+        Assert.assertEquals("i", nereidsType.getField("i").getName());
+
+        StructType roundTrip = (StructType) nereidsType.toCatalogDataType();
+        Assert.assertNull(roundTrip.getField("I"));
+    }
+
+    private static void setLegacyTurkishFieldName(JsonObject field) {
+        field.addProperty("name", "ı");
+        field.remove("originalName");
+    }
+
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/connector/ddl/CreateTableInfoToConnectorRequestConverterTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/connector/ddl/CreateTableInfoToConnectorRequestConverterTest.java
index 6abe426c049..471acd6861d 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/connector/ddl/CreateTableInfoToConnectorRequestConverterTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/connector/ddl/CreateTableInfoToConnectorRequestConverterTest.java
@@ -37,6 +37,8 @@ import 
org.apache.doris.nereids.trees.plans.commands.info.PartitionTableInfo;
 import org.apache.doris.nereids.trees.plans.commands.info.SortFieldInfo;
 import org.apache.doris.nereids.types.IntegerType;
 import org.apache.doris.nereids.types.StringType;
+import org.apache.doris.nereids.types.StructField;
+import org.apache.doris.nereids.types.StructType;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -98,6 +100,20 @@ public class CreateTableInfoToConnectorRequestConverterTest 
{
         Assertions.assertNull(req.getBucketSpec());
     }
 
+    @Test
+    public void nestedFieldSpellingIsPreservedForConnectorSchemas() {
+        StructType payloadType = new StructType(ImmutableList.of(
+                new StructField("CaseSensitive", IntegerType.INSTANCE, true, 
"")));
+        ColumnDefinition payload = new ColumnDefinition("payload", 
payloadType, true);
+        CreateTableInfo info = stubInfo("t", 
Collections.singletonList(payload),
+                null, null, "", Collections.emptyMap(), false);
+
+        ConnectorCreateTableRequest request = 
CreateTableInfoToConnectorRequestConverter.convert(info, "db");
+
+        Assertions.assertEquals(Collections.singletonList("CaseSensitive"),
+                request.getColumns().get(0).getType().getFieldNames());
+    }
+
     @Test
     public void autoIncInitValueIsPropagatedAsIsAutoInc() {
         // ColumnDefinition is mocked (its auto-inc ctor pulls in 
ColumnNullableType machinery);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/connector/converter/ConnectorColumnConverterTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/connector/converter/ConnectorColumnConverterTest.java
index 71bb443e186..85a5c65f3e5 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/datasource/connector/converter/ConnectorColumnConverterTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/datasource/connector/converter/ConnectorColumnConverterTest.java
@@ -35,6 +35,7 @@ import org.junit.jupiter.api.Test;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Locale;
 
 class ConnectorColumnConverterTest {
 
@@ -117,6 +118,48 @@ class ConnectorColumnConverterTest {
         Assertions.assertEquals(ScalarType.INT, 
backStruct.getFields().get(0).getType());
     }
 
+    @Test
+    void mixedCaseStructFieldKeepsSchemaSpellingAndNormalizedRuntimeName() {
+        ConnectorType connectorType = ConnectorType.structOf(
+                Arrays.asList("CaseSensitive"), 
Arrays.asList(ConnectorType.of("INT")));
+
+        StructType converted = (StructType) 
ConnectorColumnConverter.convertType(connectorType);
+        StructField field = converted.getFields().get(0);
+
+        Assertions.assertEquals("casesensitive", field.getName());
+        Assertions.assertEquals("CaseSensitive", field.getOriginalName());
+        Assertions.assertEquals("struct<CaseSensitive:int>", 
converted.toSql());
+        Assertions.assertEquals("casesensitive",
+                
converted.toThrift().getTypes().get(0).getStructFields().get(0).getName());
+    }
+
+    @Test
+    void structRuntimeNamesUseRootLocale() {
+        Locale originalLocale = Locale.getDefault();
+        try {
+            Locale.setDefault(Locale.forLanguageTag("tr-TR"));
+            ConnectorType connectorType = ConnectorType.structOf(
+                    Arrays.asList("I", "ı"),
+                    Arrays.asList(ConnectorType.of("INT"), 
ConnectorType.of("STRING")));
+
+            StructType catalogType = (StructType) 
ConnectorColumnConverter.convertType(connectorType);
+            Assertions.assertEquals("i", 
catalogType.getFields().get(0).getName());
+            Assertions.assertEquals("ı", 
catalogType.getFields().get(1).getName());
+            Assertions.assertSame(catalogType.getFields().get(0), 
catalogType.getField("i"));
+            Assertions.assertSame(catalogType.getFields().get(1), 
catalogType.getField("ı"));
+
+            org.apache.doris.nereids.types.StructType nereidsType =
+                    (org.apache.doris.nereids.types.StructType)
+                            
org.apache.doris.nereids.types.DataType.fromCatalogType(catalogType);
+            Assertions.assertEquals("i", 
nereidsType.getFields().get(0).getName());
+            Assertions.assertEquals("ı", 
nereidsType.getFields().get(1).getName());
+            Assertions.assertSame(nereidsType.getFields().get(0), 
nereidsType.getField("i"));
+            Assertions.assertSame(nereidsType.getFields().get(1), 
nereidsType.getField("ı"));
+        } finally {
+            Locale.setDefault(originalLocale);
+        }
+    }
+
     @Test
     void testNestedComplexType() {
         // ARRAY<MAP<STRING, INT>>
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
index 6c70a37aa0c..4f15f00ab56 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/analysis/ExpressionAnalyzerTest.java
@@ -17,13 +17,16 @@
 
 package org.apache.doris.nereids.rules.analysis;
 
+import org.apache.doris.nereids.CascadesContext;
 import org.apache.doris.nereids.analyzer.Scope;
 import org.apache.doris.nereids.analyzer.UnboundFunction;
 import org.apache.doris.nereids.analyzer.UnboundSlot;
 import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.trees.expressions.Alias;
 import org.apache.doris.nereids.trees.expressions.And;
 import org.apache.doris.nereids.trees.expressions.BoundStar;
 import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.DereferenceExpression;
 import org.apache.doris.nereids.trees.expressions.ExprId;
 import org.apache.doris.nereids.trees.expressions.Expression;
 import org.apache.doris.nereids.trees.expressions.IsFalse;
@@ -31,11 +34,17 @@ import org.apache.doris.nereids.trees.expressions.IsNull;
 import org.apache.doris.nereids.trees.expressions.IsTrue;
 import org.apache.doris.nereids.trees.expressions.Not;
 import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.ElementAt;
 import org.apache.doris.nereids.trees.expressions.literal.DateTimeV2Literal;
+import org.apache.doris.nereids.trees.expressions.literal.StringLikeLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
 import org.apache.doris.nereids.trees.expressions.literal.TinyIntLiteral;
 import org.apache.doris.nereids.types.BigIntType;
 import org.apache.doris.nereids.types.BooleanType;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.StructField;
+import org.apache.doris.nereids.types.StructType;
+import org.apache.doris.qe.ConnectContext;
 
 import com.google.common.collect.ImmutableList;
 import org.junit.jupiter.api.Assertions;
@@ -133,4 +142,96 @@ public class ExpressionAnalyzerTest {
         Assertions.assertInstanceOf(Not.class, isNotFalse);
         Assertions.assertInstanceOf(And.class, isNotFalse.child(0));
     }
+
+    @Test
+    public void testStructElementAtCanonicalizesUnicodeSelector() {
+        StructType structType = new StructType(ImmutableList.of(
+                new StructField("Σ", "Σ", IntegerType.INSTANCE, true, "", 
false),
+                new StructField("ẞ", "ẞ", IntegerType.INSTANCE, true, "", 
false)));
+        SlotReference payload = new SlotReference(
+                new ExprId(1), "payload", structType, true, 
ImmutableList.of());
+        ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, new 
Scope(ImmutableList.of()),
+                null, true, true);
+
+        Expression analyzedSigma = analyzer.analyze(new ElementAt(payload, new 
StringLiteral("Σ")));
+        Expression analyzedSharpS = analyzer.analyze(new ElementAt(payload, 
new StringLiteral("ẞ")));
+
+        Assertions.assertInstanceOf(ElementAt.class, analyzedSigma);
+        Assertions.assertInstanceOf(ElementAt.class, analyzedSharpS);
+        // The BE receives the ROOT-normalized thrift name and cannot 
Unicode-fold the displayed spelling.
+        Assertions.assertEquals("σ", ((StringLikeLiteral) 
analyzedSigma.child(1)).getStringValue());
+        Assertions.assertEquals("ß", ((StringLikeLiteral) 
analyzedSharpS.child(1)).getStringValue());
+    }
+
+    @Test
+    public void testStructElementAtFunctionCanonicalizesUnicodeSelector() {
+        StructType structType = new StructType(ImmutableList.of(
+                new StructField("Σ", "Σ", IntegerType.INSTANCE, true, "", 
false)));
+        SlotReference payload = new SlotReference(
+                new ExprId(1), "payload", structType, true, 
ImmutableList.of());
+        ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, new 
Scope(ImmutableList.of()),
+                null, true, true);
+
+        Expression analyzed = analyzer.analyze(new 
UnboundFunction("element_at",
+                ImmutableList.of(payload, new StringLiteral("Σ"))));
+
+        Assertions.assertInstanceOf(ElementAt.class, analyzed);
+        Assertions.assertEquals("σ", ((StringLikeLiteral) 
analyzed.child(1)).getStringValue());
+    }
+
+    @Test
+    public void testStructDereferenceCanonicalizesUnicodeSelector() {
+        StructType structType = new StructType(ImmutableList.of(
+                new StructField("Σ", "Σ", IntegerType.INSTANCE, true, "", 
false)));
+        SlotReference payload = new SlotReference(
+                new ExprId(1), "payload", structType, true, 
ImmutableList.of());
+        ConnectContext connectContext = new ConnectContext();
+        connectContext.setThreadLocalInfo();
+        try {
+            CascadesContext cascadesContext = 
CascadesContext.initTempContext();
+            ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, new 
Scope(ImmutableList.of(payload)),
+                    cascadesContext, true, true);
+
+            Expression analyzed = analyzer.analyze(new UnboundSlot("payload", 
"Σ"));
+
+            Assertions.assertInstanceOf(Alias.class, analyzed);
+            Assertions.assertInstanceOf(ElementAt.class, analyzed.child(0));
+            Assertions.assertEquals("σ", ((StringLikeLiteral) 
analyzed.child(0).child(1)).getStringValue());
+        } finally {
+            ConnectContext.remove();
+        }
+    }
+
+    @Test
+    public void testComputedStructDereferenceCanonicalizesUnicodeSelector() {
+        StructType structType = new StructType(ImmutableList.of(
+                new StructField("Σ", "Σ", IntegerType.INSTANCE, true, "", 
false)));
+        SlotReference payload = new SlotReference(
+                new ExprId(1), "payload", structType, true, 
ImmutableList.of());
+        ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, new 
Scope(ImmutableList.of()),
+                null, true, true);
+
+        Expression analyzed = analyzer.analyze(new DereferenceExpression(
+                new Cast(payload, structType), new StringLiteral("Σ")));
+
+        Assertions.assertInstanceOf(ElementAt.class, analyzed);
+        Assertions.assertEquals("σ", ((StringLikeLiteral) 
analyzed.child(1)).getStringValue());
+    }
+
+    @Test
+    public void testLegacyStructFieldCollisionRejectsAmbiguousSelector() {
+        org.apache.doris.catalog.StructType catalogType = new 
org.apache.doris.catalog.StructType(
+                new org.apache.doris.catalog.StructField(
+                        "ı", null, org.apache.doris.catalog.Type.INT, "", 
true, false),
+                new org.apache.doris.catalog.StructField(
+                        "i", null, org.apache.doris.catalog.Type.BIGINT, "", 
true, false));
+        StructType structType = (StructType) 
org.apache.doris.nereids.types.DataType.fromCatalogType(catalogType);
+        SlotReference payload = new SlotReference(
+                new ExprId(1), "payload", structType, true, 
ImmutableList.of());
+        ExpressionAnalyzer analyzer = new ExpressionAnalyzer(null, new 
Scope(ImmutableList.of()),
+                null, true, true);
+
+        Assertions.assertThrows(AnalysisException.class,
+                () -> analyzer.analyze(new ElementAt(payload, new 
StringLiteral("I"))));
+    }
 }
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/AccessPathExpressionCollectorTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/AccessPathExpressionCollectorTest.java
new file mode 100644
index 00000000000..d06e6905109
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/AccessPathExpressionCollectorTest.java
@@ -0,0 +1,102 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.rules.rewrite;
+
+import org.apache.doris.analysis.ColumnAccessPathType;
+import 
org.apache.doris.nereids.rules.rewrite.AccessPathExpressionCollector.CollectAccessPathResult;
+import 
org.apache.doris.nereids.rules.rewrite.NestedColumnPruning.DataTypeAccessTree;
+import org.apache.doris.nereids.trees.expressions.Cast;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.ElementAt;
+import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.StringType;
+import org.apache.doris.nereids.types.StructField;
+import org.apache.doris.nereids.types.StructType;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multimap;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+public class AccessPathExpressionCollectorTest {
+
+    @Test
+    public void testStructAccessPathUsesRootLocale() {
+        Locale originalLocale = Locale.getDefault();
+        try {
+            Locale.setDefault(Locale.forLanguageTag("tr-TR"));
+            StructType structType = new StructType(ImmutableList.of(
+                    new StructField("I", IntegerType.INSTANCE, true, ""),
+                    new StructField("ı", StringType.INSTANCE, true, "")));
+            SlotReference slot = new SlotReference("I", structType);
+            Multimap<Integer, CollectAccessPathResult> accessPaths = 
ArrayListMultimap.create();
+            AccessPathExpressionCollector collector =
+                    new AccessPathExpressionCollector(null, accessPaths, 
false, false);
+
+            collector.collect(new ElementAt(slot, new StringLiteral("I")));
+
+            List<CollectAccessPathResult> results = new ArrayList<>(
+                    accessPaths.get(slot.getExprId().asInt()));
+            Assertions.assertEquals(1, results.size());
+            Assertions.assertEquals(ImmutableList.of("i", "i"), 
results.get(0).getPath());
+
+            DataTypeAccessTree tree = DataTypeAccessTree.ofRoot(slot, 
ColumnAccessPathType.DATA);
+            tree.setAccessByPath(results.get(0).getPath(), 0, 
ColumnAccessPathType.DATA);
+            StructType prunedType = (StructType) 
tree.pruneDataType().orElseThrow();
+            Assertions.assertEquals(1, prunedType.getFields().size());
+            Assertions.assertEquals("i", 
prunedType.getFields().get(0).getName());
+            Assertions.assertEquals(IntegerType.INSTANCE, 
prunedType.getFields().get(0).getDataType());
+        } finally {
+            Locale.setDefault(originalLocale);
+        }
+    }
+
+    @Test
+    public void testCastStructAccessPathKeepsRootKeyIdentity() {
+        StructType originType = new StructType(ImmutableList.of(
+                new StructField("first", IntegerType.INSTANCE, true, ""),
+                new StructField("second", StringType.INSTANCE, true, "")));
+        StructType castType = new StructType(ImmutableList.of(
+                new StructField("I", IntegerType.INSTANCE, true, ""),
+                new StructField("ı", StringType.INSTANCE, true, "")));
+        SlotReference slot = new SlotReference("s", originType);
+        Multimap<Integer, CollectAccessPathResult> accessPaths = 
ArrayListMultimap.create();
+        AccessPathExpressionCollector collector =
+                new AccessPathExpressionCollector(null, accessPaths, false, 
false);
+
+        collector.collect(new ElementAt(new Cast(slot, castType), new 
StringLiteral("ı")));
+
+        List<CollectAccessPathResult> results = new ArrayList<>(
+                accessPaths.get(slot.getExprId().asInt()));
+        Assertions.assertEquals(1, results.size());
+        Assertions.assertEquals(ImmutableList.of("s", "second"), 
results.get(0).getPath());
+
+        DataTypeAccessTree tree = DataTypeAccessTree.ofRoot(slot, 
ColumnAccessPathType.DATA);
+        tree.setAccessByPath(results.get(0).getPath(), 0, 
ColumnAccessPathType.DATA);
+        StructType prunedType = (StructType) 
tree.pruneDataType().orElseThrow();
+        Assertions.assertEquals(1, prunedType.getFields().size());
+        Assertions.assertEquals("second", 
prunedType.getFields().get(0).getName());
+        Assertions.assertEquals(StringType.INSTANCE, 
prunedType.getFields().get(0).getDataType());
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/StructLiteralTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/StructLiteralTest.java
index 1982a6ae67f..835f7ba344d 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/StructLiteralTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/literal/StructLiteralTest.java
@@ -36,6 +36,8 @@ import org.junit.jupiter.api.Test;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 
+import java.util.Locale;
+
 public class StructLiteralTest {
 
     @Test
@@ -74,6 +76,24 @@ public class StructLiteralTest {
         Assertions.assertTrue(nullableType.getFields().get(0).isNullable());
     }
 
+    @Test
+    public void testNamedStructFieldIdentityUsesRootLocale() {
+        Locale originalLocale = Locale.getDefault();
+        try {
+            Locale.setDefault(Locale.forLanguageTag("tr-TR"));
+            CreateNamedStruct namedStruct = new CreateNamedStruct(
+                    new StringLiteral("I"), new IntegerLiteral(1),
+                    new StringLiteral("ı"), new IntegerLiteral(2));
+
+            
Assertions.assertDoesNotThrow(namedStruct::checkLegalityBeforeTypeCoercion);
+            StructType type = (StructType) 
namedStruct.customSignature().returnType;
+            Assertions.assertEquals("i", type.getFields().get(0).getName());
+            Assertions.assertEquals("ı", type.getFields().get(1).getName());
+        } finally {
+            Locale.setDefault(originalLocale);
+        }
+    }
+
     @Test
     public void testStructFunctionsKeepPhysicalCastNullabilityInStrictMode() {
         SlotReference requiredString = new SlotReference("metric", 
StringType.INSTANCE, false);
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/tablefunction/ExternalFileTableValuedFunctionTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/tablefunction/ExternalFileTableValuedFunctionTest.java
index 34df4964395..a76d9c39b0c 100644
--- 
a/fe/fe-core/src/test/java/org/apache/doris/tablefunction/ExternalFileTableValuedFunctionTest.java
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/tablefunction/ExternalFileTableValuedFunctionTest.java
@@ -19,10 +19,19 @@ package org.apache.doris.tablefunction;
 
 import org.apache.doris.catalog.Column;
 import org.apache.doris.catalog.PrimitiveType;
+import org.apache.doris.catalog.StructField;
+import org.apache.doris.catalog.StructType;
+import org.apache.doris.catalog.Type;
 import org.apache.doris.common.AnalysisException;
 import org.apache.doris.common.Config;
+import org.apache.doris.common.Pair;
 import org.apache.doris.common.util.FileFormatConstants;
 import org.apache.doris.common.util.FileFormatUtils;
+import org.apache.doris.proto.Types.PScalarType;
+import org.apache.doris.proto.Types.PStructField;
+import org.apache.doris.proto.Types.PTypeNode;
+import org.apache.doris.thrift.TPrimitiveType;
+import org.apache.doris.thrift.TTypeNodeType;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -30,10 +39,43 @@ import org.junit.Assert;
 import org.junit.Test;
 import org.mockito.Mockito;
 
+import java.lang.reflect.Method;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 
 public class ExternalFileTableValuedFunctionTest {
+    @Test
+    public void testFileSchemaPreservesNestedFieldSpelling() throws Exception {
+        ExternalFileTableValuedFunction tvf = Mockito.mock(
+                ExternalFileTableValuedFunction.class, 
Mockito.CALLS_REAL_METHODS);
+        PTypeNode structNode = PTypeNode.newBuilder()
+                .setType(TTypeNodeType.STRUCT.getValue())
+                
.setScalarType(PScalarType.newBuilder().setType(TPrimitiveType.STRUCT.getValue()))
+                .addStructFields(PStructField.newBuilder()
+                        .setName("CaseSensitive")
+                        .setComment("mixed-case child")
+                        .setContainsNull(true))
+                .build();
+        PTypeNode intNode = PTypeNode.newBuilder()
+                .setType(TTypeNodeType.SCALAR.getValue())
+                
.setScalarType(PScalarType.newBuilder().setType(TPrimitiveType.INT.getValue()))
+                .build();
+
+        Method getColumnType = ExternalFileTableValuedFunction.class
+                .getDeclaredMethod("getColumnType", List.class, int.class);
+        getColumnType.setAccessible(true);
+        @SuppressWarnings("unchecked")
+        Pair<Type, Integer> parsed = (Pair<Type, Integer>) 
getColumnType.invoke(
+                tvf, Arrays.asList(structNode, intNode), 0);
+
+        StructField field = ((StructType) parsed.key()).getFields().get(0);
+        Assert.assertEquals("casesensitive", field.getName());
+        Assert.assertEquals("CaseSensitive", field.getOriginalName());
+        Assert.assertEquals("mixed-case child", field.getComment());
+        Assert.assertTrue(field.getContainsNull());
+    }
+
     @Test
     public void 
testHiveParquetTimeZoneIsCanonicalizedAndRemovedFromStorageProperties()
             throws AnalysisException {
diff --git a/fe/fe-type/src/main/java/org/apache/doris/catalog/StructField.java 
b/fe/fe-type/src/main/java/org/apache/doris/catalog/StructField.java
index e9432c1efad..a4339d239d6 100644
--- a/fe/fe-type/src/main/java/org/apache/doris/catalog/StructField.java
+++ b/fe/fe-type/src/main/java/org/apache/doris/catalog/StructField.java
@@ -24,10 +24,15 @@ import org.apache.doris.thrift.TTypeNode;
 import com.google.common.base.Strings;
 import com.google.gson.annotations.SerializedName;
 
+import java.util.Locale;
+
 public class StructField {
     @SerializedName(value = "name")
     protected final String name;
 
+    @SerializedName(value = "originalName")
+    protected final String originalName;
+
     @SerializedName(value = "type")
     protected final Type type;
 
@@ -51,7 +56,24 @@ public class StructField {
 
     public StructField(String name, Type type, String comment, boolean 
containsNull,
             boolean commentSpecified) {
-        this.name = name.toLowerCase();
+        this(name, name, type, comment, containsNull, commentSpecified);
+    }
+
+    /**
+     * Creates a field with separate names for case-insensitive runtime lookup 
and external schema spelling.
+     *
+     * @param name field name normalized internally for runtime lookup
+     * @param originalName field spelling preserved for external schema 
metadata
+     * @param type field type
+     * @param comment field comment
+     * @param containsNull whether the field accepts null values
+     * @param commentSpecified whether the comment was explicitly specified
+     */
+    public StructField(String name, String originalName, Type type, String 
comment, boolean containsNull,
+            boolean commentSpecified) {
+        // Keep runtime identity locale-independent while preserving external 
schema spelling separately.
+        this.name = name.toLowerCase(Locale.ROOT);
+        this.originalName = originalName;
         this.type = type;
         this.comment = comment;
         this.containsNull = containsNull;
@@ -82,6 +104,15 @@ public class StructField {
         return name;
     }
 
+    public String getOriginalName() {
+        return originalName == null ? name : originalName;
+    }
+
+    /** Whether this field was persisted with its external schema spelling. */
+    public boolean hasOriginalName() {
+        return originalName != null;
+    }
+
     public Type getType() {
         return type;
     }
@@ -105,7 +136,7 @@ public class StructField {
         } else {
             typeSql = "...";
         }
-        StringBuilder sb = new StringBuilder(name);
+        StringBuilder sb = new StringBuilder(getOriginalName());
         if (type != null) {
             sb.append(":").append(typeSql);
         }
@@ -121,7 +152,7 @@ public class StructField {
      */
     public String prettyPrint(int lpad) {
         String leftPadding = Strings.repeat(" ", lpad);
-        StringBuilder sb = new StringBuilder(leftPadding + name);
+        StringBuilder sb = new StringBuilder(leftPadding + getOriginalName());
         if (type != null) {
             // Pass in the padding to make sure nested fields are aligned 
properly,
             // even if we then strip the top-level padding.
@@ -162,7 +193,7 @@ public class StructField {
 
     @Override
     public String toString() {
-        StringBuilder sb = new StringBuilder(name);
+        StringBuilder sb = new StringBuilder(getOriginalName());
         if (type != null) {
             sb.append(":").append(type);
         }
diff --git a/fe/fe-type/src/main/java/org/apache/doris/catalog/StructType.java 
b/fe/fe-type/src/main/java/org/apache/doris/catalog/StructType.java
index df06da74313..28e3a074382 100644
--- a/fe/fe-type/src/main/java/org/apache/doris/catalog/StructType.java
+++ b/fe/fe-type/src/main/java/org/apache/doris/catalog/StructType.java
@@ -33,6 +33,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 
 /**
@@ -51,7 +52,7 @@ public class StructType extends Type {
         this.fields = fields;
         for (int i = 0; i < this.fields.size(); ++i) {
             this.fields.get(i).setPosition(i);
-            fieldMap.put(this.fields.get(i).getName().toLowerCase(), 
this.fields.get(i));
+            
fieldMap.put(this.fields.get(i).getName().toLowerCase(Locale.ROOT), 
this.fields.get(i));
         }
     }
 
@@ -122,7 +123,7 @@ public class StructType extends Type {
     public void addField(StructField field) {
         field.setPosition(fields.size());
         fields.add(field);
-        fieldMap.put(field.getName().toLowerCase(), field);
+        fieldMap.put(field.getName().toLowerCase(Locale.ROOT), field);
     }
 
     public ArrayList<StructField> getFields() {
@@ -130,7 +131,23 @@ public class StructType extends Type {
     }
 
     public StructField getField(String fieldName) {
-        return fieldMap.get(fieldName.toLowerCase());
+        StructField field = fieldMap.get(fieldName.toLowerCase(Locale.ROOT));
+        if (field != null && (field.hasOriginalName() || 
field.getName().equals(fieldName))) {
+            return field;
+        }
+        StructField legacyMatch = null;
+        for (int i = fields.size() - 1; i >= 0; i--) {
+            StructField legacyField = fields.get(i);
+            // Old images lack originalName and may contain keys normalized 
with the FE's default locale.
+            if (!legacyField.hasOriginalName() && 
legacyField.getName().equalsIgnoreCase(fieldName)) {
+                if (legacyMatch != null) {
+                    // The old locale was not persisted, so choosing either 
folded sibling could return wrong data.
+                    return null;
+                }
+                legacyMatch = legacyField;
+            }
+        }
+        return legacyMatch != null ? legacyMatch : field;
     }
 
     @Override
diff --git a/fe/fe-type/src/main/java/org/apache/doris/catalog/Type.java 
b/fe/fe-type/src/main/java/org/apache/doris/catalog/Type.java
index 38dcd8f359c..fb203cedf9a 100644
--- a/fe/fe-type/src/main/java/org/apache/doris/catalog/Type.java
+++ b/fe/fe-type/src/main/java/org/apache/doris/catalog/Type.java
@@ -526,7 +526,7 @@ public abstract class Type {
             StructType structType = (StructType) this;
             for (int i = 0; i < structType.getFields().size(); i++) {
                 StructField field = structType.getFields().get(i);
-                StringBuilder desc = new 
StringBuilder(field.getName()).append(":")
+                StringBuilder desc = new 
StringBuilder(field.getOriginalName()).append(":")
                         .append(field.getType().hideVersionForVersionColumn(
                                 isToSql, showNestedComment, 
noBackslashEscapes));
                 // Requiredness is schema semantics and must survive 
independently of whether
diff --git 
a/regression-test/data/external_table_p0/iceberg/test_iceberg_struct_schema_evolution.out
 
b/regression-test/data/external_table_p0/iceberg/test_iceberg_struct_schema_evolution.out
index a364316df42..8cfc10b8204 100644
--- 
a/regression-test/data/external_table_p0/iceberg/test_iceberg_struct_schema_evolution.out
+++ 
b/regression-test/data/external_table_p0/iceberg/test_iceberg_struct_schema_evolution.out
@@ -81,7 +81,7 @@ a_struct      
struct<renamed:bigint,keep:bigint,drop_and_add:bigint,added:bigint>     Yes
 
 -- !case_desc --
 id     bigint  Yes     true    \N      
-a_struct       
struct<renamed:bigint,keep:bigint,drop_and_add:bigint,added:bigint>     Yes     
true    \N      
+a_struct       
struct<renamed:bigint,keep:bigint,DROP_AND_ADD:bigint,added:bigint>     Yes     
true    \N      
 
 -- !case_select_all --
 1      {"renamed":11, "keep":12, "drop_and_add":null, "added":null}
@@ -129,7 +129,7 @@ a_struct    
struct<renamed:bigint,keep:bigint,drop_and_add:bigint,added:bigint>     Yes
 
 -- !case_orc_desc --
 id     bigint  Yes     true    \N      
-a_struct       
struct<renamed:bigint,keep:bigint,drop_and_add:bigint,added:bigint>     Yes     
true    \N      
+a_struct       
struct<renamed:bigint,keep:bigint,DROP_AND_ADD:bigint,added:bigint>     Yes     
true    \N      
 
 -- !case_orc_select_all --
 1      {"renamed":11, "keep":12, "drop_and_add":null, "added":null}
diff --git 
a/regression-test/suites/external_table_p0/iceberg/test_iceberg_struct_schema_evolution.groovy
 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_struct_schema_evolution.groovy
index be03b238c92..d8a6a427d82 100644
--- 
a/regression-test/suites/external_table_p0/iceberg/test_iceberg_struct_schema_evolution.groovy
+++ 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_struct_schema_evolution.groovy
@@ -160,8 +160,7 @@ suite("test_iceberg_struct_schema_evolution", 
"p0,external") {
     qt_case_struct_renamed """SELECT element_at(a_struct, 'renamed') FROM 
${case_table_name} ORDER BY id"""
 
     // Test 3: Query struct field that was dropped and re-added with case 
change
-    // Note: Even though we use DROP_AND_ADD (uppercase) in SQL, the system 
normalizes
-    // field names to lowercase, so we query with 'drop_and_add' (lowercase)
+    // Iceberg metadata retains the external spelling, while runtime lookup 
still uses the normalized name.
     qt_case_struct_drop_and_add """SELECT element_at(a_struct, 'drop_and_add') 
FROM ${case_table_name} ORDER BY id"""
 
     // Test 4: Query struct field that was newly added
@@ -198,8 +197,7 @@ suite("test_iceberg_struct_schema_evolution", 
"p0,external") {
     qt_case_orc_struct_renamed """SELECT element_at(a_struct, 'renamed') FROM 
${case_orc_table_name} ORDER BY id"""
 
     // Test 3: Query struct field that was dropped and re-added with case 
change
-    // Note: Even though we use DROP_AND_ADD (uppercase) in SQL, the system 
normalizes
-    // field names to lowercase, so we query with 'drop_and_add' (lowercase)
+    // Iceberg metadata retains the external spelling, while runtime lookup 
still uses the normalized name.
     qt_case_orc_struct_drop_and_add """SELECT element_at(a_struct, 
'drop_and_add') FROM ${case_orc_table_name} ORDER BY id"""
 
     // Test 4: Query struct field that was newly added
diff --git 
a/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_write_ctas_format_boundary.groovy
 
b/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_write_ctas_format_boundary.groovy
index 9be6f3ba421..a6242cc9886 100644
--- 
a/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_write_ctas_format_boundary.groovy
+++ 
b/regression-test/suites/external_table_p0/iceberg/write/test_iceberg_write_ctas_format_boundary.groovy
@@ -156,7 +156,89 @@ suite("test_iceberg_write_ctas_format_boundary",
     }
     assertEquals(0, (sql """show tables like 
'ctas_failed_atomicity'""").size())
 
-    // WC01-S03: Iceberg allows Avro, but the current Doris writer supports
+    // WC01-S03: FILE TVF must keep the Parquet schema spelling until Iceberg 
CTAS persists it.
+    // The normalized names remain available for Doris runtime lookup.
+    spark_iceberg_multi """
+        DROP TABLE IF EXISTS demo.${dbName}.file_tvf_case_source;
+        CREATE TABLE demo.${dbName}.file_tvf_case_source (
+            id INT,
+            payload STRUCT<CaseSensitive:BIGINT,
+                           NestedArray:ARRAY<STRUCT<ArrayChild:BIGINT>>,
+                           NestedMap:MAP<STRING,STRUCT<MapChild:BIGINT>>>
+        ) USING iceberg
+        TBLPROPERTIES ('write.format.default' = 'parquet');
+        INSERT INTO demo.${dbName}.file_tvf_case_source VALUES (
+            1,
+            NAMED_STRUCT(
+                'CaseSensitive', CAST(7 AS BIGINT),
+                'NestedArray', ARRAY(NAMED_STRUCT('ArrayChild', CAST(8 AS 
BIGINT))),
+                'NestedMap', MAP('k', NAMED_STRUCT('MapChild', CAST(9 AS 
BIGINT)))
+            )
+        );
+    """
+    sql """refresh catalog ${catalogName}"""
+    String sourceFile = (sql """
+        select file_path from file_tvf_case_source\$files order by file_path 
limit 1
+    """)[0][0].toString()
+
+    sql """drop table if exists ctas_file_tvf_case"""
+    sql """
+        create table ctas_file_tvf_case as
+        select payload from file (
+            "uri" = "${sourceFile}",
+            "format" = "parquet",
+            "s3.endpoint" = "http://${externalEnvIp}:${minioPort}";,
+            "s3.region" = "us-east-1",
+            "s3.access_key" = "admin",
+            "s3.secret_key" = "password",
+            "use_path_style" = "true"
+        )
+    """
+
+    def ctasSchema = spark_iceberg """describe 
demo.${dbName}.ctas_file_tvf_case"""
+    def payloadRow = ctasSchema.find { row -> row[0].toString() == "payload" }
+    assertNotNull(payloadRow, "payload column should exist in the Iceberg CTAS 
schema")
+    String payloadType = payloadRow[1].toString()
+    assertTrue(payloadType.contains("CaseSensitive"), payloadType)
+    assertTrue(payloadType.contains("NestedArray"), payloadType)
+    assertTrue(payloadType.contains("ArrayChild"), payloadType)
+    assertTrue(payloadType.contains("NestedMap"), payloadType)
+    assertTrue(payloadType.contains("MapChild"), payloadType)
+
+    def nestedValues = sql """
+        select element_at(payload, 'casesensitive'),
+               element_at(element_at(payload, 'nestedarray')[1], 'arraychild'),
+               element_at(element_at(payload, 'nestedmap')['k'], 'mapchild')
+        from ctas_file_tvf_case
+    """
+    assertEquals([[7L, 8L, 9L]], nestedValues)
+
+    // WC01-S04: Names displayed from external metadata must remain executable 
even when Java ROOT lowercasing
+    // changes their Unicode bytes or UTF-8 length before thrift reaches the 
BE.
+    spark_iceberg_multi """
+        DROP TABLE IF EXISTS demo.${dbName}.unicode_struct_fields;
+        CREATE TABLE demo.${dbName}.unicode_struct_fields (
+            id INT,
+            payload STRUCT<`Σ`:BIGINT, `ẞ`:BIGINT>
+        ) USING iceberg;
+        INSERT INTO demo.${dbName}.unicode_struct_fields VALUES (
+            1, NAMED_STRUCT('Σ', CAST(10 AS BIGINT), 'ẞ', CAST(11 AS BIGINT))
+        );
+    """
+    sql """refresh catalog ${catalogName}"""
+    def unicodeSchema = sql """describe unicode_struct_fields"""
+    def unicodePayloadRow = unicodeSchema.find { row -> row[0].toString() == 
"payload" }
+    assertNotNull(unicodePayloadRow, "payload column should exist in the 
Unicode Iceberg schema")
+    String unicodePayloadType = unicodePayloadRow[1].toString()
+    assertTrue(unicodePayloadType.contains("Σ"), unicodePayloadType)
+    assertTrue(unicodePayloadType.contains("ẞ"), unicodePayloadType)
+    def unicodeValues = sql """
+        select element_at(payload, 'Σ'), element_at(payload, 'ẞ')
+        from unicode_struct_fields
+    """
+    assertEquals([[10L, 11L]], unicodeValues)
+
+    // WC01-S05: Iceberg allows Avro, but the current Doris writer supports
     // Parquet and ORC only. Reject Avro explicitly instead of silently 
falling back.
     sql """drop table if exists avro_write_boundary"""
     sql """


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to