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 b73b9e41f0a [fix](iceberg) Reject invalid name mapping instead of 
reading NULL (#68004)
b73b9e41f0a is described below

commit b73b9e41f0a655baa2c4d01adfae8f97cac28c41
Author: daidai <[email protected]>
AuthorDate: Thu Sep 17 09:36:37 2026 +0800

    [fix](iceberg) Reject invalid name mapping instead of reading NULL (#68004)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Problem Summary:
    
    When the Iceberg table property `schema.name-mapping.default` is present
    but malformed, `IcebergSchemaUtils.extractNameMapping` caught the parse
    failure and rebuilt a "current schema" name mapping, which is
    authoritative. Data files that do not carry Iceberg field ids could then
    only be resolved by their current column names. After a column rename
    the old physical column is unmatched, V2 semantics materializes NULL for
    it, and the query succeeds with historical values lost. If the old name
    was later reused by another column, the fallback could even bind the
    wrong physical column.
    
    Iceberg itself does not degrade like this: Spark's `BaseReader` parses
    `schema.name-mapping.default` while constructing the reader and fails
    the query for a malformed value (`IllegalArgumentException` /
    `UncheckedIOException`), regardless of whether the data files carry
    field ids.
    
    This PR removes the current-schema fallback and reports the metadata
    fault instead. The connector now throws a `DorisConnectorException` that
    names the table, the property, the root cause and the remediation, so
    the failure is visible instead of silently returning NULL. Metadata
    (system) table scans are unaffected, because the schema-evolution
    carrier is only built for base-table scans.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test
        - [ ] Regression test
        - [x] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason
    
    - Behavior changed:
        - [x] Yes.
    
    - Does this need documentation?
        - [ ] No.
        - [ ] Yes.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 .../connector/iceberg/IcebergSchemaUtils.java      | 28 ++++++++++++-------
 .../connector/iceberg/IcebergSchemaUtilsTest.java  | 31 +++++++++++++++-------
 2 files changed, 41 insertions(+), 18 deletions(-)

diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergSchemaUtils.java
 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergSchemaUtils.java
index 364d2d7e443..e156b63181b 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergSchemaUtils.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergSchemaUtils.java
@@ -17,6 +17,7 @@
 
 package org.apache.doris.connector.iceberg;
 
+import org.apache.doris.connector.spi.DorisConnectorException;
 import org.apache.doris.thrift.TColumnType;
 import org.apache.doris.thrift.TFileScanRangeParams;
 import org.apache.doris.thrift.TPrimitiveType;
@@ -28,13 +29,13 @@ import org.apache.doris.thrift.schema.external.TNestedField;
 import org.apache.doris.thrift.schema.external.TSchema;
 import org.apache.doris.thrift.schema.external.TStructField;
 
+import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.apache.iceberg.Schema;
 import org.apache.iceberg.SingleValueParser;
 import org.apache.iceberg.Table;
 import org.apache.iceberg.TableProperties;
 import org.apache.iceberg.mapping.MappedField;
 import org.apache.iceberg.mapping.MappedFields;
-import org.apache.iceberg.mapping.MappingUtil;
 import org.apache.iceberg.mapping.NameMapping;
 import org.apache.iceberg.mapping.NameMappingParser;
 import org.apache.iceberg.transforms.Transforms;
@@ -214,8 +215,15 @@ public final class IcebergSchemaUtils {
      * name-mapping property, and a present (possibly empty) map when it does 
— the distinction #65784 relies on
      * to make a table-level mapping AUTHORITATIVE (an unmapped field then 
materializes its default/NULL instead
      * of silently matching a physical column by its current name; see {@link 
#buildField}). Port of legacy
-     * {@code IcebergScanNode.extractNameMapping} + {@code 
IcebergUtils.getNameMapping} (#65784). A malformed
-     * property fails soft to a current-name mapping instead of becoming 
indistinguishable from no property.
+     * {@code IcebergScanNode.extractNameMapping} + {@code 
IcebergUtils.getNameMapping} (#65784).
+     *
+     * <p>A property that is present but cannot be parsed is a metadata fault, 
not an absent mapping: Iceberg
+     * readers refuse such tables (Spark parses {@code 
schema.name-mapping.default} while constructing the file
+     * reader), so fail instead of degrading to current-schema aliases. Those 
aliases cannot resolve the old
+     * physical columns of ID-less files after a rename — the scan would 
silently return NULL, or bind the wrong
+     * physical column once a name has been reused.
+     *
+     * @throws DorisConnectorException if the property is present but cannot 
be parsed as a name mapping
      */
     static Optional<Map<Integer, List<String>>> extractNameMapping(Table 
table) {
         String nameMappingJson = 
table.properties().get(TableProperties.DEFAULT_NAME_MAPPING);
@@ -231,12 +239,14 @@ public final class IcebergSchemaUtils {
             collectNameMappings(mapping.asMappedFields(), result);
             return Optional.of(result);
         } catch (Exception e) {
-            // Preserve legacy current-name readability for ID-less files when 
a malformed table property
-            // cannot provide authoritative aliases; Optional.empty() now 
means the property is truly absent.
-            LOG.warn("Failed to parse name mapping from Iceberg table 
properties", e);
-            Map<Integer, List<String>> fallback = new HashMap<>();
-            
collectNameMappings(MappingUtil.create(table.schema()).asMappedFields(), 
fallback);
-            return Optional.of(fallback);
+            LOG.warn("Failed to parse name mapping of table {}", table.name(), 
e);
+            throw new DorisConnectorException(String.format(
+                    "Invalid table property '%s' of Iceberg table %s: %s. "
+                            + "The value must be an Iceberg name mapping JSON 
array; please fix or drop "
+                            + "the property (for example with ALTER TABLE ... 
UNSET TBLPROPERTIES in "
+                            + "Spark) and refresh the table.",
+                    TableProperties.DEFAULT_NAME_MAPPING, table.name(),
+                    ExceptionUtils.getRootCauseMessage(e)), e);
         }
     }
 
diff --git 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergSchemaUtilsTest.java
 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergSchemaUtilsTest.java
index aaa597580e1..bc184d50d90 100644
--- 
a/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergSchemaUtilsTest.java
+++ 
b/fe/fe-connector/fe-connector-iceberg/src/test/java/org/apache/doris/connector/iceberg/IcebergSchemaUtilsTest.java
@@ -17,6 +17,7 @@
 
 package org.apache.doris.connector.iceberg;
 
+import org.apache.doris.connector.spi.DorisConnectorException;
 import org.apache.doris.thrift.TFileScanRangeParams;
 import org.apache.doris.thrift.TPrimitiveType;
 import org.apache.doris.thrift.schema.external.TField;
@@ -715,21 +716,33 @@ public class IcebergSchemaUtilsTest {
     }
 
     @Test
-    public void malformedNameMappingKeepsIdlessCurrentNameFallback() {
-        // A malformed name-mapping property must not break the scan or look 
identical to a genuinely absent
-        // mapping. Required and optional fields both need current-name 
aliases for ID-less legacy files.
+    public void malformedNameMappingFailsInsteadOfFallingBackToCurrentNames() {
+        // Iceberg refuses to read a table whose name mapping cannot be parsed 
(Spark's BaseReader parses the
+        // property while constructing the file reader), so the connector must 
surface the metadata fault.
+        // Rewriting the property into current-schema aliases would silently 
return NULL for the renamed
+        // columns of ID-less files instead of reporting it.
         Table table = createTable("t1", SCHEMA,
                 Collections.singletonMap(TableProperties.DEFAULT_NAME_MAPPING, 
"{not valid json"));
 
-        Map<Integer, List<String>> fallback = 
IcebergSchemaUtils.extractNameMapping(table).orElseThrow();
-        Assertions.assertEquals(Collections.singletonList("id"), 
fallback.get(1));
-        Assertions.assertEquals(Collections.singletonList("name"), 
fallback.get(2));
+        DorisConnectorException exception = 
Assertions.assertThrows(DorisConnectorException.class,
+                () -> IcebergSchemaUtils.extractNameMapping(table));
+        
Assertions.assertTrue(exception.getMessage().contains(TableProperties.DEFAULT_NAME_MAPPING));
+        Assertions.assertTrue(exception.getMessage().contains("t1"));
+    }
+
+    @Test
+    public void validEmptyNameMappingStaysAuthoritative() {
+        // An explicitly empty mapping is NOT the same as an absent property: 
it stays authoritative, so an
+        // ID-less file's columns resolve to their defaults/NULLs instead of 
matching by current name.
+        Table table = createTable("t1", SCHEMA,
+                Collections.singletonMap(TableProperties.DEFAULT_NAME_MAPPING, 
"[]"));
+
+        Map<Integer, List<String>> mapping = 
IcebergSchemaUtils.extractNameMapping(table).orElseThrow();
+        Assertions.assertTrue(mapping.isEmpty());
 
         Map<String, TField> fields = topFields(dict(table, "id", "name"));
         Assertions.assertTrue(fields.get("id").isNameMappingIsAuthoritative());
-        Assertions.assertEquals(Collections.singletonList("id"), 
fields.get("id").getNameMapping());
-        
Assertions.assertTrue(fields.get("name").isNameMappingIsAuthoritative());
-        Assertions.assertEquals(Collections.singletonList("name"), 
fields.get("name").getNameMapping());
+        Assertions.assertTrue(fields.get("id").getNameMapping().isEmpty());
     }
 
     // --- round-trip through the prop transport (what the generic node does) 
---


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

Reply via email to