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 6509987c60 [avro] Optimize array in avro (#8567)
6509987c60 is described below

commit 6509987c60321892d8c8acb00e9b5281fd61d78d
Author: yuzelin <[email protected]>
AuthorDate: Mon Jul 13 10:18:31 2026 +0800

    [avro] Optimize array in avro (#8567)
---
 .../benchmark/compact/ArrayCompactBenchmark.java   | 142 ++++++++
 .../apache/paimon/format/avro/AvroBytesArray.java  | 380 +++++++++++++++++++++
 .../paimon/format/avro/FieldReaderFactory.java     |  17 +-
 .../paimon/format/avro/FieldWriterFactory.java     |  36 ++
 .../paimon/format/avro/AvroBytesArrayTest.java     | 335 ++++++++++++++++++
 5 files changed, 907 insertions(+), 3 deletions(-)

diff --git 
a/paimon-benchmark/paimon-micro-benchmarks/src/test/java/org/apache/paimon/benchmark/compact/ArrayCompactBenchmark.java
 
b/paimon-benchmark/paimon-micro-benchmarks/src/test/java/org/apache/paimon/benchmark/compact/ArrayCompactBenchmark.java
new file mode 100644
index 0000000000..83f0238517
--- /dev/null
+++ 
b/paimon-benchmark/paimon-micro-benchmarks/src/test/java/org/apache/paimon/benchmark/compact/ArrayCompactBenchmark.java
@@ -0,0 +1,142 @@
+/*
+ * 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.paimon.benchmark.compact;
+
+import org.apache.paimon.benchmark.Benchmark;
+import org.apache.paimon.catalog.FileSystemCatalog;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.GenericArray;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.sink.TableWriteImpl;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.DataTypes;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/** Benchmark for compacting rows with array. */
+public class ArrayCompactBenchmark {
+
+    @TempDir java.nio.file.Path tempDir;
+
+    private final int rowCount = 4_000_000;
+    private final int numberArraySize = 100;
+    private final int stringArraySize = 20;
+
+    @Test
+    public void testArrayInt() throws Exception {
+        runBenchmark("array-int", DataTypes.INT(), this::intArray);
+    }
+
+    @Test
+    public void testArrayBigInt() throws Exception {
+        runBenchmark("array-bigint", DataTypes.BIGINT(), this::bigIntArray);
+    }
+
+    @Test
+    public void testArrayString() throws Exception {
+        runBenchmark("array-string", DataTypes.STRING(), this::stringArray);
+    }
+
+    private void runBenchmark(String name, DataType elementType, ArrayFactory 
arrayFactory)
+            throws Exception {
+        Benchmark benchmark =
+                new Benchmark("avro-" + name + "-compact-benchmark", rowCount)
+                        .setNumWarmupIters(1)
+                        .setOutputPerIteration(true);
+
+        FileStoreTable table = createTable(elementType);
+        benchmark.addCase(
+                "write-and-compact",
+                1,
+                () -> {
+                    try {
+                        writeAndCompact(table, arrayFactory);
+                    } catch (Exception e) {
+                        throw new RuntimeException(e);
+                    }
+                });
+        benchmark.run();
+    }
+
+    private FileStoreTable createTable(DataType elementType) throws Exception {
+        FileSystemCatalog catalog =
+                new FileSystemCatalog(LocalFileIO.create(), new 
Path(tempDir.toString()));
+        catalog.createDatabase("default", false);
+        Identifier identifier = Identifier.create("default", "T");
+        catalog.createTable(
+                identifier,
+                Schema.newBuilder()
+                        .column("k", DataTypes.STRING())
+                        .column("v", DataTypes.ARRAY(elementType))
+                        .primaryKey("k")
+                        .option("bucket", "1")
+                        .option("file.format", "avro")
+                        .build(),
+                false);
+        return (FileStoreTable) catalog.getTable(identifier);
+    }
+
+    private void writeAndCompact(FileStoreTable table, ArrayFactory 
arrayFactory) {
+        try (TableWriteImpl<?> write = table.newWrite("test")) {
+            for (int i = 0; i < rowCount; i++) {
+                write.write(
+                        GenericRow.of(
+                                BinaryString.fromString(String.valueOf(i)),
+                                arrayFactory.create(i)));
+            }
+            write.prepareCommit(true, 1);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private GenericArray intArray(int rowId) {
+        Integer[] array = new Integer[numberArraySize];
+        for (int i = 0; i < numberArraySize; i++) {
+            array[i] = rowId * numberArraySize + i;
+        }
+        return new GenericArray(array);
+    }
+
+    private GenericArray bigIntArray(int rowId) {
+        Long[] array = new Long[numberArraySize];
+        for (int i = 0; i < numberArraySize; i++) {
+            array[i] = (long) rowId * numberArraySize + i;
+        }
+        return new GenericArray(array);
+    }
+
+    private GenericArray stringArray(int rowId) {
+        BinaryString[] array = new BinaryString[stringArraySize];
+        for (int i = 0; i < stringArraySize; i++) {
+            array[i] = BinaryString.fromString(String.valueOf(rowId * 
stringArraySize + i));
+        }
+        return new GenericArray(array);
+    }
+
+    private interface ArrayFactory {
+        GenericArray create(int rowId);
+    }
+}
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroBytesArray.java 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroBytesArray.java
new file mode 100644
index 0000000000..5babf19f5c
--- /dev/null
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroBytesArray.java
@@ -0,0 +1,380 @@
+/*
+ * 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.paimon.format.avro;
+
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.Blob;
+import org.apache.paimon.data.Decimal;
+import org.apache.paimon.data.GenericArray;
+import org.apache.paimon.data.InternalArray;
+import org.apache.paimon.data.InternalMap;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.InternalVector;
+import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.data.variant.Variant;
+import org.apache.paimon.utils.IntArrayList;
+
+import org.apache.avro.Schema;
+import org.apache.avro.io.BinaryData;
+import org.apache.avro.io.BinaryDecoder;
+import org.apache.avro.io.BinaryEncoder;
+import org.apache.avro.io.DecoderFactory;
+
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+
+/** An {@link InternalArray} whose elements are stored as Avro binary 
payloads. */
+public class AvroBytesArray implements InternalArray {
+
+    // stores original element bytes from avro files
+    private final byte[] bytes;
+    // total bytes length
+    private final int lengthInBytes;
+    // offset of each element payload
+    private final IntArrayList off;
+    // length of each element payload
+    private final IntArrayList len;
+    // reader to decode element payloads lazily
+    private final FieldReader elementReader;
+
+    // decoded array, lazily initialized when elements are accessed
+    private GenericArray decodedArray;
+
+    private AvroBytesArray(
+            byte[] bytes,
+            int lengthInBytes,
+            IntArrayList off,
+            IntArrayList len,
+            FieldReader elementReader) {
+        this.bytes = bytes;
+        this.lengthInBytes = lengthInBytes;
+        this.off = off;
+        this.len = len;
+        this.elementReader = elementReader;
+    }
+
+    @Nullable
+    public static AvroBytesArray create(
+            BinaryDecoder decoder, FieldReader elementReader, Schema 
elementType)
+            throws IOException {
+        RawFieldReader rawFieldReader = createRawFieldReader(elementType);
+        if (rawFieldReader == null) {
+            return null;
+        }
+
+        BytesBuilder builder = new BytesBuilder();
+        IntArrayList off = new IntArrayList(16);
+        IntArrayList len = new IntArrayList(16);
+
+        long chunkLength = decoder.readArrayStart();
+        while (chunkLength > 0) {
+            for (int i = 0; i < chunkLength; i++) {
+                int offset = builder.lengthInBytes;
+                rawFieldReader.read(decoder, builder);
+                off.add(offset);
+                len.add(builder.lengthInBytes - offset);
+            }
+            chunkLength = decoder.arrayNext();
+        }
+
+        return new AvroBytesArray(builder.bytes, builder.lengthInBytes, off, 
len, elementReader);
+    }
+
+    @Nullable
+    private static RawFieldReader createRawFieldReader(Schema elementSchema) {
+        // handle nullable
+        if (elementSchema.getType() == Schema.Type.UNION) {
+            RawFieldReader nested = 
createRawFieldReader(elementSchema.getTypes().get(1));
+            if (nested == null) {
+                return null;
+            }
+            return (decoder, builder) -> {
+                int index = decoder.readInt();
+                builder.addInt(index);
+                if (index == 1) {
+                    nested.read(decoder, builder);
+                }
+            };
+        }
+
+        switch (elementSchema.getType()) {
+            case BOOLEAN:
+                return (decoder, builder) -> 
builder.addBoolean(decoder.readBoolean());
+            case INT:
+                return (decoder, builder) -> builder.addInt(decoder.readInt());
+            case LONG:
+                return (decoder, builder) -> 
builder.addLong(decoder.readLong());
+            case FLOAT:
+                return (decoder, builder) -> 
builder.addFloat(decoder.readFloat());
+            case DOUBLE:
+                return (decoder, builder) -> 
builder.addDouble(decoder.readDouble());
+            case STRING:
+            case BYTES:
+                return (decoder, builder) -> builder.addBytes(decoder);
+            default:
+                // unknown or unsupported
+                return null;
+        }
+    }
+
+    public void writeRawElements(BinaryEncoder encoder, int offset, int 
length) throws IOException {
+        if (length == 0) {
+            return;
+        }
+
+        checkRange(offset, length);
+        int rawOffset = off.get(offset);
+        int last = offset + length - 1;
+        int rawLength = off.get(last) + len.get(last) - rawOffset;
+        encoder.writeFixed(bytes, rawOffset, rawLength);
+    }
+
+    private void checkRange(int offset, int length) {
+        if (offset < 0 || length < 0 || offset + length > size()) {
+            throw new IndexOutOfBoundsException(
+                    String.format(
+                            "Invalid range offset %s and length %s for array 
size %s.",
+                            offset, length, size()));
+        }
+    }
+
+    public byte[] bytes() {
+        return bytes;
+    }
+
+    public int lengthInBytes() {
+        return lengthInBytes;
+    }
+
+    @Override
+    public int size() {
+        return off.size();
+    }
+
+    @Override
+    public boolean isNullAt(int pos) {
+        return decodedArray().isNullAt(pos);
+    }
+
+    @Override
+    public boolean getBoolean(int pos) {
+        return decodedArray().getBoolean(pos);
+    }
+
+    @Override
+    public byte getByte(int pos) {
+        return decodedArray().getByte(pos);
+    }
+
+    @Override
+    public short getShort(int pos) {
+        return decodedArray().getShort(pos);
+    }
+
+    @Override
+    public int getInt(int pos) {
+        return decodedArray().getInt(pos);
+    }
+
+    @Override
+    public long getLong(int pos) {
+        return decodedArray().getLong(pos);
+    }
+
+    @Override
+    public float getFloat(int pos) {
+        return decodedArray().getFloat(pos);
+    }
+
+    @Override
+    public double getDouble(int pos) {
+        return decodedArray().getDouble(pos);
+    }
+
+    @Override
+    public BinaryString getString(int pos) {
+        return decodedArray().getString(pos);
+    }
+
+    @Override
+    public Decimal getDecimal(int pos, int precision, int scale) {
+        return decodedArray().getDecimal(pos, precision, scale);
+    }
+
+    @Override
+    public Timestamp getTimestamp(int pos, int precision) {
+        return decodedArray().getTimestamp(pos, precision);
+    }
+
+    @Override
+    public byte[] getBinary(int pos) {
+        return decodedArray().getBinary(pos);
+    }
+
+    @Override
+    public Variant getVariant(int pos) {
+        return decodedArray().getVariant(pos);
+    }
+
+    @Override
+    public Blob getBlob(int pos) {
+        return decodedArray().getBlob(pos);
+    }
+
+    @Override
+    public InternalArray getArray(int pos) {
+        return decodedArray().getArray(pos);
+    }
+
+    @Override
+    public InternalVector getVector(int pos) {
+        return decodedArray().getVector(pos);
+    }
+
+    @Override
+    public InternalMap getMap(int pos) {
+        return decodedArray().getMap(pos);
+    }
+
+    @Override
+    public InternalRow getRow(int pos, int numFields) {
+        return decodedArray().getRow(pos, numFields);
+    }
+
+    @Override
+    public boolean[] toBooleanArray() {
+        return decodedArray().toBooleanArray();
+    }
+
+    @Override
+    public byte[] toByteArray() {
+        return decodedArray().toByteArray();
+    }
+
+    @Override
+    public short[] toShortArray() {
+        return decodedArray().toShortArray();
+    }
+
+    @Override
+    public int[] toIntArray() {
+        return decodedArray().toIntArray();
+    }
+
+    @Override
+    public long[] toLongArray() {
+        return decodedArray().toLongArray();
+    }
+
+    @Override
+    public float[] toFloatArray() {
+        return decodedArray().toFloatArray();
+    }
+
+    @Override
+    public double[] toDoubleArray() {
+        return decodedArray().toDoubleArray();
+    }
+
+    private GenericArray decodedArray() {
+        if (decodedArray != null) {
+            return decodedArray;
+        }
+
+        try {
+            BinaryDecoder decoder =
+                    DecoderFactory.get().binaryDecoder(bytes, 0, 
lengthInBytes, null);
+            Object[] values = new Object[size()];
+            for (int i = 0; i < size(); i++) {
+                values[i] = elementReader.read(decoder, null);
+            }
+            decodedArray = new GenericArray(values);
+            return decodedArray;
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to decode Avro array bytes.", 
e);
+        }
+    }
+
+    /** Read the raw bytes from decoder and add to the {@link BytesBuilder}. */
+    private interface RawFieldReader {
+
+        void read(BinaryDecoder decoder, BytesBuilder builder) throws 
IOException;
+    }
+
+    /**
+     * A growable byte buffer used to copy Avro binary-encoded element 
payloads from a decoder.
+     *
+     * <p>Please see the #encodeX method comments to know how many spaces 
should be ensured.
+     */
+    private static class BytesBuilder {
+
+        private byte[] bytes = new byte[256];
+        private int lengthInBytes;
+
+        void addBoolean(boolean value) {
+            ensure(1);
+            lengthInBytes += BinaryData.encodeBoolean(value, bytes, 
lengthInBytes);
+        }
+
+        void addInt(int value) {
+            ensure(5);
+            lengthInBytes += BinaryData.encodeInt(value, bytes, lengthInBytes);
+        }
+
+        void addLong(long value) {
+            ensure(10);
+            lengthInBytes += BinaryData.encodeLong(value, bytes, 
lengthInBytes);
+        }
+
+        void addFloat(float value) {
+            ensure(4);
+            lengthInBytes += BinaryData.encodeFloat(value, bytes, 
lengthInBytes);
+        }
+
+        void addDouble(double value) {
+            ensure(8);
+            lengthInBytes += BinaryData.encodeDouble(value, bytes, 
lengthInBytes);
+        }
+
+        void addBytes(BinaryDecoder decoder) throws IOException {
+            // BinaryDecoder#readString,readBytes
+            int l = (int) decoder.readLong();
+            ensure(l + 10);
+            lengthInBytes += BinaryData.encodeLong(l, bytes, lengthInBytes);
+            decoder.readFixed(bytes, lengthInBytes, l);
+            lengthInBytes += l;
+        }
+
+        private void ensure(int need) {
+            if (lengthInBytes + need <= bytes.length) {
+                return;
+            }
+
+            int cap = bytes.length;
+            while (lengthInBytes + need > cap) {
+                cap *= 2;
+            }
+
+            byte[] newBytes = new byte[cap];
+            System.arraycopy(bytes, 0, newBytes, 0, lengthInBytes);
+            bytes = newBytes;
+        }
+    }
+}
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/FieldReaderFactory.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/FieldReaderFactory.java
index 4801134507..41e71d4fde 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/FieldReaderFactory.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/FieldReaderFactory.java
@@ -164,7 +164,7 @@ public class FieldReaderFactory implements 
AvroSchemaVisitor<FieldReader> {
     @Override
     public FieldReader visitArray(Schema schema, @Nullable DataType 
elementType) {
         FieldReader elementReader = visit(schema.getElementType(), 
elementType);
-        return new ArrayReader(elementReader);
+        return new ArrayReader(schema.getElementType(), elementReader);
     }
 
     @Override
@@ -429,15 +429,26 @@ public class FieldReaderFactory implements 
AvroSchemaVisitor<FieldReader> {
 
     private static class ArrayReader implements FieldReader {
 
+        private final Schema elementSchema;
         private final FieldReader elementReader;
         private final List<Object> reusedList = new ArrayList<>();
 
-        private ArrayReader(FieldReader elementReader) {
+        private ArrayReader(@Nullable Schema elementSchema, FieldReader 
elementReader) {
+            this.elementSchema = elementSchema;
             this.elementReader = elementReader;
         }
 
         @Override
         public Object read(Decoder decoder, Object reuse) throws IOException {
+            if (elementSchema != null && decoder instanceof BinaryDecoder) {
+                AvroBytesArray avroBytesArray =
+                        AvroBytesArray.create(
+                                (BinaryDecoder) decoder, elementReader, 
elementSchema);
+                if (avroBytesArray != null) {
+                    return avroBytesArray;
+                }
+            }
+
             reusedList.clear();
             long chunkLength = decoder.readArrayStart();
 
@@ -471,7 +482,7 @@ public class FieldReaderFactory implements 
AvroSchemaVisitor<FieldReader> {
         private final DataType elementType;
 
         private ArrayVectorReader(FieldReader elementReader, DataType 
elementType) {
-            super(elementReader);
+            super(null, elementReader);
             this.elementType = elementType;
         }
 
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/FieldWriterFactory.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/FieldWriterFactory.java
index 8ca985bd1a..9cefc48e09 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/FieldWriterFactory.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/FieldWriterFactory.java
@@ -186,6 +186,11 @@ public class FieldWriterFactory implements 
AvroSchemaVisitor<FieldWriter> {
         FieldWriter elementWriter = visit(schema.getElementType(), 
elementType);
         return (container, index, encoder) -> {
             InternalArray array = container.getArray(index);
+            if (encoder instanceof BinaryEncoder) {
+                writeArrayData(array, elementWriter, (BinaryEncoder) encoder);
+                return;
+            }
+
             encoder.writeArrayStart();
             int numElements = array.size();
             encoder.setItemCount(numElements);
@@ -197,6 +202,37 @@ public class FieldWriterFactory implements 
AvroSchemaVisitor<FieldWriter> {
         };
     }
 
+    private void writeArrayData(
+            InternalArray array, FieldWriter elementWriter, BinaryEncoder 
encoder)
+            throws IOException {
+        encoder.writeArrayStart();
+        encoder.setItemCount(array.size());
+        writeArrayItems(array, 0, array.size(), elementWriter, encoder);
+        encoder.writeArrayEnd();
+    }
+
+    private void writeArrayItems(
+            InternalArray array,
+            int offset,
+            int length,
+            FieldWriter elementWriter,
+            BinaryEncoder encoder)
+            throws IOException {
+        if (length == 0) {
+            return;
+        }
+
+        if (array instanceof AvroBytesArray) {
+            ((AvroBytesArray) array).writeRawElements(encoder, offset, length);
+        } else {
+            // fallback to element by element
+            for (int i = 0; i < length; i++) {
+                encoder.startItem();
+                elementWriter.write(array, offset + i, encoder);
+            }
+        }
+    }
+
     @Override
     public FieldWriter visitArrayVector(Schema schema, DataType elementType) {
         FieldWriter elementWriter = visit(schema.getElementType(), 
elementType);
diff --git 
a/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroBytesArrayTest.java
 
b/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroBytesArrayTest.java
new file mode 100644
index 0000000000..12280c5a2f
--- /dev/null
+++ 
b/paimon-format/src/test/java/org/apache/paimon/format/avro/AvroBytesArrayTest.java
@@ -0,0 +1,335 @@
+/*
+ * 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.paimon.format.avro;
+
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.Decimal;
+import org.apache.paimon.data.GenericArray;
+import org.apache.paimon.data.InternalArray;
+import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.DecimalType;
+import org.apache.paimon.types.LocalZonedTimestampType;
+import org.apache.paimon.types.TimestampType;
+
+import org.apache.avro.Schema;
+import org.apache.avro.io.BinaryDecoder;
+import org.apache.avro.io.BinaryEncoder;
+import org.apache.avro.io.DecoderFactory;
+import org.apache.avro.io.EncoderFactory;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.math.BigDecimal;
+import java.util.HashMap;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/** Tests for {@link AvroBytesArray}. */
+public class AvroBytesArrayTest {
+
+    @Test
+    public void testNotNullElementsForSupportedTypes() throws Exception {
+        checkResult(DataTypes.BOOLEAN(), objectArray(true, false, true), 
false);
+        checkResult(DataTypes.TINYINT(), objectArray((byte) 1, (byte) 2, 
(byte) 3), false);
+        checkResult(DataTypes.SMALLINT(), objectArray((short) 1, (short) 2, 
(short) 3), false);
+        checkResult(DataTypes.INT(), objectArray(1, 2, 3), false);
+        checkResult(DataTypes.BIGINT(), objectArray(1L, 2L, 3L), false);
+        checkResult(DataTypes.FLOAT(), objectArray(1.25F, 2.5F, 3.75F), false);
+        checkResult(DataTypes.DOUBLE(), objectArray(1.25D, 2.5D, 3.75D), 
false);
+        checkResult(
+                DataTypes.CHAR(10),
+                objectArray(
+                        BinaryString.fromString("a"),
+                        BinaryString.fromString("b"),
+                        BinaryString.fromString("c")),
+                false);
+        checkResult(
+                DataTypes.STRING(),
+                objectArray(
+                        BinaryString.fromString("d"),
+                        BinaryString.fromString("e"),
+                        BinaryString.fromString("f")),
+                false);
+        checkResult(
+                DataTypes.BINARY(10),
+                objectArray(new byte[] {1, 2}, new byte[] {3, 4}, new byte[] 
{5, 6}),
+                false);
+        checkResult(
+                DataTypes.VARBINARY(10),
+                objectArray(new byte[] {1, 2}, new byte[] {3, 4}, new byte[] 
{5, 6}),
+                false);
+        checkResult(DataTypes.DATE(), objectArray(1, 2, 3), false);
+        checkResult(DataTypes.TIME(), objectArray(1000, 2000, 3000), false);
+        checkResult(
+                DataTypes.TIMESTAMP(3),
+                objectArray(
+                        Timestamp.fromEpochMillis(1000),
+                        Timestamp.fromEpochMillis(2000),
+                        Timestamp.fromEpochMillis(3000)),
+                false);
+        checkResult(
+                DataTypes.TIMESTAMP(6),
+                objectArray(
+                        Timestamp.fromMicros(1001),
+                        Timestamp.fromMicros(2002),
+                        Timestamp.fromMicros(3003)),
+                false);
+        checkResult(
+                DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(3),
+                objectArray(
+                        Timestamp.fromEpochMillis(1000),
+                        Timestamp.fromEpochMillis(2000),
+                        Timestamp.fromEpochMillis(3000)),
+                false);
+        checkResult(
+                DataTypes.DECIMAL(10, 2),
+                objectArray(decimal("1.23"), decimal("2.34"), decimal("3.45")),
+                false);
+    }
+
+    @Test
+    public void testNullableElementsForSupportedTypes() throws Exception {
+        checkResult(DataTypes.BOOLEAN(), objectArray(true, null, false), true);
+        checkResult(DataTypes.TINYINT(), objectArray((byte) 1, null, (byte) 
2), true);
+        checkResult(DataTypes.SMALLINT(), objectArray((short) 1, null, (short) 
2), true);
+        checkResult(DataTypes.INT(), objectArray(1, null, 2), true);
+        checkResult(DataTypes.BIGINT(), objectArray(1L, null, 2L), true);
+        checkResult(DataTypes.FLOAT(), objectArray(1.25F, null, 2.5F), true);
+        checkResult(DataTypes.DOUBLE(), objectArray(1.25D, null, 2.5D), true);
+        checkResult(
+                DataTypes.CHAR(10),
+                objectArray(BinaryString.fromString("a"), null, 
BinaryString.fromString("b")),
+                true);
+        checkResult(
+                DataTypes.STRING(),
+                objectArray(BinaryString.fromString("c"), null, 
BinaryString.fromString("d")),
+                true);
+        checkResult(
+                DataTypes.BINARY(10),
+                objectArray(new byte[] {1, 2}, null, new byte[] {3, 4}),
+                true);
+        checkResult(
+                DataTypes.VARBINARY(10),
+                objectArray(new byte[] {1, 2}, null, new byte[] {3, 4}),
+                true);
+        checkResult(DataTypes.DATE(), objectArray(1, null, 2), true);
+        checkResult(DataTypes.TIME(), objectArray(1000, null, 2000), true);
+        checkResult(
+                DataTypes.TIMESTAMP(3),
+                objectArray(Timestamp.fromEpochMillis(1000), null, 
Timestamp.fromEpochMillis(2000)),
+                true);
+        checkResult(
+                DataTypes.TIMESTAMP(6),
+                objectArray(Timestamp.fromMicros(1001), null, 
Timestamp.fromMicros(2002)),
+                true);
+        checkResult(
+                DataTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE(3),
+                objectArray(Timestamp.fromEpochMillis(1000), null, 
Timestamp.fromEpochMillis(2000)),
+                true);
+        checkResult(
+                DataTypes.DECIMAL(10, 2),
+                objectArray(decimal("1.23"), null, decimal("2.34")),
+                true);
+    }
+
+    private void checkResult(DataType elementType, GenericArray values, 
boolean nullable)
+            throws Exception {
+        DataType arrayElementType = nullable ? elementType : 
elementType.notNull();
+        Schema arraySchema =
+                AvroSchemaConverter.convertToSchema(
+                        DataTypes.ARRAY(arrayElementType).notNull(), new 
HashMap<>());
+        Schema elementSchema = arraySchema.getElementType();
+
+        byte[] bytes = writeRawArrayBytes(arrayElementType, values);
+        BinaryDecoder decoder = DecoderFactory.get().binaryDecoder(bytes, 
null);
+        FieldReader elementReader = new 
FieldReaderFactory().visit(elementSchema, arrayElementType);
+        AvroBytesArray array = AvroBytesArray.create(decoder, elementReader, 
elementSchema);
+        assertThat(array).isNotNull();
+        assertThat(array.size()).isEqualTo(values.size());
+
+        checkArrayValues(elementType, values, array);
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(baos, null);
+        encoder.writeArrayStart();
+        encoder.setItemCount(array.size());
+        array.writeRawElements(encoder, 0, array.size());
+        encoder.writeArrayEnd();
+        encoder.flush();
+        assertThat(baos.toByteArray()).isEqualTo(bytes);
+    }
+
+    private byte[] writeRawArrayBytes(DataType elementType, GenericArray 
values) throws Exception {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(baos, null);
+        encoder.writeArrayStart();
+        encoder.setItemCount(values.size());
+        for (int i = 0; i < values.size(); i++) {
+            encoder.startItem();
+            if (values.isNullAt(i)) {
+                encoder.writeIndex(0);
+            } else {
+                if (elementType.isNullable()) {
+                    encoder.writeIndex(1);
+                }
+                writeElement(
+                        elementType,
+                        
InternalArray.createElementGetter(elementType).getElementOrNull(values, i),
+                        encoder);
+            }
+        }
+        encoder.writeArrayEnd();
+        encoder.flush();
+        return baos.toByteArray();
+    }
+
+    private void checkArrayValues(
+            DataType elementType, GenericArray expectedValues, InternalArray 
actualArray) {
+        InternalArray.ElementGetter getter = 
InternalArray.createElementGetter(elementType);
+        for (int i = 0; i < expectedValues.size(); i++) {
+            if (expectedValues.isNullAt(i)) {
+                assertThat(actualArray.isNullAt(i)).isTrue();
+            } else {
+                checkValue(elementType, 
getter.getElementOrNull(expectedValues, i), actualArray, i);
+            }
+        }
+    }
+
+    private void checkValue(
+            DataType elementType, Object expected, InternalArray actualArray, 
int pos) {
+        switch (elementType.getTypeRoot()) {
+            case CHAR:
+            case VARCHAR:
+                
assertThat(actualArray.getString(pos).toString()).isEqualTo(expected.toString());
+                return;
+            case BOOLEAN:
+                assertThat(actualArray.getBoolean(pos)).isEqualTo(expected);
+                return;
+            case BINARY:
+            case VARBINARY:
+                
assertThat(actualArray.getBinary(pos)).containsExactly((byte[]) expected);
+                return;
+            case DECIMAL:
+                DecimalType decimalType = (DecimalType) elementType;
+                assertThat(
+                                actualArray.getDecimal(
+                                        pos, decimalType.getPrecision(), 
decimalType.getScale()))
+                        .isEqualTo(expected);
+                return;
+            case TINYINT:
+                assertThat(actualArray.getByte(pos)).isEqualTo(expected);
+                return;
+            case SMALLINT:
+                assertThat(actualArray.getShort(pos)).isEqualTo(expected);
+                return;
+            case INTEGER:
+            case DATE:
+            case TIME_WITHOUT_TIME_ZONE:
+                assertThat(actualArray.getInt(pos)).isEqualTo(expected);
+                return;
+            case BIGINT:
+                assertThat(actualArray.getLong(pos)).isEqualTo(expected);
+                return;
+            case FLOAT:
+                assertThat(actualArray.getFloat(pos)).isEqualTo(expected);
+                return;
+            case DOUBLE:
+                assertThat(actualArray.getDouble(pos)).isEqualTo(expected);
+                return;
+            case TIMESTAMP_WITHOUT_TIME_ZONE:
+            case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+                assertThat(actualArray.getTimestamp(pos, 
timestampPrecision(elementType)))
+                        .isEqualTo(expected);
+                return;
+            default:
+                throw new UnsupportedOperationException(
+                        "Unsupported AvroBytesArray test type: " + 
elementType);
+        }
+    }
+
+    private void writeElement(DataType elementType, Object value, 
BinaryEncoder encoder)
+            throws Exception {
+        switch (elementType.getTypeRoot()) {
+            case BOOLEAN:
+                encoder.writeBoolean((Boolean) value);
+                return;
+            case TINYINT:
+            case SMALLINT:
+            case INTEGER:
+            case DATE:
+            case TIME_WITHOUT_TIME_ZONE:
+                encoder.writeInt(((Number) value).intValue());
+                return;
+            case BIGINT:
+                encoder.writeLong((Long) value);
+                return;
+            case FLOAT:
+                encoder.writeFloat((Float) value);
+                return;
+            case DOUBLE:
+                encoder.writeDouble((Double) value);
+                return;
+            case VARCHAR:
+            case CHAR:
+                encoder.writeString(value.toString());
+                return;
+            case BINARY:
+            case VARBINARY:
+                encoder.writeBytes((byte[]) value);
+                return;
+            case DECIMAL:
+                encoder.writeBytes(((Decimal) value).toUnscaledBytes());
+                return;
+            case TIMESTAMP_WITHOUT_TIME_ZONE:
+            case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+                Timestamp timestamp = (Timestamp) value;
+                if (timestampPrecision(elementType) <= 3) {
+                    encoder.writeLong(timestamp.getMillisecond());
+                } else {
+                    encoder.writeLong(timestamp.toMicros());
+                }
+                return;
+            default:
+                throw new UnsupportedOperationException(
+                        "Unsupported E2E AvroBytesArray test type: " + 
elementType);
+        }
+    }
+
+    private int timestampPrecision(DataType elementType) {
+        switch (elementType.getTypeRoot()) {
+            case TIMESTAMP_WITHOUT_TIME_ZONE:
+                return ((TimestampType) elementType).getPrecision();
+            case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+                return ((LocalZonedTimestampType) elementType).getPrecision();
+            default:
+                throw new UnsupportedOperationException(
+                        "Unsupported timestamp type: " + elementType);
+        }
+    }
+
+    private GenericArray objectArray(Object... values) {
+        return new GenericArray(values);
+    }
+
+    private Decimal decimal(String value) {
+        return Decimal.fromBigDecimal(new BigDecimal(value), 10, 2);
+    }
+}


Reply via email to