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

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


The following commit(s) were added to refs/heads/master by this push:
     new 616f9ae86765 fix(common): dedupe repeated Avro fixed names (#20324)
616f9ae86765 is described below

commit 616f9ae867654d70b58674600887e4ff1205ccf2
Author: voonhous <[email protected]>
AuthorDate: Thu Oct 15 16:04:59 2026 +0800

    fix(common): dedupe repeated Avro fixed names (#20324)
    
    * fix(common): dedupe repeated Avro fixed names
    
    AvroSchemaConverterWithTimestampNTZ is Hudi's copy of parquet-java
    1.13.1's AvroSchemaConverter. In the Parquet-to-Avro direction it
    names every fixed (including DECIMAL on fixed_len_byte_array) after
    its field with no namespace, while repeated record names already get
    a dedup namespace. Shredded variants name every leaf typed_value, so a
    DECIMAL leaf collides with its enclosing typed_value record and the
    read schema no longer survives the toString/parse round trip in
    HoodieAvroParquetReader, failing COW upserts with
    "Undefined schema: k102.typed_value".
    
    Port PARQUET-2456 (apache/parquet-java#1304, released in 1.14.0):
    records and fixed types share one name counter, and repeated names get
    a <name><count> namespace. Hudi builds DECIMAL fixed types in its
    logical-type branch rather than in convertFIXED_LEN_BYTE_ARRAY, so
    that branch gets the same treatment.
    
    Also restore the INT96 doc string, which the HoodieSchema migration
    passed in the namespace slot (HoodieSchema.createFixed takes the
    namespace second, Avro's Schema.createFixed takes the doc second).
    
    Fixes #20307
    
    * refactor(common): rename dedup namespace helper
    
    Rename namespace() to nextDedupNamespace(): it bumps the shared name
    counter as a side effect, which the old name did not say. Addresses
    review feedback on #20324.
---
 .../avro/AvroSchemaConverterWithTimestampNTZ.java  | 25 +++++---
 .../parquet/avro/TestAvroSchemaConverter.java      | 67 +++++++++++++++++++++-
 2 files changed, 84 insertions(+), 8 deletions(-)

diff --git 
a/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/AvroSchemaConverterWithTimestampNTZ.java
 
b/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/AvroSchemaConverterWithTimestampNTZ.java
index af7238b17de9..66c1c361fd5b 100644
--- 
a/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/AvroSchemaConverterWithTimestampNTZ.java
+++ 
b/hudi-hadoop-common/src/main/java/org/apache/parquet/avro/AvroSchemaConverterWithTimestampNTZ.java
@@ -354,8 +354,8 @@ public class AvroSchemaConverterWithTimestampNTZ extends 
HoodieAvroParquetSchema
   }
 
   private HoodieSchema convertFields(String name, List<Type> parquetFields, 
Map<String, Integer> names) {
+    String ns = nextDedupNamespace(name, names);
     List<HoodieSchemaField> fields = new ArrayList<>(parquetFields.size());
-    Integer nameCount = names.merge(name, 1, (oldValue, value) -> oldValue + 
1);
     for (Type parquetType : parquetFields) {
       HoodieSchema fieldSchema = convertField(parquetType, names);
       if (parquetType.isRepetition(REPEATED)) {
@@ -368,10 +368,16 @@ public class AvroSchemaConverterWithTimestampNTZ extends 
HoodieAvroParquetSchema
             parquetType.getName(), fieldSchema, null, (Object) null));
       }
     }
-    HoodieSchema schema = HoodieSchema.createRecord(name, null, nameCount > 1 
? name + nameCount : null, false, fields);
+    HoodieSchema schema = HoodieSchema.createRecord(name, null, ns, false, 
fields);
     return schema;
   }
 
+  // PARQUET-2456: repeated record and fixed names get a dedup namespace so 
the schema survives toString and parse
+  private static String nextDedupNamespace(String name, Map<String, Integer> 
names) {
+    Integer nameCount = names.merge(name, 1, (oldValue, value) -> oldValue + 
1);
+    return nameCount > 1 ? name + nameCount : null;
+  }
+
   private HoodieSchema convertField(final Type parquetType, Map<String, 
Integer> names) {
     if (parquetType.isPrimitive()) {
       final PrimitiveType asPrimitive = parquetType.asPrimitiveType();
@@ -381,7 +387,7 @@ public class AvroSchemaConverterWithTimestampNTZ extends 
HoodieAvroParquetSchema
 
       // Handle logical type annotations directly with HoodieSchema creation 
methods
       if (annotation != null) {
-        HoodieSchema logicalSchema = 
convertLogicalTypeAnnotationToHoodieSchema(annotation, parquetType);
+        HoodieSchema logicalSchema = 
convertLogicalTypeAnnotationToHoodieSchema(annotation, parquetType, names);
         if (logicalSchema != null) {
           return logicalSchema;
         }
@@ -405,7 +411,7 @@ public class AvroSchemaConverterWithTimestampNTZ extends 
HoodieAvroParquetSchema
             @Override
             public HoodieSchema convertINT96(PrimitiveTypeName 
primitiveTypeName) {
               if (readInt96AsFixed) {
-                return HoodieSchema.createFixed("INT96", "INT96 represented as 
byte[12]", null, 12);
+                return HoodieSchema.createFixed("INT96", null, "INT96 
represented as byte[12]", 12);
               }
               throw new IllegalArgumentException(
                   "INT96 is deprecated. As interim enable READ_INT96_AS_FIXED 
flag to read as byte array.");
@@ -424,7 +430,9 @@ public class AvroSchemaConverterWithTimestampNTZ extends 
HoodieAvroParquetSchema
                 return HoodieSchema.createUUID();
               } else {
                 int size = parquetType.asPrimitiveType().getTypeLength();
-                return HoodieSchema.createFixed(parquetType.getName(), null, 
null, size);
+                String name = parquetType.getName();
+                String ns = nextDedupNamespace(name, names);
+                return HoodieSchema.createFixed(name, ns, null, size);
               }
             }
             @Override
@@ -528,10 +536,12 @@ public class AvroSchemaConverterWithTimestampNTZ extends 
HoodieAvroParquetSchema
    *
    * @param annotation Parquet logical type annotation
    * @param parquetType The parquet type containing the annotation
+   * @param names counts of record and fixed names seen so far, used to 
namespace repeated names
    * @return HoodieSchema with the logical type applied, or null if no logical 
type conversion is needed
    * @throws IllegalArgumentException if the logical type is not compatible 
with the primitive type
    */
-  private HoodieSchema 
convertLogicalTypeAnnotationToHoodieSchema(LogicalTypeAnnotation annotation, 
Type parquetType) {
+  private HoodieSchema 
convertLogicalTypeAnnotationToHoodieSchema(LogicalTypeAnnotation annotation, 
Type parquetType,
+                                                                  Map<String, 
Integer> names) {
     if (annotation == null) {
       return null;
     }
@@ -545,7 +555,8 @@ public class AvroSchemaConverterWithTimestampNTZ extends 
HoodieAvroParquetSchema
         if (primitiveType == FIXED_LEN_BYTE_ARRAY) {
           int fixedSize = parquetType.asPrimitiveType().getTypeLength();
           String name = parquetType.getName();
-          return java.util.Optional.of(HoodieSchema.createDecimal(name, null, 
null,
+          String ns = nextDedupNamespace(name, names);
+          return java.util.Optional.of(HoodieSchema.createDecimal(name, ns, 
null,
               decimalLogicalType.getPrecision(), 
decimalLogicalType.getScale(), fixedSize));
         } else if (primitiveType == BINARY) {
           return 
java.util.Optional.of(HoodieSchema.createDecimal(decimalLogicalType.getPrecision(),
 decimalLogicalType.getScale()));
diff --git 
a/hudi-hadoop-common/src/test/java/org/apache/parquet/avro/TestAvroSchemaConverter.java
 
b/hudi-hadoop-common/src/test/java/org/apache/parquet/avro/TestAvroSchemaConverter.java
index 1c468f5320c7..5167923ecb83 100644
--- 
a/hudi-hadoop-common/src/test/java/org/apache/parquet/avro/TestAvroSchemaConverter.java
+++ 
b/hudi-hadoop-common/src/test/java/org/apache/parquet/avro/TestAvroSchemaConverter.java
@@ -24,6 +24,7 @@ import 
org.apache.hudi.common.schema.HoodieSchemaCompatibility;
 import org.apache.hudi.common.schema.HoodieSchemaField;
 import org.apache.hudi.common.schema.HoodieSchemaType;
 
+import org.apache.avro.Schema;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.parquet.schema.LogicalTypeAnnotation;
 import org.apache.parquet.schema.MessageType;
@@ -549,7 +550,7 @@ public class TestAvroSchemaConverter {
     Configuration enableInt96ReadingConfig = new Configuration();
     enableInt96ReadingConfig.setBoolean(AvroReadSupport.READ_INT96_AS_FIXED, 
true);
 
-    HoodieSchema int96schema = HoodieSchema.createFixed("INT96", "INT96 
represented as byte[12]", null, 12);
+    HoodieSchema int96schema = HoodieSchema.createFixed("INT96", null, "INT96 
represented as byte[12]", 12);
     HoodieSchema schema = HoodieSchema.createRecord("myrecord", null, null, 
false,
         Collections.singletonList(HoodieSchemaField.of("int96_field", 
int96schema, null, null)));
 
@@ -846,6 +847,70 @@ public class TestAvroSchemaConverter {
     testParquetToAvroConversion(NEW_BEHAVIOR, schema, parquetSchema);
   }
 
+  @Test
+  public void testReuseNamesArrays() throws Exception {
+    HoodieSchema a1 = HoodieSchema.createRecord("array", null, null, false,
+        Collections.singletonList(HoodieSchemaField.of("a4", 
HoodieSchema.create(HoodieSchemaType.FLOAT))));
+    HoodieSchema a2 = HoodieSchema.createFixed("array", "array2", null, 1);
+    HoodieSchema a3 = HoodieSchema.createFixed("array", "array3", null, 1);
+    HoodieSchema schema = HoodieSchema.createRecord("Message", null, null, 
false,
+        Arrays.asList(
+            HoodieSchemaField.of("a1", HoodieSchema.createArray(a1)),
+            HoodieSchemaField.of("a2", HoodieSchema.createArray(a2)),
+            HoodieSchemaField.of("a3", HoodieSchema.createArray(a3))));
+
+    String parquetSchema = "message Message {\n"
+        + "  required group a1 (LIST) {\n"
+        + "    repeated group array {\n"
+        + "      required float a4;\n"
+        + "    }\n"
+        + "  }\n"
+        + "  required group a2 (LIST) {\n"
+        + "    repeated fixed_len_byte_array(1) array;\n"
+        + "  }\n"
+        + "  required group a3 (LIST) {\n"
+        + "    repeated fixed_len_byte_array(1) array;\n"
+        + "  }\n"
+        + "}\n";
+
+    testParquetToAvroConversion(schema, parquetSchema);
+    testParquetToAvroConversion(NEW_BEHAVIOR, schema, parquetSchema);
+  }
+
+  @Test
+  public void testReuseNamesOnFixedSurvivesStringRoundTrip() {
+    // Shredded-variant shape (GH-20307): a DECIMAL fixed typed_value nested 
under a typed_value record,
+    // plus two same-named DECIMAL fixed leaves of different sizes.
+    HoodieSchema schema = getAvroSchemaConverter(new 
Configuration()).convert(MessageTypeParser.parseMessageType(
+        "message m {\n"
+            + "  optional group payload {\n"
+            + "    required binary metadata;\n"
+            + "    optional binary value;\n"
+            + "    optional group typed_value {\n"
+            + "      required group k10 {\n"
+            + "        optional binary value;\n"
+            + "        optional fixed_len_byte_array(8) typed_value 
(DECIMAL(18,12));\n"
+            + "      }\n"
+            + "    }\n"
+            + "  }\n"
+            + "  optional group price { optional fixed_len_byte_array(5) 
amount (DECIMAL(10,2)); }\n"
+            + "  optional group tax { optional fixed_len_byte_array(8) amount 
(DECIMAL(18,4)); }\n"
+            + "}\n"));
+
+    Schema avroSchema = schema.toAvroSchema();
+    assertEquals(avroSchema, new Schema.Parser().parse(avroSchema.toString()));
+
+    HoodieSchema decimalLeaf = nonNullField(nonNullField(nonNullField(schema, 
"payload"), "typed_value"), "k10")
+        .getField("typed_value").get().schema().getNonNullType();
+    assertEquals("typed_value2.typed_value", decimalLeaf.getFullName());
+    assertEquals("amount", nonNullField(nonNullField(schema, "price"), 
"amount").getFullName());
+    assertEquals("amount2.amount", nonNullField(nonNullField(schema, "tax"), 
"amount").getFullName());
+  }
+
+  private static HoodieSchema nonNullField(HoodieSchema record, String 
fieldName) {
+    return record.getField(fieldName).get().schema().getNonNullType();
+  }
+
   @Test
   public void testUUIDType() throws Exception {
     HoodieSchema fromSchema = HoodieSchema.createRecord(

Reply via email to