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

luoyuxia pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fluss.git


The following commit(s) were added to refs/heads/main by this push:
     new 0a699b322 [lake/paimon] TieringSourceReader adjust to scan arrow 
record batch and write arrow record batch to lake (#3430)
0a699b322 is described below

commit 0a699b3228498c189f52db65eff0f7a8d4f6d413
Author: yuxia Luo <[email protected]>
AuthorDate: Fri Jun 5 21:25:58 2026 +0800

    [lake/paimon] TieringSourceReader adjust to scan arrow record batch and 
write arrow record batch to lake (#3430)
---
 .../table/scanner/log/ArrowLogFetchCollector.java  |   4 +-
 .../client/table/scanner/log/ArrowScanRecords.java |  25 +-
 .../apache/fluss/lake/batch/ArrowRecordBatch.java  |  25 +-
 .../org/apache/fluss/record/ArrowBatchData.java    |  43 +-
 .../apache/fluss/utils/UnshadedArrowReadUtils.java |   4 +-
 .../flink/tiering/source/TieringSourceReader.java  |   7 +-
 .../flink/tiering/source/TieringSplitReader.java   | 249 ++++++--
 .../tiering/source/TieringSplitReaderTest.java     |  11 +-
 fluss-lake/fluss-lake-paimon/pom.xml               |  25 +
 .../lake/paimon/tiering/PaimonLakeWriter.java      |  24 +-
 .../tiering/append/AppendOnlyArrowBatchHelper.java | 230 ++++++++
 .../paimon/tiering/append/AppendOnlyWriter.java    |  35 ++
 .../converter/Arrow2PaimonVectorConverter.java     | 630 +++++++++++++++++++++
 .../lake/paimon/tiering/PaimonTieringITCase.java   |   4 +-
 fluss-spark/fluss-spark-ut/pom.xml                 |   7 +
 fluss-test-coverage/pom.xml                        |   1 +
 16 files changed, 1273 insertions(+), 51 deletions(-)

diff --git 
a/fluss-client/src/main/java/org/apache/fluss/client/table/scanner/log/ArrowLogFetchCollector.java
 
b/fluss-client/src/main/java/org/apache/fluss/client/table/scanner/log/ArrowLogFetchCollector.java
index 4c6f7eab9..ef53b464e 100644
--- 
a/fluss-client/src/main/java/org/apache/fluss/client/table/scanner/log/ArrowLogFetchCollector.java
+++ 
b/fluss-client/src/main/java/org/apache/fluss/client/table/scanner/log/ArrowLogFetchCollector.java
@@ -65,9 +65,7 @@ public class ArrowLogFetchCollector
     protected ArrowScanRecords toResult(
             Map<TableBucket, List<ArrowBatchData>> fetchedRecords,
             Map<TableBucket, Long> consumedUpToOffsets) {
-        // Arrow scan paths don't need consumedUpToOffsets (issue #2371 is 
specific to
-        // row-based tiering), so it's discarded here rather than carried in 
ArrowScanRecords.
-        return new ArrowScanRecords(fetchedRecords);
+        return new ArrowScanRecords(fetchedRecords, consumedUpToOffsets);
     }
 
     @Override
diff --git 
a/fluss-client/src/main/java/org/apache/fluss/client/table/scanner/log/ArrowScanRecords.java
 
b/fluss-client/src/main/java/org/apache/fluss/client/table/scanner/log/ArrowScanRecords.java
index d5cb63a8e..048eba4e1 100644
--- 
a/fluss-client/src/main/java/org/apache/fluss/client/table/scanner/log/ArrowScanRecords.java
+++ 
b/fluss-client/src/main/java/org/apache/fluss/client/table/scanner/log/ArrowScanRecords.java
@@ -24,6 +24,7 @@ import org.apache.fluss.utils.AbstractIterator;
 import org.apache.fluss.utils.IOUtils;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import java.util.Collections;
 import java.util.Iterator;
@@ -43,8 +44,18 @@ public class ArrowScanRecords implements 
Iterable<ArrowBatchData>, AutoCloseable
 
     private final Map<TableBucket, List<ArrowBatchData>> records;
 
+    /** The exclusive upper bound of consumed offsets per polled bucket in 
this round. */
+    private final Map<TableBucket, Long> consumedUpToOffsets;
+
     public ArrowScanRecords(Map<TableBucket, List<ArrowBatchData>> records) {
+        this(records, Collections.emptyMap());
+    }
+
+    public ArrowScanRecords(
+            Map<TableBucket, List<ArrowBatchData>> records,
+            Map<TableBucket, Long> consumedUpToOffsets) {
         this.records = records;
+        this.consumedUpToOffsets = consumedUpToOffsets;
     }
 
     /** Get just the Arrow batches for the given bucket. */
@@ -56,11 +67,23 @@ public class ArrowScanRecords implements 
Iterable<ArrowBatchData>, AutoCloseable
         return Collections.unmodifiableList(recs);
     }
 
-    /** Returns the buckets that contain Arrow batches. */
+    /** Returns the buckets that were polled in this round. */
     public Set<TableBucket> buckets() {
         return Collections.unmodifiableSet(records.keySet());
     }
 
+    /**
+     * Get the exclusive upper bound of offsets consumed for the given bucket 
in this poll round.
+     *
+     * @param bucket the bucket to query
+     * @return the exclusive upper bound offset, or {@code null} if the bucket 
was not polled in
+     *     this round
+     */
+    @Nullable
+    public Long consumedUpToOffset(TableBucket bucket) {
+        return consumedUpToOffsets.get(bucket);
+    }
+
     /** Returns the total number of rows in all batches. */
     public int count() {
         int count = 0;
diff --git 
a/fluss-common/src/main/java/org/apache/fluss/lake/batch/ArrowRecordBatch.java 
b/fluss-common/src/main/java/org/apache/fluss/lake/batch/ArrowRecordBatch.java
index 78548d69d..769e6fe9c 100644
--- 
a/fluss-common/src/main/java/org/apache/fluss/lake/batch/ArrowRecordBatch.java
+++ 
b/fluss-common/src/main/java/org/apache/fluss/lake/batch/ArrowRecordBatch.java
@@ -18,11 +18,32 @@
 package org.apache.fluss.lake.batch;
 
 import org.apache.fluss.annotation.PublicEvolving;
+import org.apache.fluss.record.ArrowBatchData;
 
 /**
- * The Arrow implementation of the RecordBatch interface.
+ * The Arrow implementation of the {@link RecordBatch} interface.
+ *
+ * <p>Wraps an {@link ArrowBatchData} for use by lake writers that support 
batch writing via {@link
+ * org.apache.fluss.lake.writer.SupportsRecordBatchWrite}.
  *
  * @since 0.7
  */
 @PublicEvolving
-public class ArrowRecordBatch implements RecordBatch {}
+public class ArrowRecordBatch implements RecordBatch, AutoCloseable {
+
+    private final ArrowBatchData arrowBatchData;
+
+    public ArrowRecordBatch(ArrowBatchData arrowBatchData) {
+        this.arrowBatchData = arrowBatchData;
+    }
+
+    /** Returns the underlying {@link ArrowBatchData}. */
+    public ArrowBatchData getArrowBatchData() {
+        return arrowBatchData;
+    }
+
+    @Override
+    public void close() {
+        arrowBatchData.close();
+    }
+}
diff --git 
a/fluss-common/src/main/java/org/apache/fluss/record/ArrowBatchData.java 
b/fluss-common/src/main/java/org/apache/fluss/record/ArrowBatchData.java
index 515f73d5a..c3e55fadf 100644
--- a/fluss-common/src/main/java/org/apache/fluss/record/ArrowBatchData.java
+++ b/fluss-common/src/main/java/org/apache/fluss/record/ArrowBatchData.java
@@ -19,6 +19,8 @@ package org.apache.fluss.record;
 
 import org.apache.fluss.annotation.Internal;
 
+import org.apache.arrow.memory.ArrowBuf;
+import org.apache.arrow.vector.FieldVector;
 import org.apache.arrow.vector.VectorSchemaRoot;
 
 import static org.apache.fluss.utils.Preconditions.checkArgument;
@@ -38,6 +40,7 @@ public class ArrowBatchData implements AutoCloseable {
     private final long baseLogOffset;
     private final long timestamp;
     private final int schemaId;
+    private boolean closed;
 
     public ArrowBatchData(
             VectorSchemaRoot vectorSchemaRoot, long baseLogOffset, long 
timestamp, int schemaId) {
@@ -72,6 +75,17 @@ public class ArrowBatchData implements AutoCloseable {
         return vectorSchemaRoot.getRowCount();
     }
 
+    /** Returns the total size in bytes of the underlying Arrow buffers. */
+    public long getSizeInBytes() {
+        long size = 0;
+        for (FieldVector vector : vectorSchemaRoot.getFieldVectors()) {
+            for (ArrowBuf buf : vector.getBuffers(false)) {
+                size += buf.readableBytes();
+            }
+        }
+        return size;
+    }
+
     /**
      * Creates a new {@link ArrowBatchData} containing a contiguous slice of 
this batch's rows and
      * releases the original vector data.
@@ -92,12 +106,37 @@ public class ArrowBatchData implements AutoCloseable {
         int remainingRows = getRecordCount() - skipRows;
         VectorSchemaRoot slicedRoot = vectorSchemaRoot.slice(skipRows, 
remainingRows);
         // release original vector buffers; sliced vectors hold independent 
copies
-        vectorSchemaRoot.close();
+        close();
         return new ArrowBatchData(slicedRoot, baseLogOffset + skipRows, 
timestamp, schemaId);
     }
 
+    /**
+     * Creates a new {@link ArrowBatchData} containing only the first {@code 
rowCount} rows and
+     * releases the original vector data.
+     *
+     * <p>After this method returns, the original {@link ArrowBatchData} 
instance MUST NOT be used
+     * or closed. The caller is responsible for closing the returned instance.
+     *
+     * @param rowCount the number of leading rows to keep
+     * @return a new {@link ArrowBatchData} containing the first {@code 
rowCount} rows
+     */
+    public ArrowBatchData truncateAndTransferOwnership(int rowCount) {
+        checkArgument(rowCount > 0, "rowCount must be > 0, but is %s", 
rowCount);
+        checkArgument(
+                rowCount <= getRecordCount(),
+                "rowCount(%s) must be <= recordCount(%s)",
+                rowCount,
+                getRecordCount());
+        VectorSchemaRoot slicedRoot = vectorSchemaRoot.slice(0, rowCount);
+        close();
+        return new ArrowBatchData(slicedRoot, baseLogOffset, timestamp, 
schemaId);
+    }
+
     @Override
     public void close() {
-        vectorSchemaRoot.close();
+        if (!closed) {
+            closed = true;
+            vectorSchemaRoot.close();
+        }
     }
 }
diff --git 
a/fluss-common/src/main/java/org/apache/fluss/utils/UnshadedArrowReadUtils.java 
b/fluss-common/src/main/java/org/apache/fluss/utils/UnshadedArrowReadUtils.java
index dff13f040..5ea16bc17 100644
--- 
a/fluss-common/src/main/java/org/apache/fluss/utils/UnshadedArrowReadUtils.java
+++ 
b/fluss-common/src/main/java/org/apache/fluss/utils/UnshadedArrowReadUtils.java
@@ -54,7 +54,9 @@ public final class UnshadedArrowReadUtils {
     public static Schema toArrowSchema(RowType rowType) {
         
org.apache.fluss.shaded.arrow.org.apache.arrow.vector.types.pojo.Schema 
shadedSchema =
                 ArrowUtils.toArrowSchema(rowType);
-        return 
Schema.deserializeMessage(ByteBuffer.wrap(shadedSchema.serializeAsMessage()));
+        // Use toByteArray()/deserialize() instead of 
serializeAsMessage()/deserializeMessage()
+        // for compatibility with Arrow 12.x (used by Spark 3.4/3.5)
+        return Schema.deserialize(ByteBuffer.wrap(shadedSchema.toByteArray()));
     }
 
     public static void loadArrowBatch(
diff --git 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/tiering/source/TieringSourceReader.java
 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/tiering/source/TieringSourceReader.java
index 6f0fc43b9..3e2fa13fd 100644
--- 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/tiering/source/TieringSourceReader.java
+++ 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/tiering/source/TieringSourceReader.java
@@ -90,11 +90,16 @@ public final class TieringSourceReader<WriteResult>
             LakeTieringFactory<WriteResult, ?> lakeTieringFactory,
             Duration pollTimeout) {
         TieringMetrics tieringMetrics = new 
TieringMetrics(context.metricGroup());
+        ClassLoader userClassLoader = 
context.getUserCodeClassLoader().asClassLoader();
         return new TieringSourceFetcherManager<>(
                 elementsQueue,
                 () ->
                         new TieringSplitReader<>(
-                                connection, lakeTieringFactory, pollTimeout, 
tieringMetrics),
+                                connection,
+                                lakeTieringFactory,
+                                userClassLoader,
+                                pollTimeout,
+                                tieringMetrics),
                 context.getConfiguration(),
                 (ignore) -> {});
     }
diff --git 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/tiering/source/TieringSplitReader.java
 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/tiering/source/TieringSplitReader.java
index 335f5114c..324ce8a9e 100644
--- 
a/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/tiering/source/TieringSplitReader.java
+++ 
b/fluss-flink/fluss-flink-common/src/main/java/org/apache/fluss/flink/tiering/source/TieringSplitReader.java
@@ -21,7 +21,9 @@ import org.apache.fluss.annotation.VisibleForTesting;
 import org.apache.fluss.client.Connection;
 import org.apache.fluss.client.table.Table;
 import org.apache.fluss.client.table.scanner.ScanRecord;
+import org.apache.fluss.client.table.scanner.log.ArrowScanRecords;
 import org.apache.fluss.client.table.scanner.log.LogScanner;
+import org.apache.fluss.client.table.scanner.log.LogScannerImpl;
 import org.apache.fluss.client.table.scanner.log.ScanRecords;
 import org.apache.fluss.flink.source.reader.BoundedSplitReader;
 import org.apache.fluss.flink.source.reader.RecordAndPos;
@@ -29,12 +31,18 @@ import 
org.apache.fluss.flink.tiering.source.metrics.TieringMetrics;
 import org.apache.fluss.flink.tiering.source.split.TieringLogSplit;
 import org.apache.fluss.flink.tiering.source.split.TieringSnapshotSplit;
 import org.apache.fluss.flink.tiering.source.split.TieringSplit;
+import org.apache.fluss.lake.batch.ArrowRecordBatch;
 import org.apache.fluss.lake.writer.LakeTieringFactory;
 import org.apache.fluss.lake.writer.LakeWriter;
+import org.apache.fluss.lake.writer.SupportsRecordBatchWrite;
+import org.apache.fluss.metadata.DataLakeFormat;
+import org.apache.fluss.metadata.LogFormat;
 import org.apache.fluss.metadata.TableBucket;
 import org.apache.fluss.metadata.TableInfo;
 import org.apache.fluss.metadata.TablePath;
+import org.apache.fluss.record.ArrowBatchData;
 import org.apache.fluss.utils.CloseableIterator;
+import org.apache.fluss.utils.function.SupplierWithException;
 
 import org.apache.flink.connector.base.source.reader.RecordsWithSplitIds;
 import org.apache.flink.connector.base.source.reader.splitreader.SplitReader;
@@ -56,6 +64,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Queue;
 import java.util.Set;
+import java.util.function.Function;
 
 import static org.apache.fluss.utils.Preconditions.checkArgument;
 import static org.apache.fluss.utils.Preconditions.checkNotNull;
@@ -98,6 +107,8 @@ public class TieringSplitReader<WriteResult>
     @Nullable private BoundedSplitReader currentSnapshotSplitReader;
     @Nullable private TieringSnapshotSplit currentSnapshotSplit;
     @Nullable private Integer currentTableNumberOfSplits;
+    // whether the current table uses the Arrow record batch path for tiering
+    @Nullable private Boolean currentTableUseRecordBatchPath;
 
     // map from table bucket to split id
     private final Map<TableBucket, TieringSplit> currentTableSplitsByBucket;
@@ -108,18 +119,21 @@ public class TieringSplitReader<WriteResult>
     private final Set<TieringSplit> currentEmptySplits;
 
     private final TieringMetrics tieringMetrics;
+    private final boolean unshadedArrowAvailable;
 
     public TieringSplitReader(
             Connection connection,
             LakeTieringFactory<WriteResult, ?> lakeTieringFactory,
+            ClassLoader userClassLoader,
             TieringMetrics tieringMetrics) {
-        this(connection, lakeTieringFactory, DEFAULT_POLL_TIMEOUT, 
tieringMetrics);
+        this(connection, lakeTieringFactory, userClassLoader, 
DEFAULT_POLL_TIMEOUT, tieringMetrics);
     }
 
     @VisibleForTesting
     protected TieringSplitReader(
             Connection connection,
             LakeTieringFactory<WriteResult, ?> lakeTieringFactory,
+            ClassLoader userClassLoader,
             Duration pollTimeout,
             TieringMetrics tieringMetrics) {
         this.lakeTieringFactory = lakeTieringFactory;
@@ -136,6 +150,7 @@ public class TieringSplitReader<WriteResult>
         this.reachTieringMaxDurationTables = new HashSet<>();
         this.pollTimeout = pollTimeout;
         this.tieringMetrics = tieringMetrics;
+        this.unshadedArrowAvailable = 
checkUnshadedArrowAvailable(userClassLoader);
     }
 
     @Override
@@ -171,8 +186,23 @@ public class TieringSplitReader<WriteResult>
                 if (reachTieringMaxDurationTables.contains(currentTableId)) {
                     return forceCompleteTieringLogRecords();
                 }
-                ScanRecords scanRecords = currentLogScanner.poll(pollTimeout);
-                return forLogRecords(scanRecords);
+                if (useRecordBatchPath()) {
+                    try (ArrowScanRecords arrowScanRecords =
+                            ((LogScannerImpl) 
currentLogScanner).pollRecordBatch(pollTimeout)) {
+                        return processLogRecords(
+                                arrowScanRecords.buckets(),
+                                arrowScanRecords::records,
+                                this::handleArrowBatchRecords,
+                                arrowScanRecords::consumedUpToOffset);
+                    }
+                } else {
+                    ScanRecords scanRecords = 
currentLogScanner.poll(pollTimeout);
+                    return processLogRecords(
+                            scanRecords.buckets(),
+                            scanRecords::records,
+                            this::handleLogRecords,
+                            scanRecords::consumedUpToOffset);
+                }
             } else {
                 return emptyTableBucketWriteResultWithSplitIds();
             }
@@ -350,59 +380,88 @@ public class TieringSplitReader<WriteResult>
         return new TableBucketWriteResultWithSplitIds(writeResults, 
finishedSplitIds);
     }
 
-    private RecordsWithSplitIds<TableBucketWriteResult<WriteResult>> 
forLogRecords(
-            ScanRecords scanRecords) throws IOException {
+    /**
+     * Determines whether the current table should use the Arrow record batch 
path for tiering. The
+     * batch path is used when the table is an ARROW format append-only (log) 
table and the lake
+     * writer supports batch writing.
+     */
+    private boolean useRecordBatchPath() {
+        if (currentTableUseRecordBatchPath != null) {
+            return currentTableUseRecordBatchPath;
+        }
+        TableInfo tableInfo = checkNotNull(currentTable).getTableInfo();
+
+        currentTableUseRecordBatchPath =
+                unshadedArrowAvailable
+                        && !tableInfo.hasPrimaryKey()
+                        && tableInfo.getTableConfig().getLogFormat() == 
LogFormat.ARROW
+                        && 
tableInfo.getTableConfig().getDataLakeFormat().orElse(null)
+                                == DataLakeFormat.PAIMON;
+        return currentTableUseRecordBatchPath;
+    }
+
+    /**
+     * Generic template method for processing tiering log records. 
Encapsulates the shared workflow
+     * of bucket traversal, stopping offset checks, LakeWriter management, 
offset/timestamp
+     * tracking, split completion, and table completion.
+     *
+     * @param buckets the set of buckets that have records
+     * @param recordsExtractor function to extract records for a given bucket
+     * @param handler callback for processing records within a single bucket
+     * @param consumedUpToOffsetExtractor function to extract the 
consumed-up-to offset for a given
+     *     bucket. The offset is used for progress tracking and split 
completion even when records
+     *     are empty.
+     * @param <R> the record type
+     * @return the write results and finished split IDs
+     * @throws IOException if an I/O error occurs during processing
+     */
+    private <R> RecordsWithSplitIds<TableBucketWriteResult<WriteResult>> 
processLogRecords(
+            Set<TableBucket> buckets,
+            Function<TableBucket, List<R>> recordsExtractor,
+            BucketRecordsHandler<R> handler,
+            Function<TableBucket, Long> consumedUpToOffsetExtractor)
+            throws IOException {
         Map<TableBucket, TableBucketWriteResult<WriteResult>> writeResults = 
new HashMap<>();
         Map<TableBucket, String> finishedSplitIds = new HashMap<>();
 
         // Iterate every polled bucket, including those that only advanced 
their offset.
-        for (TableBucket bucket : scanRecords.buckets()) {
+        for (TableBucket bucket : buckets) {
             Long stoppingOffset = currentTableStoppingOffsets.get(bucket);
             if (stoppingOffset == null) {
                 continue;
             }
 
-            List<ScanRecord> records = scanRecords.records(bucket);
-            LakeWriter<WriteResult> lakeWriter = null;
-            ScanRecord lastRecord = null;
-
-            for (ScanRecord record : records) {
-                lastRecord = record;
-
-                // The scanner may return records beyond this split's 
exclusive stopping offset.
-                // Those records belong to the next split and must not be 
tiered here.
-                if (record.logOffset() >= stoppingOffset) {
-                    continue;
-                }
-
-                if (lakeWriter == null) {
-                    lakeWriter =
-                            getOrCreateLakeWriter(
-                                    bucket,
-                                    
currentTableSplitsByBucket.get(bucket).getPartitionName());
-                }
-                lakeWriter.write(record);
-                if (record.getSizeInBytes() > 0) {
-                    tieringMetrics.recordBytesRead(record.getSizeInBytes());
-                }
-            }
+            List<R> records = recordsExtractor.apply(bucket);
 
             // consumedUpToOffset is an exclusive upper bound: all offsets 
before it have been
             // consumed by the scanner in this poll round. It may advance even 
when records is
-            // empty, for example when FIRST_ROW filters duplicate upserts 
into empty WAL batches.
-            Long consumedUpToOffset = scanRecords.consumedUpToOffset(bucket);
+            // empty, e.g. when FIRST_ROW filters duplicate upserts into empty 
WAL batches.
+            Long consumedUpToOffset = 
consumedUpToOffsetExtractor.apply(bucket);
             checkState(
                     consumedUpToOffset != null,
                     "Missing consumed-up-to offset for polled bucket %s.",
                     bucket);
 
+            // Write records to the lake; returns the last written timestamp,
+            // or UNKNOWN_BUCKET_TIMESTAMP if no records were actually written.
+            long lastWrittenTimestamp =
+                    handler.handleRecords(
+                            records,
+                            () ->
+                                    getOrCreateLakeWriter(
+                                            bucket,
+                                            currentTableSplitsByBucket
+                                                    .get(bucket)
+                                                    .getPartitionName()),
+                            stoppingOffset);
+
             // The split owns offsets before stoppingOffset only. If the 
scanner consumed past
             // the split boundary, cap the tiered progress at stoppingOffset 
so the next split
             // still owns later data.
             long tieredLogEndOffset = Math.min(consumedUpToOffset, 
stoppingOffset);
             long tieredTimestamp;
-            if (lastRecord != null) {
-                tieredTimestamp = lastRecord.timestamp();
+            if (lastWrittenTimestamp >= 0) {
+                tieredTimestamp = lastWrittenTimestamp;
             } else {
                 LogOffsetAndTimestamp latest = 
currentTableTieredOffsetAndTimestamp.get(bucket);
                 tieredTimestamp = latest != null ? latest.timestamp : 
UNKNOWN_BUCKET_TIMESTAMP;
@@ -410,12 +469,13 @@ public class TieringSplitReader<WriteResult>
             currentTableTieredOffsetAndTimestamp.put(
                     bucket, new LogOffsetAndTimestamp(tieredLogEndOffset - 1, 
tieredTimestamp));
 
-            // The split owns offsets below stoppingOffset. If the scanner has 
not consumed up to
-            // that exclusive bound yet, keep the split active.
+            // The split owns offsets below stoppingOffset. If the scanner has 
not consumed up
+            // to that exclusive bound yet, keep the split active.
             if (consumedUpToOffset < stoppingOffset) {
                 continue;
             }
 
+            // Split completion: unsubscribe, remove split, complete lake 
writer.
             currentTableStoppingOffsets.remove(bucket);
             if (bucket.getPartitionId() != null) {
                 currentLogScanner.unsubscribe(bucket.getPartitionId(), 
bucket.getBucket());
@@ -447,6 +507,87 @@ public class TieringSplitReader<WriteResult>
         return new TableBucketWriteResultWithSplitIds(writeResults, 
finishedSplitIds);
     }
 
+    /**
+     * Handles row-based ScanRecord writing for the log path.
+     *
+     * @return the timestamp of the last written record, or -1 if no records 
were written
+     */
+    private long handleLogRecords(
+            List<ScanRecord> records,
+            SupplierWithException<LakeWriter<?>, IOException> 
lakeWriterSupplier,
+            long stoppingOffset)
+            throws IOException {
+        long lastWrittenTimestamp = UNKNOWN_BUCKET_TIMESTAMP;
+        LakeWriter<?> lakeWriter = null;
+        for (ScanRecord record : records) {
+            if (record.logOffset() < stoppingOffset) {
+                if (lakeWriter == null) {
+                    lakeWriter = lakeWriterSupplier.get();
+                }
+                lakeWriter.write(record);
+                lastWrittenTimestamp = record.timestamp();
+                if (record.getSizeInBytes() > 0) {
+                    tieringMetrics.recordBytesRead(record.getSizeInBytes());
+                }
+            }
+        }
+        return lastWrittenTimestamp;
+    }
+
+    /**
+     * Handles Arrow batch writing for the record batch path.
+     *
+     * @return the timestamp of the last written batch, or -1 if no batches 
were written
+     */
+    private long handleArrowBatchRecords(
+            List<ArrowBatchData> batches,
+            SupplierWithException<LakeWriter<?>, IOException> 
lakeWriterSupplier,
+            long stoppingOffset)
+            throws IOException {
+        SupportsRecordBatchWrite batchWriter = null;
+        long lastWrittenTimestamp = UNKNOWN_BUCKET_TIMESTAMP;
+        for (ArrowBatchData batch : batches) {
+            long batchBaseOffset = batch.getBaseLogOffset();
+            long batchRecordCount = batch.getRecordCount();
+            long batchTimestamp = batch.getTimestamp();
+            if (batchBaseOffset >= stoppingOffset) {
+                batch.close();
+                continue;
+            }
+
+            long writableRowCount = stoppingOffset - batchBaseOffset;
+            int writableRows = (int) Math.min(batchRecordCount, 
writableRowCount);
+            if (writableRows <= 0) {
+                batch.close();
+                continue;
+            }
+
+            if (batchWriter == null) {
+                LakeWriter<?> lakeWriter = lakeWriterSupplier.get();
+                if (!(lakeWriter instanceof SupportsRecordBatchWrite)) {
+                    throw new IOException(
+                            "LakeWriter does not support RecordBatch writes: "
+                                    + lakeWriter.getClass().getName());
+                }
+                batchWriter = (SupportsRecordBatchWrite) lakeWriter;
+            }
+
+            ArrowBatchData batchToWrite = batch;
+            if (writableRows < batchRecordCount) {
+                batchToWrite = 
batch.truncateAndTransferOwnership(writableRows);
+            }
+            long batchSizeInBytes = batchToWrite.getSizeInBytes();
+            try (ArrowRecordBatch arrowRecordBatch = new 
ArrowRecordBatch(batchToWrite)) {
+                batchWriter.write(arrowRecordBatch);
+            }
+            if (batchSizeInBytes > 0) {
+                tieringMetrics.recordBytesRead(batchSizeInBytes);
+            }
+            lastWrittenTimestamp = batchTimestamp;
+        }
+        return lastWrittenTimestamp;
+    }
+
     private LakeWriter<WriteResult> getOrCreateLakeWriter(
             TableBucket bucket, @Nullable String partitionName) throws 
IOException {
         LakeWriter<WriteResult> lakeWriter = lakeWriters.get(bucket);
@@ -591,6 +732,7 @@ public class TieringSplitReader<WriteResult>
         currentTableId = null;
         currentTablePath = null;
         currentTableNumberOfSplits = null;
+        currentTableUseRecordBatchPath = null;
         currentPendingSnapshotSplits.clear();
         currentTableStoppingOffsets.clear();
         currentTableTieredOffsetAndTimestamp.clear();
@@ -732,6 +874,41 @@ public class TieringSplitReader<WriteResult>
         }
     }
 
+    /**
+     * Callback interface for processing records within a single bucket. 
Encapsulates the
+     * differences in write strategy between the row-based (ScanRecord) and 
Arrow batch
+     * (ArrowBatchData) paths.
+     *
+     * @param <R> the record type (ScanRecord or ArrowBatchData)
+     */
+    @FunctionalInterface
+    private interface BucketRecordsHandler<R> {
+
+        /**
+         * Processes the records for a bucket and writes them to the lake.
+         *
+         * @param records the records for this bucket
+         * @param lakeWriterSupplier supplier for lazily creating the lake 
writer
+         * @param stoppingOffset the stopping offset for this bucket
+         * @return the timestamp of the last written record, or -1 if no 
records were written
+         * @throws IOException if an I/O error occurs during writing
+         */
+        long handleRecords(
+                List<R> records,
+                SupplierWithException<LakeWriter<?>, IOException> 
lakeWriterSupplier,
+                long stoppingOffset)
+                throws IOException;
+    }
+
+    private static boolean checkUnshadedArrowAvailable(ClassLoader 
classLoader) {
+        try {
+            Class.forName("org.apache.arrow.vector.VectorSchemaRoot", false, 
classLoader);
+            return true;
+        } catch (ClassNotFoundException e) {
+            return false;
+        }
+    }
+
     private static final class LogOffsetAndTimestamp {
 
         private final long logOffset;
diff --git 
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/tiering/source/TieringSplitReaderTest.java
 
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/tiering/source/TieringSplitReaderTest.java
index 9aad5d1c3..91c65ffc7 100644
--- 
a/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/tiering/source/TieringSplitReaderTest.java
+++ 
b/fluss-flink/fluss-flink-common/src/test/java/org/apache/fluss/flink/tiering/source/TieringSplitReaderTest.java
@@ -431,7 +431,10 @@ class TieringSplitReaderTest extends FlinkTestBase {
                         InternalSourceReaderMetricGroup.mock(
                                 new MetricListener().getMetricGroup()));
         return new TieringSplitReader<>(
-                connection, new TestingLakeTieringFactory(), tieringMetrics);
+                connection,
+                new TestingLakeTieringFactory(),
+                Thread.currentThread().getContextClassLoader(),
+                tieringMetrics);
     }
 
     private TieringSplitReader<TestingWriteResult> createTieringReader(
@@ -440,7 +443,11 @@ class TieringSplitReaderTest extends FlinkTestBase {
                 new TieringMetrics(
                         InternalSourceReaderMetricGroup.mock(
                                 new MetricListener().getMetricGroup()));
-        return new TieringSplitReader<>(connection, lakeTieringFactory, 
tieringMetrics);
+        return new TieringSplitReader<>(
+                connection,
+                lakeTieringFactory,
+                Thread.currentThread().getContextClassLoader(),
+                tieringMetrics);
     }
 
     private void verifyTieringRows(
diff --git a/fluss-lake/fluss-lake-paimon/pom.xml 
b/fluss-lake/fluss-lake-paimon/pom.xml
index 1deb0fd65..a1e6d1abc 100644
--- a/fluss-lake/fluss-lake-paimon/pom.xml
+++ b/fluss-lake/fluss-lake-paimon/pom.xml
@@ -51,6 +51,31 @@
             <scope>provided</scope>
         </dependency>
 
+        <!-- paimon-arrow and Arrow dependencies are provided because they are 
only needed
+             at runtime when the tiering service writes Arrow record batches 
to Paimon.
+             The Flink tiering plugin bundles these jars; fluss-lake-paimon 
itself is a
+             lightweight module that should not pull them transitively. -->
+        <dependency>
+            <groupId>org.apache.paimon</groupId>
+            <artifactId>paimon-arrow</artifactId>
+            <version>${paimon.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.arrow</groupId>
+            <artifactId>arrow-vector</artifactId>
+            <version>${arrow.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.arrow</groupId>
+            <artifactId>arrow-memory-netty</artifactId>
+            <version>${arrow.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.fluss</groupId>
             <artifactId>fluss-client</artifactId>
diff --git 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/tiering/PaimonLakeWriter.java
 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/tiering/PaimonLakeWriter.java
index 8ac7aabe4..5c2738e23 100644
--- 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/tiering/PaimonLakeWriter.java
+++ 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/tiering/PaimonLakeWriter.java
@@ -17,9 +17,12 @@
 
 package org.apache.fluss.lake.paimon.tiering;
 
+import org.apache.fluss.lake.batch.ArrowRecordBatch;
+import org.apache.fluss.lake.batch.RecordBatch;
 import org.apache.fluss.lake.paimon.tiering.append.AppendOnlyWriter;
 import org.apache.fluss.lake.paimon.tiering.mergetree.MergeTreeWriter;
 import org.apache.fluss.lake.writer.LakeWriter;
+import org.apache.fluss.lake.writer.SupportsRecordBatchWrite;
 import org.apache.fluss.lake.writer.WriterInitContext;
 import org.apache.fluss.metadata.TablePath;
 import org.apache.fluss.record.LogRecord;
@@ -38,7 +41,7 @@ import java.util.Map;
 import static org.apache.fluss.lake.paimon.utils.PaimonConversions.toPaimon;
 
 /** Implementation of {@link LakeWriter} for Paimon. */
-public class PaimonLakeWriter implements LakeWriter<PaimonWriteResult> {
+public class PaimonLakeWriter implements LakeWriter<PaimonWriteResult>, 
SupportsRecordBatchWrite {
 
     private final Catalog paimonCatalog;
     private final RecordWriter<?> recordWriter;
@@ -80,6 +83,25 @@ public class PaimonLakeWriter implements 
LakeWriter<PaimonWriteResult> {
         }
     }
 
+    @Override
+    public void write(RecordBatch recordBatch) throws IOException {
+        if (!(recordBatch instanceof ArrowRecordBatch)) {
+            throw new IllegalArgumentException(
+                    "PaimonLakeWriter only supports ArrowRecordBatch, but got "
+                            + recordBatch.getClass().getSimpleName());
+        }
+        if (!(recordWriter instanceof AppendOnlyWriter)) {
+            throw new IllegalStateException(
+                    "Arrow record batch writing is only supported for 
append-only tables.");
+        }
+        try {
+            ((AppendOnlyWriter) recordWriter)
+                    .writeArrowBatch(((ArrowRecordBatch) 
recordBatch).getArrowBatchData());
+        } catch (Exception e) {
+            throw new IOException("Failed to write Arrow record batch to 
Paimon.", e);
+        }
+    }
+
     @Override
     public PaimonWriteResult complete() throws IOException {
         CommitMessage commitMessage;
diff --git 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/tiering/append/AppendOnlyArrowBatchHelper.java
 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/tiering/append/AppendOnlyArrowBatchHelper.java
new file mode 100644
index 000000000..4fdb8bce7
--- /dev/null
+++ 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/tiering/append/AppendOnlyArrowBatchHelper.java
@@ -0,0 +1,230 @@
+/*
+ * 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.fluss.lake.paimon.tiering.append;
+
+import org.apache.fluss.metadata.TableDescriptor;
+import org.apache.fluss.record.ArrowBatchData;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.IntVector;
+import org.apache.arrow.vector.TimeStampMilliVector;
+import org.apache.arrow.vector.VectorSchemaRoot;
+import org.apache.arrow.vector.types.TimeUnit;
+import org.apache.arrow.vector.types.pojo.ArrowType;
+import org.apache.arrow.vector.types.pojo.Field;
+import org.apache.arrow.vector.types.pojo.FieldType;
+import org.apache.arrow.vector.types.pojo.Schema;
+import org.apache.paimon.arrow.ArrowBundleRecords;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.table.BucketMode;
+import org.apache.paimon.table.FileStoreTable;
+import org.apache.paimon.table.sink.TableWriteImpl;
+import org.apache.paimon.types.RowType;
+
+import javax.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class that encapsulates Arrow-dependent batch writing logic for 
append-only tables.
+ *
+ * <p>This class is separated from {@link AppendOnlyWriter} to avoid loading 
Arrow classes when
+ * Arrow is not on the classpath. It is lazily loaded only when Arrow batch 
writing is actually
+ * needed.
+ */
+class AppendOnlyArrowBatchHelper implements AutoCloseable {
+
+    private final FileStoreTable fileStoreTable;
+    private final TableWriteImpl<InternalRow> tableWrite;
+    private final RowType tableRowType;
+    private final int bucket;
+
+    private static final Field BUCKET_FIELD =
+            new Field(
+                    TableDescriptor.BUCKET_COLUMN_NAME,
+                    new FieldType(false, new ArrowType.Int(32, true), null),
+                    null);
+    private static final Field OFFSET_FIELD =
+            new Field(
+                    TableDescriptor.OFFSET_COLUMN_NAME,
+                    new FieldType(false, new ArrowType.Int(64, true), null),
+                    null);
+    private static final Field TIMESTAMP_FIELD =
+            new Field(
+                    TableDescriptor.TIMESTAMP_COLUMN_NAME,
+                    new FieldType(false, new 
ArrowType.Timestamp(TimeUnit.MILLISECOND, null), null),
+                    null);
+
+    // Child allocator for system column vectors, sharing the same root as the 
batch allocator
+    @Nullable private BufferAllocator systemColumnAllocator;
+
+    // Reusable resources for enriched VectorSchemaRoot with system columns
+    @Nullable private VectorSchemaRoot enrichedRoot;
+    @Nullable private Schema enrichedSchema;
+    @Nullable private IntVector bucketVector;
+    @Nullable private BigIntVector offsetVector;
+    @Nullable private TimeStampMilliVector timestampVector;
+
+    AppendOnlyArrowBatchHelper(
+            FileStoreTable fileStoreTable,
+            TableWriteImpl<InternalRow> tableWrite,
+            RowType tableRowType,
+            int bucket) {
+        this.fileStoreTable = fileStoreTable;
+        this.tableWrite = tableWrite;
+        this.tableRowType = tableRowType;
+        this.bucket = bucket;
+    }
+
+    /**
+     * Writes an Arrow batch directly to Paimon Parquet files. Enriches the 
VectorSchemaRoot with
+     * system columns (__bucket, __offset, __timestamp) and uses Paimon's 
{@link ArrowBundleRecords}
+     * for efficient batch writing.
+     */
+    void writeArrowBatch(ArrowBatchData arrowBatchData, BinaryRow partition) 
throws Exception {
+        int writtenBucket = bucket;
+        if (fileStoreTable.store().bucketMode() == BucketMode.BUCKET_UNAWARE) {
+            writtenBucket = 0;
+        }
+
+        VectorSchemaRoot originalRoot = arrowBatchData.getVectorSchemaRoot();
+        long baseOffset = arrowBatchData.getBaseLogOffset();
+        long timestamp = arrowBatchData.getTimestamp();
+        int rowCount = originalRoot.getRowCount();
+
+        ensureEnrichedRootInitialized(originalRoot, 
originalRoot.getVector(0).getAllocator());
+        updateEnrichedVectorSchemaRoot(writtenBucket, baseOffset, timestamp, 
rowCount);
+
+        ArrowBundleRecords arrowBundleRecords =
+                new ArrowBundleRecords(enrichedRoot, tableRowType, false);
+
+        tableWrite.writeBundle(partition, writtenBucket, arrowBundleRecords);
+    }
+
+    /**
+     * Ensures the enriched VectorSchemaRoot is initialized with system column 
vectors. Reuses
+     * system column vectors when the root allocator has not changed. The 
enrichedRoot references
+     * the current originalRoot's data vectors plus the system column vectors.
+     *
+     * <p>System column vectors must share the same root allocator as the data 
vectors. Batches from
+     * different poll rounds may use different root allocators (each {@code 
CompletedFetch} creates
+     * its own {@code LogRecordReadContext} with a fresh {@code 
RootAllocator}), so the system
+     * column vectors are recreated when the root allocator changes.
+     */
+    private void ensureEnrichedRootInitialized(
+            VectorSchemaRoot originalRoot, BufferAllocator batchAllocator) {
+        List<Field> originalFields = originalRoot.getSchema().getFields();
+        int currentFieldCount = originalFields.size();
+
+        BufferAllocator currentRoot = batchAllocator.getRoot();
+
+        // (Re)create system column vectors when the root allocator has 
changed.
+        if (bucketVector == null || systemColumnAllocator.getRoot() != 
currentRoot) {
+            closeSystemColumns();
+
+            systemColumnAllocator =
+                    currentRoot.newChildAllocator("system-column-allocator", 
0, Long.MAX_VALUE);
+            bucketVector = new IntVector(BUCKET_FIELD, systemColumnAllocator);
+            offsetVector = new BigIntVector(OFFSET_FIELD, 
systemColumnAllocator);
+            timestampVector = new TimeStampMilliVector(TIMESTAMP_FIELD, 
systemColumnAllocator);
+        }
+
+        if (enrichedSchema == null) {
+            List<Field> enrichedFields = new ArrayList<>(originalFields);
+            enrichedFields.add(BUCKET_FIELD);
+            enrichedFields.add(OFFSET_FIELD);
+            enrichedFields.add(TIMESTAMP_FIELD);
+            enrichedSchema = new Schema(enrichedFields);
+        }
+
+        // recreate enrichedRoot to reference the current originalRoot's data 
vectors
+        List<FieldVector> allVectors = new ArrayList<>();
+        for (int i = 0; i < currentFieldCount; i++) {
+            allVectors.add(originalRoot.getVector(i));
+        }
+        allVectors.add(bucketVector);
+        allVectors.add(offsetVector);
+        allVectors.add(timestampVector);
+
+        enrichedRoot = new VectorSchemaRoot(enrichedSchema, allVectors, 
originalRoot.getRowCount());
+    }
+
+    private void closeSystemColumns() {
+        if (bucketVector != null) {
+            bucketVector.close();
+            bucketVector = null;
+        }
+        if (offsetVector != null) {
+            offsetVector.close();
+            offsetVector = null;
+        }
+        if (timestampVector != null) {
+            timestampVector.close();
+            timestampVector = null;
+        }
+        if (systemColumnAllocator != null) {
+            systemColumnAllocator.close();
+            systemColumnAllocator = null;
+        }
+    }
+
+    /**
+     * Updates system column values in the enriched VectorSchemaRoot. Data 
columns are already
+     * referenced from the original root.
+     */
+    private void updateEnrichedVectorSchemaRoot(
+            int bucket, long baseOffset, long timestamp, int rowCount) {
+        enrichedRoot.setRowCount(rowCount);
+
+        if (bucketVector.getValueCapacity() < rowCount) {
+            bucketVector.allocateNew(rowCount);
+        }
+        if (offsetVector.getValueCapacity() < rowCount) {
+            offsetVector.allocateNew(rowCount);
+        }
+        if (timestampVector.getValueCapacity() < rowCount) {
+            timestampVector.allocateNew(rowCount);
+        }
+
+        for (int i = 0; i < rowCount; i++) {
+            bucketVector.set(i, bucket);
+        }
+        bucketVector.setValueCount(rowCount);
+
+        for (int i = 0; i < rowCount; i++) {
+            offsetVector.set(i, baseOffset + i);
+        }
+        offsetVector.setValueCount(rowCount);
+
+        for (int i = 0; i < rowCount; i++) {
+            timestampVector.set(i, timestamp);
+        }
+        timestampVector.setValueCount(rowCount);
+    }
+
+    @Override
+    public void close() {
+        closeSystemColumns();
+        enrichedRoot = null;
+        enrichedSchema = null;
+    }
+}
diff --git 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/tiering/append/AppendOnlyWriter.java
 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/tiering/append/AppendOnlyWriter.java
index a0966dfd4..0caaed97c 100644
--- 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/tiering/append/AppendOnlyWriter.java
+++ 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/fluss/lake/paimon/tiering/append/AppendOnlyWriter.java
@@ -19,6 +19,7 @@ package org.apache.fluss.lake.paimon.tiering.append;
 
 import org.apache.fluss.lake.paimon.tiering.RecordWriter;
 import org.apache.fluss.metadata.TableBucket;
+import org.apache.fluss.record.ArrowBatchData;
 import org.apache.fluss.record.LogRecord;
 import org.apache.fluss.types.RowType;
 
@@ -38,6 +39,13 @@ public class AppendOnlyWriter extends 
RecordWriter<InternalRow> {
 
     private final FileStoreTable fileStoreTable;
 
+    /**
+     * Lazily-initialized helper for Arrow batch writing. Stored as {@link 
AutoCloseable} to avoid
+     * loading Arrow classes when Arrow is not on the classpath. The actual 
type is {@link
+     * AppendOnlyArrowBatchHelper} which is only loaded when {@link 
#writeArrowBatch} is called.
+     */
+    @Nullable private AutoCloseable arrowBatchHelper;
+
     public AppendOnlyWriter(
             FileStoreTable fileStoreTable,
             TableBucket tableBucket,
@@ -71,4 +79,31 @@ public class AppendOnlyWriter extends 
RecordWriter<InternalRow> {
         }
         tableWrite.getWrite().write(partition, writtenBucket, 
flussRecordAsPaimonRow);
     }
+
+    /**
+     * Writes an Arrow batch directly to Paimon Parquet files. Delegates to 
{@link
+     * AppendOnlyArrowBatchHelper} which is lazily loaded to avoid class 
loading issues when Arrow
+     * is not on the classpath.
+     */
+    public void writeArrowBatch(ArrowBatchData arrowBatchData) throws 
Exception {
+        AppendOnlyArrowBatchHelper helper;
+        if (arrowBatchHelper == null) {
+            helper =
+                    new AppendOnlyArrowBatchHelper(
+                            fileStoreTable, tableWrite, tableRowType, bucket);
+            arrowBatchHelper = helper;
+        } else {
+            helper = (AppendOnlyArrowBatchHelper) arrowBatchHelper;
+        }
+        helper.writeArrowBatch(arrowBatchData, partition);
+    }
+
+    @Override
+    public void close() throws Exception {
+        if (arrowBatchHelper != null) {
+            arrowBatchHelper.close();
+            arrowBatchHelper = null;
+        }
+        super.close();
+    }
 }
diff --git 
a/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/paimon/arrow/converter/Arrow2PaimonVectorConverter.java
 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/paimon/arrow/converter/Arrow2PaimonVectorConverter.java
new file mode 100644
index 000000000..a8e395725
--- /dev/null
+++ 
b/fluss-lake/fluss-lake-paimon/src/main/java/org/apache/paimon/arrow/converter/Arrow2PaimonVectorConverter.java
@@ -0,0 +1,630 @@
+/*
+ * 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.arrow.converter;
+
+import org.apache.arrow.vector.BigIntVector;
+import org.apache.arrow.vector.BitVector;
+import org.apache.arrow.vector.DateDayVector;
+import org.apache.arrow.vector.DecimalVector;
+import org.apache.arrow.vector.FieldVector;
+import org.apache.arrow.vector.FixedSizeBinaryVector;
+import org.apache.arrow.vector.Float4Vector;
+import org.apache.arrow.vector.Float8Vector;
+import org.apache.arrow.vector.IntVector;
+import org.apache.arrow.vector.SmallIntVector;
+import org.apache.arrow.vector.TimeMicroVector;
+import org.apache.arrow.vector.TimeMilliVector;
+import org.apache.arrow.vector.TimeNanoVector;
+import org.apache.arrow.vector.TimeSecVector;
+import org.apache.arrow.vector.TimeStampVector;
+import org.apache.arrow.vector.TinyIntVector;
+import org.apache.arrow.vector.VarBinaryVector;
+import org.apache.arrow.vector.VarCharVector;
+import org.apache.arrow.vector.complex.ListVector;
+import org.apache.arrow.vector.complex.StructVector;
+import org.apache.paimon.data.Decimal;
+import org.apache.paimon.data.InternalArray;
+import org.apache.paimon.data.InternalMap;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.data.columnar.ArrayColumnVector;
+import org.apache.paimon.data.columnar.BooleanColumnVector;
+import org.apache.paimon.data.columnar.ByteColumnVector;
+import org.apache.paimon.data.columnar.BytesColumnVector;
+import org.apache.paimon.data.columnar.ColumnVector;
+import org.apache.paimon.data.columnar.ColumnarArray;
+import org.apache.paimon.data.columnar.ColumnarMap;
+import org.apache.paimon.data.columnar.ColumnarRow;
+import org.apache.paimon.data.columnar.DecimalColumnVector;
+import org.apache.paimon.data.columnar.DoubleColumnVector;
+import org.apache.paimon.data.columnar.FloatColumnVector;
+import org.apache.paimon.data.columnar.IntColumnVector;
+import org.apache.paimon.data.columnar.LongColumnVector;
+import org.apache.paimon.data.columnar.MapColumnVector;
+import org.apache.paimon.data.columnar.RowColumnVector;
+import org.apache.paimon.data.columnar.ShortColumnVector;
+import org.apache.paimon.data.columnar.TimestampColumnVector;
+import org.apache.paimon.data.columnar.VectorizedColumnBatch;
+import org.apache.paimon.types.ArrayType;
+import org.apache.paimon.types.BigIntType;
+import org.apache.paimon.types.BinaryType;
+import org.apache.paimon.types.BooleanType;
+import org.apache.paimon.types.CharType;
+import org.apache.paimon.types.DataField;
+import org.apache.paimon.types.DataType;
+import org.apache.paimon.types.DataTypeVisitor;
+import org.apache.paimon.types.DateType;
+import org.apache.paimon.types.DecimalType;
+import org.apache.paimon.types.DoubleType;
+import org.apache.paimon.types.FloatType;
+import org.apache.paimon.types.IntType;
+import org.apache.paimon.types.LocalZonedTimestampType;
+import org.apache.paimon.types.MapType;
+import org.apache.paimon.types.MultisetType;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.types.SmallIntType;
+import org.apache.paimon.types.TimeType;
+import org.apache.paimon.types.TimestampType;
+import org.apache.paimon.types.TinyIntType;
+import org.apache.paimon.types.VarBinaryType;
+import org.apache.paimon.types.VarCharType;
+import org.apache.paimon.types.VariantType;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/* This file is based on source code of Apache Paimon Project 
(https://paimon.apache.org/), licensed by the Apache
+ * Software Foundation (ASF) under the Apache License, Version 2.0. See the 
NOTICE file distributed with this work for
+ * additional information regarding copyright ownership. */
+
+/**
+ * Convert a {@link FieldVector} to {@link ColumnVector}.
+ *
+ * <p>This is copied from Paimon's {@code Arrow2PaimonVectorConverter} 
(paimon-arrow) with the
+ * following bug fixes:
+ *
+ * <ul>
+ *   <li>{@code TimestampType} and {@code LocalZonedTimestampType}: read the 
raw long value via
+ *       {@code vector.getDataBuffer().getLong()} instead of {@code 
((TimeStampVector)
+ *       vector).get(i)} or {@code (long) vector.getObject(i)}, which may 
return {@code
+ *       LocalDateTime} and cause {@code ClassCastException} for 
timezone-unaware timestamp vectors.
+ *   <li>{@code TimeType}: handle all Arrow time vector types ({@code 
TimeMilliVector}, {@code
+ *       TimeMicroVector}, {@code TimeNanoVector}, {@code TimeSecVector}) 
instead of hardcoding
+ *       {@code TimeMilliVector}.
+ *   <li>{@code BinaryType}: use {@code FixedSizeBinaryVector} instead of 
{@code VarBinaryVector} to
+ *       match the fixed-length binary Arrow type.
+ * </ul>
+ *
+ * <p>TODO: remove this class once 
https://github.com/apache/paimon/issues/8134 is fixed in upstream
+ * Paimon.
+ */
+public interface Arrow2PaimonVectorConverter {
+
+    static Arrow2PaimonVectorConverter construct(DataType type) {
+        return type.accept(Arrow2PaimonVectorConvertorVisitor.INSTANCE);
+    }
+
+    static Arrow2PaimonVectorConverter construct(
+            Arrow2PaimonVectorConvertorVisitor visitor, DataType type) {
+        return type.accept(visitor);
+    }
+
+    ColumnVector convertVector(FieldVector vector);
+
+    /** Visitor to create convertor from arrow to paimon. */
+    class Arrow2PaimonVectorConvertorVisitor
+            implements DataTypeVisitor<Arrow2PaimonVectorConverter> {
+
+        public static final Arrow2PaimonVectorConvertorVisitor INSTANCE =
+                new Arrow2PaimonVectorConvertorVisitor();
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(CharType charType) {
+            return vector ->
+                    new BytesColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public Bytes getBytes(int index) {
+                            byte[] bytes;
+                            if (vector instanceof FixedSizeBinaryVector) {
+                                bytes = ((FixedSizeBinaryVector) 
vector).get(index);
+                            } else {
+                                bytes = ((VarCharVector) vector).get(index);
+                            }
+
+                            return new Bytes(bytes, 0, bytes.length) {
+                                @Override
+                                public byte[] getBytes() {
+                                    return bytes;
+                                }
+                            };
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(VarCharType varCharType) {
+            return vector ->
+                    new BytesColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public Bytes getBytes(int index) {
+                            byte[] bytes = ((VarCharVector) vector).get(index);
+                            return new Bytes(bytes, 0, bytes.length) {
+                                @Override
+                                public byte[] getBytes() {
+                                    return bytes;
+                                }
+                            };
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(BooleanType booleanType) {
+            return vector ->
+                    new BooleanColumnVector() {
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public boolean getBoolean(int index) {
+                            return ((BitVector) vector).getObject(index);
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(BinaryType binaryType) {
+            return vector ->
+                    new BytesColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public Bytes getBytes(int index) {
+                            byte[] bytes = ((FixedSizeBinaryVector) 
vector).get(index);
+                            return new Bytes(bytes, 0, bytes.length) {
+                                @Override
+                                public byte[] getBytes() {
+                                    return bytes;
+                                }
+                            };
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(VarBinaryType varBinaryType) {
+            return vector ->
+                    new BytesColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public Bytes getBytes(int index) {
+                            byte[] bytes = ((VarBinaryVector) 
vector).get(index);
+                            return new Bytes(bytes, 0, bytes.length) {
+                                @Override
+                                public byte[] getBytes() {
+                                    return bytes;
+                                }
+                            };
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(DecimalType decimalType) {
+            return vector ->
+                    new DecimalColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public Decimal getDecimal(int index, int precision, 
int scale) {
+                            return Decimal.fromBigDecimal(
+                                    ((DecimalVector) vector).getObject(index), 
precision, scale);
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(TinyIntType tinyIntType) {
+            return vector ->
+                    new ByteColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public byte getByte(int index) {
+                            return ((TinyIntVector) vector).getObject(index);
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(SmallIntType smallIntType) {
+            return vector ->
+                    new ShortColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public short getShort(int index) {
+                            return ((SmallIntVector) vector).getObject(index);
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(IntType intType) {
+            return vector ->
+                    new IntColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public int getInt(int index) {
+                            return ((IntVector) vector).getObject(index);
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(BigIntType bigIntType) {
+            return vector ->
+                    new LongColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public long getLong(int index) {
+                            return ((BigIntVector) vector).getObject(index);
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(FloatType floatType) {
+            return vector ->
+                    new FloatColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public float getFloat(int index) {
+                            return ((Float4Vector) vector).getObject(index);
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(DoubleType doubleType) {
+            return vector ->
+                    new DoubleColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public double getDouble(int index) {
+                            return ((Float8Vector) vector).getObject(index);
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(DateType dateType) {
+            return vector ->
+                    new IntColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public int getInt(int index) {
+                            return ((DateDayVector) vector).getObject(index);
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(TimeType timeType) {
+            // Paimon stores TIME as milliseconds int. Arrow may use different 
units
+            // depending on precision, so convert to millis accordingly.
+            return vector ->
+                    new IntColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public int getInt(int index) {
+                            if (vector instanceof TimeMilliVector) {
+                                return ((TimeMilliVector) vector).get(index);
+                            } else if (vector instanceof TimeMicroVector) {
+                                return (int) (((TimeMicroVector) 
vector).get(index) / 1_000);
+                            } else if (vector instanceof TimeNanoVector) {
+                                return (int) (((TimeNanoVector) 
vector).get(index) / 1_000_000);
+                            } else if (vector instanceof TimeSecVector) {
+                                return ((TimeSecVector) vector).get(index) * 
1_000;
+                            }
+                            throw new UnsupportedOperationException(
+                                    "Unsupported Arrow time vector type: "
+                                            + vector.getClass().getName());
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(TimestampType timestampType) {
+            return vector ->
+                    new TimestampColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public Timestamp getTimestamp(int i, int precision) {
+                            long value =
+                                    vector.getDataBuffer()
+                                            .getLong((long) i * 
TimeStampVector.TYPE_WIDTH);
+                            if (precision == 0) {
+                                return Timestamp.fromEpochMillis(value * 1000);
+                            } else if (precision >= 1 && precision <= 3) {
+                                return Timestamp.fromEpochMillis(value);
+                            } else if (precision >= 4 && precision <= 6) {
+                                return Timestamp.fromMicros(value);
+                            } else {
+                                return Timestamp.fromEpochMillis(
+                                        value / 1_000_000, (int) (value % 
1_000_000));
+                            }
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(LocalZonedTimestampType 
localZonedTimestampType) {
+            return vector ->
+                    new TimestampColumnVector() {
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public Timestamp getTimestamp(int i, int precision) {
+                            long value =
+                                    vector.getDataBuffer()
+                                            .getLong((long) i * 
TimeStampVector.TYPE_WIDTH);
+                            if (precision == 0) {
+                                return Timestamp.fromEpochMillis(value * 1000);
+                            } else if (precision >= 1 && precision <= 3) {
+                                return Timestamp.fromEpochMillis(value);
+                            } else if (precision >= 4 && precision <= 6) {
+                                return Timestamp.fromMicros(value);
+                            } else {
+                                return Timestamp.fromEpochMillis(
+                                        value / 1_000_000, (int) (value % 
1_000_000));
+                            }
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(VariantType variantType) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(ArrayType arrayType) {
+            final Arrow2PaimonVectorConverter arrowVectorConvertor =
+                    arrayType.getElementType().accept(this);
+
+            return vector ->
+                    new ArrayColumnVector() {
+
+                        private boolean inited = false;
+                        private ColumnVector columnVector;
+
+                        private void init() {
+                            if (!inited) {
+                                FieldVector child = ((ListVector) 
vector).getDataVector();
+                                this.columnVector = 
arrowVectorConvertor.convertVector(child);
+                                inited = true;
+                            }
+                        }
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public InternalArray getArray(int index) {
+                            init();
+                            ListVector listVector = (ListVector) vector;
+                            int start = listVector.getElementStartIndex(index);
+                            int end = listVector.getElementEndIndex(index);
+                            return new ColumnarArray(columnVector, start, end 
- start);
+                        }
+
+                        @Override
+                        public ColumnVector getColumnVector() {
+                            init();
+                            return columnVector;
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(MultisetType multisetType) {
+            throw new UnsupportedOperationException("Doesn't support 
MultisetType.");
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(MapType mapType) {
+            final Arrow2PaimonVectorConverter keyConvertor = 
mapType.getKeyType().accept(this);
+            final Arrow2PaimonVectorConverter valueConverter = 
mapType.getValueType().accept(this);
+
+            return vector ->
+                    new MapColumnVector() {
+
+                        private boolean inited = false;
+                        private ListVector mapVector;
+                        private ColumnVector keyColumnVector;
+                        private ColumnVector valueColumnVector;
+
+                        private void init() {
+                            if (!inited) {
+                                this.mapVector = (ListVector) vector;
+                                StructVector listVector = (StructVector) 
mapVector.getDataVector();
+
+                                FieldVector keyVector = 
listVector.getChildrenFromFields().get(0);
+                                FieldVector valueVector = 
listVector.getChildrenFromFields().get(1);
+
+                                this.keyColumnVector = 
keyConvertor.convertVector(keyVector);
+                                this.valueColumnVector = 
valueConverter.convertVector(valueVector);
+                                inited = true;
+                            }
+                        }
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public InternalMap getMap(int index) {
+                            init();
+
+                            int start = mapVector.getElementStartIndex(index);
+                            int end = mapVector.getElementEndIndex(index);
+
+                            return new ColumnarMap(
+                                    keyColumnVector, valueColumnVector, start, 
end - start);
+                        }
+
+                        @Override
+                        public ColumnVector[] getChildren() {
+                            return new ColumnVector[] {keyColumnVector, 
valueColumnVector};
+                        }
+                    };
+        }
+
+        @Override
+        public Arrow2PaimonVectorConverter visit(RowType rowType) {
+            final List<Arrow2PaimonVectorConverter> convertors = new 
ArrayList<>();
+            final List<String> names = new ArrayList<>();
+            List<DataField> fields = rowType.getFields();
+            for (int i = 0; i < rowType.getFields().size(); i++) {
+                convertors.add(rowType.getTypeAt(i).accept(this));
+                names.add(fields.get(i).name());
+            }
+
+            return vector ->
+                    new RowColumnVector() {
+
+                        private boolean inited = false;
+                        private VectorizedColumnBatch vectorizedColumnBatch;
+
+                        private void init() {
+                            if (!inited) {
+                                List<FieldVector> children =
+                                        ((StructVector) 
vector).getChildrenFromFields();
+                                ColumnVector[] vectors = new 
ColumnVector[convertors.size()];
+
+                                for (FieldVector child : children) {
+                                    int index = names.indexOf(child.getName());
+                                    if (index != -1) {
+                                        vectors[index] = 
convertors.get(index).convertVector(child);
+                                    }
+                                }
+
+                                this.vectorizedColumnBatch = new 
VectorizedColumnBatch(vectors);
+                                inited = true;
+                            }
+                        }
+
+                        @Override
+                        public boolean isNullAt(int index) {
+                            return vector.isNull(index);
+                        }
+
+                        @Override
+                        public InternalRow getRow(int index) {
+                            init();
+                            return new ColumnarRow(vectorizedColumnBatch, 
index);
+                        }
+
+                        @Override
+                        public VectorizedColumnBatch getBatch() {
+                            init();
+                            return vectorizedColumnBatch;
+                        }
+                    };
+        }
+    }
+}
diff --git 
a/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/tiering/PaimonTieringITCase.java
 
b/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/tiering/PaimonTieringITCase.java
index bf92cd5f5..3d4da4fe5 100644
--- 
a/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/tiering/PaimonTieringITCase.java
+++ 
b/fluss-lake/fluss-lake-paimon/src/test/java/org/apache/fluss/lake/paimon/tiering/PaimonTieringITCase.java
@@ -366,9 +366,9 @@ class PaimonTieringITCase extends 
FlinkPaimonTieringTestBase {
             for (int i = 0; i < 10; i++) {
                 rows = Arrays.asList(row(1, "v1"), row(2, "v2"), row(3, "v3"));
                 flussRows.addAll(rows);
-                // write records
-                writeRows(t2, rows, true);
             }
+            // write records
+            writeRows(t2, flussRows, true);
             // check the status of replica after synced;
             // note: we can't update log start offset for unaware bucket mode 
log table
             assertReplicaStatus(t2Bucket, 30);
diff --git a/fluss-spark/fluss-spark-ut/pom.xml 
b/fluss-spark/fluss-spark-ut/pom.xml
index c565f22db..3e2de2761 100644
--- a/fluss-spark/fluss-spark-ut/pom.xml
+++ b/fluss-spark/fluss-spark-ut/pom.xml
@@ -95,6 +95,13 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.paimon</groupId>
+            <artifactId>paimon-arrow</artifactId>
+            <version>${paimon.version}</version>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.apache.fluss</groupId>
             <artifactId>fluss-lake-iceberg</artifactId>
diff --git a/fluss-test-coverage/pom.xml b/fluss-test-coverage/pom.xml
index 944378b00..38aeaa989 100644
--- a/fluss-test-coverage/pom.xml
+++ b/fluss-test-coverage/pom.xml
@@ -454,6 +454,7 @@
                                         
<exclude>org.apache.fluss.lake.batch.ArrowRecordBatch</exclude>
                                         
<exclude>org.apache.fluss.lake.committer.CommittedLakeSnapshot</exclude>
                                         
<exclude>org.apache.fluss.lake.paimon.utils.FlussDataTypeToPaimonDataType</exclude>
+                                        
<exclude>org.apache.paimon.arrow.converter.Arrow2PaimonVectorConverter*</exclude>
                                         <!-- start exclude for lake lance -->
                                         
<exclude>org.apache.fluss.lake.lance.*</exclude>
                                         <!-- temporarily exclude iceberg -->

Reply via email to