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

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


The following commit(s) were added to refs/heads/master by this push:
     new 90bb27c6ff [avro] Fix ClassCastException when writing MULTISET with 
non-string element type (#9066)
90bb27c6ff is described below

commit 90bb27c6ff5b2fb6b21cd5f49572f07a97fb4315
Author: Eunbin Son <[email protected]>
AuthorDate: Thu Aug 6 23:15:49 2026 +0900

    [avro] Fix ClassCastException when writing MULTISET with non-string element 
type (#9066)
---
 docs/docs/concepts/spec/fileformat.md              | 16 +++---
 .../paimon/format/avro/AvroSchemaVisitor.java      | 13 ++++-
 .../format/avro/AvroFormatReadWriteTest.java       | 67 ++++++++++++++++++++++
 3 files changed, 85 insertions(+), 11 deletions(-)

diff --git a/docs/docs/concepts/spec/fileformat.md 
b/docs/docs/concepts/spec/fileformat.md
index 541498df57..2d199e6f15 100644
--- a/docs/docs/concepts/spec/fileformat.md
+++ b/docs/docs/concepts/spec/fileformat.md
@@ -234,16 +234,16 @@ The following table lists the type mapping from Paimon 
type to Avro type.
       <td></td>
     </tr>
     <tr>
-      <td><code>MAP</code><br>
-      (key must be string/char/varchar type)</td>
-      <td><code>map</code></td>
-      <td></td>
+      <td><code>MAP</code></td>
+      <td>string/char/varchar key: <code>map</code><br>
+      other key: <code>array</code> of key-value <code>record</code></td>
+      <td>other key: <code>map</code></td>
     </tr>
     <tr>
-      <td><code>MULTISET</code><br>
-      (element must be string/char/varchar type)</td>
-      <td><code>map</code></td>
-      <td></td>
+      <td><code>MULTISET</code></td>
+      <td>string/char/varchar element: <code>map</code><br>
+      other element: <code>array</code> of element-count 
<code>record</code></td>
+      <td>other element: <code>map</code></td>
     </tr>
     <tr>
       <td><code>ROW</code></td>
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroSchemaVisitor.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroSchemaVisitor.java
index aa329c6132..3ffe8bfeae 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroSchemaVisitor.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroSchemaVisitor.java
@@ -23,6 +23,7 @@ import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.MapType;
+import org.apache.paimon.types.MultisetType;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.types.VectorType;
 
@@ -51,9 +52,15 @@ public interface AvroSchemaVisitor<T> {
                 return visitUnion(schema, type);
 
             case ARRAY:
-                if (type instanceof MapType) {
-                    MapType mapType = (MapType) type;
-                    return visitArrayMap(schema, mapType.getKeyType(), 
mapType.getValueType());
+                if (type instanceof MapType || type instanceof MultisetType) {
+                    // A multiset is encoded as a map from element to its 
multiplicity, so it uses
+                    // the same array-of-record encoding as a map with a 
non-string key. Reuse the
+                    // converter's key/value extraction so that schema 
creation and schema visiting
+                    // always agree on that encoding.
+                    return visitArrayMap(
+                            schema,
+                            AvroSchemaConverter.extractKeyTypeToAvroMap(type),
+                            
AvroSchemaConverter.extractValueTypeToAvroMap(type));
                 } else if (type instanceof VectorType) {
                     return visitArrayVector(schema, ((VectorType) 
type).getElementType());
                 } else {
diff --git 
a/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroFormatReadWriteTest.java
 
b/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroFormatReadWriteTest.java
index 4a7d724635..43228b9487 100644
--- 
a/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroFormatReadWriteTest.java
+++ 
b/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroFormatReadWriteTest.java
@@ -18,20 +18,34 @@
 
 package org.apache.paimon.format.avro;
 
+import org.apache.paimon.data.BinaryString;
 import org.apache.paimon.data.BinaryVector;
+import org.apache.paimon.data.GenericMap;
 import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalArray;
+import org.apache.paimon.data.InternalMap;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.serializer.InternalRowSerializer;
 import org.apache.paimon.format.FileFormat;
 import org.apache.paimon.format.FileFormatFactory;
 import org.apache.paimon.format.FormatReadWriteTest;
+import org.apache.paimon.format.FormatReaderContext;
 import org.apache.paimon.options.Options;
+import org.apache.paimon.reader.RecordReader;
 import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataType;
 import org.apache.paimon.types.DataTypes;
 import org.apache.paimon.types.RowType;
 
 import org.junit.jupiter.api.Test;
 
+import java.io.IOException;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
 
 /** An avro {@link FormatReadWriteTest}. */
 public class AvroFormatReadWriteTest extends FormatReadWriteTest {
@@ -50,6 +64,59 @@ public class AvroFormatReadWriteTest extends 
FormatReadWriteTest {
         testArrayBlobDescriptorRoundTrip();
     }
 
+    @Test
+    public void testMultisetWithNonStringElement() throws IOException {
+        // a non-string element makes the multiset use the array-of-record 
encoding
+        testMultisetRoundTrip(DataTypes.INT().notNull(), 10, 20);
+    }
+
+    @Test
+    public void testMultisetWithStringElement() throws IOException {
+        // a string element makes the multiset use the native avro map encoding
+        testMultisetRoundTrip(
+                DataTypes.STRING().notNull(),
+                BinaryString.fromString("a"),
+                BinaryString.fromString("b"));
+    }
+
+    private void testMultisetRoundTrip(DataType elementType, Object first, 
Object second)
+            throws IOException {
+        RowType rowType =
+                RowType.builder()
+                        .field("id", DataTypes.INT().notNull())
+                        .field("ms", DataTypes.MULTISET(elementType))
+                        .build();
+        Map<Object, Object> multiset = new HashMap<>();
+        multiset.put(first, 1);
+        multiset.put(second, 2);
+        GenericRow expected = GenericRow.of(1, new GenericMap(multiset));
+
+        FileFormat format = fileFormat();
+        write(format.createWriterFactory(rowType), file, expected);
+
+        List<InternalRow> result = new ArrayList<>();
+        try (RecordReader<InternalRow> reader =
+                format.createReaderFactory(rowType, rowType, new ArrayList<>())
+                        .createReader(
+                                new FormatReaderContext(fileIO, file, 
fileIO.getFileSize(file)))) {
+            InternalRowSerializer serializer = new 
InternalRowSerializer(rowType);
+            reader.forEachRemaining(row -> result.add(serializer.copy(row)));
+        }
+
+        assertThat(result).hasSize(1);
+        assertThat(result.get(0).getInt(0)).isEqualTo(1);
+        InternalMap actual = result.get(0).getMap(1);
+        assertThat(actual.size()).isEqualTo(multiset.size());
+        InternalArray.ElementGetter keyGetter = 
InternalArray.createElementGetter(elementType);
+        InternalArray keys = actual.keyArray();
+        InternalArray values = actual.valueArray();
+        Map<Object, Object> actualMultiset = new HashMap<>();
+        for (int i = 0; i < actual.size(); i++) {
+            actualMultiset.put(keyGetter.getElementOrNull(keys, i), 
values.getInt(i));
+        }
+        assertThat(actualMultiset).isEqualTo(multiset);
+    }
+
     @Override
     protected RowType rowTypeForFullTypesTest() {
         RowType rowWithoutVector = super.rowTypeForFullTypesTest();

Reply via email to