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 4425a598b2 [core] Add manifest Avro writer (#9194)
4425a598b2 is described below

commit 4425a598b2ec186e84779eec622b9151a1c4cead
Author: YeJunHao <[email protected]>
AuthorDate: Wed Aug 12 21:26:23 2026 +0800

    [core] Add manifest Avro writer (#9194)
    
    Extract manifest Avro writing from `ManifestFile` into a dedicated
    writer and provide the encoded-record / raw-block writing primitives 
required by subsequent manifest compaction optimizations.
    
    This PR only introduces and adopts the writer infrastructure. It does
    not include the run-merge sorting algorithm.
---
 .../apache/paimon/manifest/ManifestAvroWriter.java | 546 +++++++++++++++++++++
 .../org/apache/paimon/manifest/ManifestFile.java   | 150 ++----
 .../operation/ManifestEntryExternalSort.java       |  11 +-
 .../paimon/operation/ManifestFileMerger.java       |   5 +-
 .../apache/paimon/manifest/ManifestFileTest.java   | 155 ++++++
 .../flink/copy/CopyManifestFileOperator.java       |   9 +-
 .../apache/paimon/format/avro/AvroBlockWriter.java |  63 +++
 .../org/apache/paimon/format/avro/AvroBuilder.java |  34 --
 .../apache/paimon/format/avro/AvroBulkWriter.java  |  53 --
 .../apache/paimon/format/avro/AvroFileFormat.java  |  53 +-
 .../paimon/format/avro/AvroWriterFactory.java      |  48 --
 11 files changed, 834 insertions(+), 293 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestAvroWriter.java 
b/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestAvroWriter.java
new file mode 100644
index 0000000000..6244ed569c
--- /dev/null
+++ 
b/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestAvroWriter.java
@@ -0,0 +1,546 @@
+/*
+ * 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.manifest;
+
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.format.SimpleColStats;
+import org.apache.paimon.format.SimpleStatsCollector;
+import org.apache.paimon.format.avro.AvroBlockWriter;
+import org.apache.paimon.format.avro.AvroFileFormat;
+import org.apache.paimon.format.avro.AvroRawBlock;
+import org.apache.paimon.fs.FileIO;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.PositionOutputStream;
+import org.apache.paimon.io.RollingFileWriter;
+import org.apache.paimon.schema.SchemaManager;
+import org.apache.paimon.stats.SimpleStatsConverter;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.IOUtils;
+import org.apache.paimon.utils.ObjectSerializer;
+import org.apache.paimon.utils.PathFactory;
+
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Avro writer for manifest entries.
+ *
+ * <p>The writer accepts materialized entries, encoded records and compressed 
Avro blocks. It is
+ * intentionally separate from the generic writer abstractions because encoded 
Avro data is an
+ * implementation detail of manifest run merging.
+ */
+public final class ManifestAvroWriter implements AutoCloseable {
+
+    private final FileIO fileIO;
+    private final SchemaManager schemaManager;
+    private final RowType partitionType;
+    private final AvroFileFormat avroFileFormat;
+    private final ObjectSerializer<ManifestEntry> serializer;
+    private final String compression;
+    private final PathFactory pathFactory;
+    private final long targetFileSize;
+
+    private final List<ManifestFileMeta> results = new ArrayList<>();
+    private final List<Path> completedPaths = new ArrayList<>();
+    private @Nullable FileWriter currentWriter;
+    private long recordCount;
+    private boolean closed;
+
+    ManifestAvroWriter(
+            FileIO fileIO,
+            SchemaManager schemaManager,
+            RowType partitionType,
+            AvroFileFormat avroFileFormat,
+            ObjectSerializer<ManifestEntry> serializer,
+            String compression,
+            PathFactory pathFactory,
+            long targetFileSize) {
+        this.fileIO = fileIO;
+        this.schemaManager = schemaManager;
+        this.partitionType = partitionType;
+        this.avroFileFormat = avroFileFormat;
+        this.serializer = serializer;
+        this.compression = compression;
+        this.pathFactory = pathFactory;
+        this.targetFileSize = targetFileSize;
+    }
+
+    public void write(ManifestEntry entry) throws IOException {
+        try {
+            currentWriter().write(entry);
+            afterWrite(1, false);
+        } catch (IOException | RuntimeException | Error failure) {
+            abort();
+            throw failure;
+        }
+    }
+
+    public void write(Iterable<? extends ManifestEntry> entries) throws 
IOException {
+        for (ManifestEntry entry : entries) {
+            write(entry);
+        }
+    }
+
+    public void writeEncoded(ByteBuffer encodedRecord, EncodedEntry metadata) 
throws IOException {
+        try {
+            currentWriter().writeEncoded(encodedRecord, metadata);
+            afterWrite(1, false);
+        } catch (IOException | RuntimeException | Error failure) {
+            abort();
+            throw failure;
+        }
+    }
+
+    public void writeEncodedBlock(AvroRawBlock block, EncodedBlock metadata) 
throws IOException {
+        if (metadata.addedFiles < 0 || metadata.deletedFiles < 0) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "Manifest block file counts must be non-negative: 
added %s, deleted %s.",
+                            metadata.addedFiles, metadata.deletedFiles));
+        }
+        long metadataRecordCount = Math.addExact(metadata.addedFiles, 
metadata.deletedFiles);
+        if (metadataRecordCount != block.recordCount()) {
+            throw new IllegalArgumentException(
+                    String.format(
+                            "Manifest block record count mismatch: metadata 
%s, block %s.",
+                            metadataRecordCount, block.recordCount()));
+        }
+        try {
+            currentWriter().writeEncodedBlock(block, metadata);
+            afterWrite(metadataRecordCount, true);
+        } catch (IOException | RuntimeException | Error failure) {
+            abort();
+            throw failure;
+        }
+    }
+
+    private FileWriter currentWriter() {
+        if (closed) {
+            throw new IllegalStateException("Manifest writer has already 
closed.");
+        }
+        if (currentWriter == null) {
+            currentWriter = new FileWriter(pathFactory.newPath());
+        }
+        return currentWriter;
+    }
+
+    private void afterWrite(long addedRecords, boolean forceSizeCheck) throws 
IOException {
+        recordCount = Math.addExact(recordCount, addedRecords);
+        if (currentWriter.reachTargetSize(
+                forceSizeCheck || recordCount % 
RollingFileWriter.CHECK_ROLLING_RECORD_CNT == 0,
+                targetFileSize)) {
+            closeCurrentWriter();
+        }
+    }
+
+    private void closeCurrentWriter() throws IOException {
+        if (currentWriter == null) {
+            return;
+        }
+        currentWriter.close();
+        ManifestFileMeta result = currentWriter.result();
+        completedPaths.add(currentWriter.path);
+        results.add(result);
+        currentWriter = null;
+    }
+
+    public long recordCount() {
+        return recordCount;
+    }
+
+    public List<ManifestFileMeta> result() {
+        if (!closed) {
+            throw new IllegalStateException(
+                    "Cannot access manifest results before closing the 
writer.");
+        }
+        return results;
+    }
+
+    public void abort() {
+        if (currentWriter != null) {
+            currentWriter.abort();
+        }
+        for (Path path : completedPaths) {
+            fileIO.deleteQuietly(path);
+        }
+        completedPaths.clear();
+        results.clear();
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (closed) {
+            return;
+        }
+        try {
+            closeCurrentWriter();
+        } catch (IOException | RuntimeException | Error failure) {
+            abort();
+            throw failure;
+        } finally {
+            closed = true;
+        }
+    }
+
+    /** Reusable statistics needed when an encoded manifest record is copied 
directly. */
+    public static final class EncodedEntry {
+
+        private byte kind;
+        private BinaryRow partition;
+        private int bucket;
+        private int level;
+        private long schemaId;
+        private long firstRowId;
+        private long rowCount;
+
+        public EncodedEntry replace(
+                byte kind,
+                BinaryRow partition,
+                int bucket,
+                int level,
+                long schemaId,
+                long firstRowId,
+                long rowCount) {
+            this.kind = kind;
+            this.partition = partition;
+            this.bucket = bucket;
+            this.level = level;
+            this.schemaId = schemaId;
+            this.firstRowId = firstRowId;
+            this.rowCount = rowCount;
+            return this;
+        }
+    }
+
+    /** Aggregate statistics for an encoded Avro block copied without 
decompression. */
+    public static final class EncodedBlock {
+
+        private final long addedFiles;
+        private final long deletedFiles;
+        private final long schemaId;
+        private final int minBucket;
+        private final int maxBucket;
+        private final int minLevel;
+        private final int maxLevel;
+        private final long minRowId;
+        private final long maxRowId;
+        private final @Nullable BinaryRow nullPartition;
+        private final long nullPartitionCount;
+        private final @Nullable BinaryRow minNonNullPartition;
+        private final @Nullable BinaryRow maxNonNullPartition;
+
+        public EncodedBlock(
+                long addedFiles,
+                long deletedFiles,
+                long schemaId,
+                int minBucket,
+                int maxBucket,
+                int minLevel,
+                int maxLevel,
+                long minRowId,
+                long maxRowId,
+                @Nullable BinaryRow nullPartition,
+                long nullPartitionCount,
+                @Nullable BinaryRow minNonNullPartition,
+                @Nullable BinaryRow maxNonNullPartition) {
+            this.addedFiles = addedFiles;
+            this.deletedFiles = deletedFiles;
+            this.schemaId = schemaId;
+            this.minBucket = minBucket;
+            this.maxBucket = maxBucket;
+            this.minLevel = minLevel;
+            this.maxLevel = maxLevel;
+            this.minRowId = minRowId;
+            this.maxRowId = maxRowId;
+            this.nullPartition = nullPartition;
+            this.nullPartitionCount = nullPartitionCount;
+            this.minNonNullPartition = minNonNullPartition;
+            this.maxNonNullPartition = maxNonNullPartition;
+        }
+    }
+
+    private final class FileWriter {
+
+        private final Path path;
+        private final SimpleStatsCollector partitionStatsCollector;
+        private final SimpleStatsConverter partitionStatsSerializer;
+        private final Map<BinaryRow, long[]> encodedPartitionCounts = new 
IdentityHashMap<>();
+        private final long[] repeatedNullCounts = new 
long[partitionType.getFieldCount()];
+        private @Nullable PositionOutputStream out;
+        private @Nullable AvroBlockWriter writer;
+        private @Nullable Long outputBytes;
+        private long numAddedFiles;
+        private long numDeletedFiles;
+        private long schemaId = Long.MIN_VALUE;
+        private int minBucket = Integer.MAX_VALUE;
+        private int maxBucket = Integer.MIN_VALUE;
+        private int minLevel = Integer.MAX_VALUE;
+        private int maxLevel = Integer.MIN_VALUE;
+        private @Nullable RowIdStats rowIdStats = new RowIdStats();
+        private boolean closed;
+
+        private FileWriter(Path path) {
+            this.path = path;
+            this.partitionStatsCollector = new 
SimpleStatsCollector(partitionType);
+            this.partitionStatsSerializer = new 
SimpleStatsConverter(partitionType);
+            boolean outputCreated = false;
+            try {
+                out = fileIO.newOutputStream(path, false);
+                outputCreated = true;
+                writer =
+                        avroFileFormat.createBlockWriter(
+                                out, ManifestEntry.MANIFEST_ROW_TYPE, 
compression);
+            } catch (IOException failure) {
+                IOUtils.closeQuietly(writer);
+                IOUtils.closeQuietly(out);
+                if (outputCreated) {
+                    fileIO.deleteQuietly(path);
+                }
+                throw new UncheckedIOException(
+                        "Failed to create manifest Avro writer for " + path, 
failure);
+            } catch (RuntimeException | Error failure) {
+                IOUtils.closeQuietly(writer);
+                IOUtils.closeQuietly(out);
+                if (outputCreated) {
+                    fileIO.deleteQuietly(path);
+                }
+                throw failure;
+            }
+        }
+
+        private void write(ManifestEntry entry) throws IOException {
+            ensureOpen();
+            writer.addElement(
+                    entry instanceof ProjectedManifestEntry
+                            ? ((ProjectedManifestEntry) entry).fullRow()
+                            : serializer.toRow(entry));
+            collectStats(entry);
+        }
+
+        private void writeEncoded(ByteBuffer encodedRecord, EncodedEntry 
metadata)
+                throws IOException {
+            ensureOpen();
+            writer.addEncoded(encodedRecord);
+            collectStats(metadata);
+            addEncodedPartition(metadata.partition, 1);
+        }
+
+        private void writeEncodedBlock(AvroRawBlock block, EncodedBlock 
metadata)
+                throws IOException {
+            ensureOpen();
+            writer.addEncodedBlock(block);
+            collectStats(metadata);
+            if (metadata.nullPartitionCount > 0) {
+                addEncodedPartition(metadata.nullPartition, 
metadata.nullPartitionCount);
+            }
+            if (metadata.minNonNullPartition != null) {
+                addEncodedPartition(metadata.minNonNullPartition, 1);
+                if (metadata.maxNonNullPartition != 
metadata.minNonNullPartition) {
+                    addEncodedPartition(metadata.maxNonNullPartition, 1);
+                }
+            }
+        }
+
+        private void collectStats(ManifestEntry entry) {
+            switch (entry.kind()) {
+                case ADD:
+                    numAddedFiles++;
+                    break;
+                case DELETE:
+                    numDeletedFiles++;
+                    break;
+                default:
+                    throw new UnsupportedOperationException("Unknown entry 
kind: " + entry.kind());
+            }
+            schemaId = Math.max(schemaId, entry.file().schemaId());
+            minBucket = Math.min(minBucket, entry.bucket());
+            maxBucket = Math.max(maxBucket, entry.bucket());
+            minLevel = Math.min(minLevel, entry.level());
+            maxLevel = Math.max(maxLevel, entry.level());
+            if (rowIdStats != null) {
+                Long firstRowId = entry.file().firstRowId();
+                if (firstRowId == null) {
+                    rowIdStats = null;
+                } else {
+                    rowIdStats.collect(firstRowId, entry.file().rowCount());
+                }
+            }
+            partitionStatsCollector.collect(entry.partition());
+        }
+
+        private void collectStats(EncodedEntry entry) {
+            switch (FileKind.fromByteValue(entry.kind)) {
+                case ADD:
+                    numAddedFiles++;
+                    break;
+                case DELETE:
+                    numDeletedFiles++;
+                    break;
+                default:
+                    throw new UnsupportedOperationException("Unknown entry 
kind: " + entry.kind);
+            }
+            schemaId = Math.max(schemaId, entry.schemaId);
+            minBucket = Math.min(minBucket, entry.bucket);
+            maxBucket = Math.max(maxBucket, entry.bucket);
+            minLevel = Math.min(minLevel, entry.level);
+            maxLevel = Math.max(maxLevel, entry.level);
+            if (rowIdStats != null) {
+                rowIdStats.collect(entry.firstRowId, entry.rowCount);
+            }
+        }
+
+        private void collectStats(EncodedBlock block) {
+            numAddedFiles = Math.addExact(numAddedFiles, block.addedFiles);
+            numDeletedFiles = Math.addExact(numDeletedFiles, 
block.deletedFiles);
+            schemaId = Math.max(schemaId, block.schemaId);
+            minBucket = Math.min(minBucket, block.minBucket);
+            maxBucket = Math.max(maxBucket, block.maxBucket);
+            minLevel = Math.min(minLevel, block.minLevel);
+            maxLevel = Math.max(maxLevel, block.maxLevel);
+            if (rowIdStats != null) {
+                rowIdStats.collectRange(block.minRowId, block.maxRowId);
+            }
+        }
+
+        private void addEncodedPartition(@Nullable BinaryRow partition, long 
count) {
+            if (partition == null || count <= 0) {
+                return;
+            }
+            long[] value =
+                    encodedPartitionCounts.computeIfAbsent(partition, ignored 
-> new long[1]);
+            value[0] = Math.addExact(value[0], count);
+        }
+
+        private SimpleColStats[] partitionStats() {
+            for (Map.Entry<BinaryRow, long[]> entry : 
encodedPartitionCounts.entrySet()) {
+                BinaryRow partition = entry.getKey();
+                partitionStatsCollector.collect(partition);
+                long repeated = entry.getValue()[0] - 1;
+                if (repeated <= 0) {
+                    continue;
+                }
+                for (int field = 0; field < partition.getFieldCount(); 
field++) {
+                    if (partition.isNullAt(field)) {
+                        repeatedNullCounts[field] =
+                                Math.addExact(repeatedNullCounts[field], 
repeated);
+                    }
+                }
+            }
+            encodedPartitionCounts.clear();
+            SimpleColStats[] stats = partitionStatsCollector.extract();
+            for (int field = 0; field < stats.length; field++) {
+                if (repeatedNullCounts[field] == 0) {
+                    continue;
+                }
+                SimpleColStats current = stats[field];
+                stats[field] =
+                        new SimpleColStats(
+                                current.min(),
+                                current.max(),
+                                Math.addExact(current.nullCount(), 
repeatedNullCounts[field]));
+            }
+            return stats;
+        }
+
+        private boolean reachTargetSize(boolean suggestedCheck, long 
targetSize)
+                throws IOException {
+            ensureOpen();
+            return writer.reachTargetSize(suggestedCheck, targetSize);
+        }
+
+        private void ensureOpen() {
+            if (closed || writer == null) {
+                throw new IllegalStateException("Manifest writer has already 
closed.");
+            }
+        }
+
+        private void abort() {
+            IOUtils.closeQuietly(writer);
+            writer = null;
+            IOUtils.closeQuietly(out);
+            out = null;
+            fileIO.deleteQuietly(path);
+            closed = true;
+        }
+
+        private void close() throws IOException {
+            if (closed) {
+                return;
+            }
+            try {
+                writer.close();
+                writer = null;
+                out.flush();
+                outputBytes = out.getPos();
+                out.close();
+                out = null;
+            } catch (IOException | RuntimeException | Error failure) {
+                abort();
+                throw failure;
+            } finally {
+                closed = true;
+            }
+        }
+
+        private ManifestFileMeta result() {
+            if (!closed || outputBytes == null) {
+                throw new IllegalStateException(
+                        "Cannot access manifest result before closing the 
writer.");
+            }
+            return new ManifestFileMeta(
+                    path.getName(),
+                    outputBytes,
+                    numAddedFiles,
+                    numDeletedFiles,
+                    partitionStatsSerializer.toBinaryAllMode(partitionStats()),
+                    numAddedFiles + numDeletedFiles > 0
+                            ? schemaId
+                            : schemaManager.latest().get().id(),
+                    minBucket,
+                    maxBucket,
+                    minLevel,
+                    maxLevel,
+                    rowIdStats == null ? null : rowIdStats.minRowId,
+                    rowIdStats == null ? null : rowIdStats.maxRowId);
+        }
+    }
+
+    private static class RowIdStats {
+
+        private long minRowId = Long.MAX_VALUE;
+        private long maxRowId = Long.MIN_VALUE;
+
+        private void collect(long firstRowId, long rowCount) {
+            minRowId = Math.min(minRowId, firstRowId);
+            maxRowId = Math.max(maxRowId, firstRowId + rowCount - 1);
+        }
+
+        private void collectRange(long minRowId, long maxRowId) {
+            this.minRowId = Math.min(this.minRowId, minRowId);
+            this.maxRowId = Math.max(this.maxRowId, maxRowId);
+        }
+    }
+}
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestFile.java 
b/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestFile.java
index 0564ff198e..b9fa876f61 100644
--- a/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestFile.java
+++ b/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestFile.java
@@ -21,19 +21,14 @@ package org.apache.paimon.manifest;
 import org.apache.paimon.annotation.VisibleForTesting;
 import org.apache.paimon.data.InternalRow;
 import org.apache.paimon.format.FileFormat;
-import org.apache.paimon.format.FormatWriterFactory;
-import org.apache.paimon.format.SimpleStatsCollector;
+import org.apache.paimon.format.avro.AvroFileFormat;
 import org.apache.paimon.fs.FileIO;
 import org.apache.paimon.fs.Path;
 import org.apache.paimon.io.DataFileMeta;
-import org.apache.paimon.io.RollingFileWriter;
-import org.apache.paimon.io.RollingFileWriterImpl;
-import org.apache.paimon.io.SingleFileWriter;
 import org.apache.paimon.manifest.ProjectedManifestEntry.Projection;
 import org.apache.paimon.operation.metrics.CacheMetrics;
 import org.apache.paimon.partition.PartitionPredicate;
 import org.apache.paimon.schema.SchemaManager;
-import org.apache.paimon.stats.SimpleStatsConverter;
 import org.apache.paimon.types.RowType;
 import org.apache.paimon.utils.CloseableIterator;
 import org.apache.paimon.utils.FileStorePathFactory;
@@ -62,15 +57,15 @@ public class ManifestFile extends 
ObjectsFile<ManifestEntry> {
 
     private final SchemaManager schemaManager;
     private final RowType partitionType;
-    private final FormatWriterFactory writerFactory;
+    private final AvroFileFormat avroFileFormat;
     private final long suggestedFileSize;
 
     private ManifestFile(
             FileIO fileIO,
             SchemaManager schemaManager,
             RowType partitionType,
+            AvroFileFormat avroFileFormat,
             ManifestEntrySerializer serializer,
-            FormatWriterFactory writerFactory,
             String compression,
             PathFactory pathFactory,
             long suggestedFileSize,
@@ -82,13 +77,13 @@ public class ManifestFile extends 
ObjectsFile<ManifestEntry> {
                 (path, ignoredFileSize) ->
                         createManifestIterator(
                                 fileIO, path, ManifestEntry.MANIFEST_ROW_TYPE, 
null, null),
-                writerFactory,
+                
avroFileFormat.createWriterFactory(ManifestEntry.MANIFEST_ROW_TYPE),
                 compression,
                 pathFactory,
                 cache);
         this.schemaManager = schemaManager;
         this.partitionType = partitionType;
-        this.writerFactory = writerFactory;
+        this.avroFileFormat = avroFileFormat;
         this.suggestedFileSize = suggestedFileSize;
     }
 
@@ -261,7 +256,7 @@ public class ManifestFile extends 
ObjectsFile<ManifestEntry> {
      * <p>NOTE: This method is atomic.
      */
     public List<ManifestFileMeta> write(List<ManifestEntry> entries) {
-        RollingFileWriter<ManifestEntry, ManifestFileMeta> writer = 
createRollingWriter();
+        ManifestAvroWriter writer = createAvroWriter();
         try {
             writer.write(entries);
             writer.close();
@@ -271,108 +266,52 @@ public class ManifestFile extends 
ObjectsFile<ManifestEntry> {
         }
     }
 
-    public RollingFileWriter<ManifestEntry, ManifestFileMeta> 
createRollingWriter() {
-        return new RollingFileWriterImpl<>(
-                () -> new ManifestEntryWriter(writerFactory, 
pathFactory.newPath(), compression),
-                suggestedFileSize,
-                Long.MAX_VALUE);
+    /** Creates a rolling Avro manifest writer. */
+    public ManifestAvroWriter createAvroWriter() {
+        return new ManifestAvroWriter(
+                fileIO,
+                schemaManager,
+                partitionType,
+                avroFileFormat,
+                serializer,
+                compression,
+                pathFactory,
+                suggestedFileSize);
     }
 
-    public ManifestEntryWriter createManifestEntryWriter(Path manifestPath) {
-        return new ManifestEntryWriter(writerFactory, manifestPath, 
compression);
+    /** Creates an Avro manifest writer for one explicit path. */
+    public ManifestAvroWriter createAvroWriter(Path manifestPath) {
+        return new ManifestAvroWriter(
+                fileIO,
+                schemaManager,
+                partitionType,
+                avroFileFormat,
+                serializer,
+                compression,
+                singlePathFactory(manifestPath),
+                Long.MAX_VALUE);
     }
 
-    /** Writer for {@link ManifestEntry}. */
-    public class ManifestEntryWriter extends SingleFileWriter<ManifestEntry, 
ManifestFileMeta> {
-
-        private final SimpleStatsCollector partitionStatsCollector;
-        private final SimpleStatsConverter partitionStatsSerializer;
-
-        private long numAddedFiles = 0;
-        private long numDeletedFiles = 0;
-        private long schemaId = Long.MIN_VALUE;
-        private int minBucket = Integer.MAX_VALUE;
-        private int maxBucket = Integer.MIN_VALUE;
-        private int minLevel = Integer.MAX_VALUE;
-        private int maxLevel = Integer.MIN_VALUE;
-        private @Nullable RowIdStats rowIdStats = new RowIdStats();
-
-        ManifestEntryWriter(FormatWriterFactory factory, Path path, String 
fileCompression) {
-            super(
-                    ManifestFile.this.fileIO,
-                    factory,
-                    path,
-                    serializer::toRow,
-                    fileCompression,
-                    false);
-            this.partitionStatsCollector = new 
SimpleStatsCollector(partitionType);
-            this.partitionStatsSerializer = new 
SimpleStatsConverter(partitionType);
-        }
+    private PathFactory singlePathFactory(Path manifestPath) {
+        return new PathFactory() {
 
-        @Override
-        public void write(ManifestEntry entry) throws IOException {
-            if (entry instanceof ProjectedManifestEntry) {
-                writeRow(((ProjectedManifestEntry) entry).fullRow());
-            } else {
-                super.write(entry);
-            }
+            private boolean created;
 
-            switch (entry.kind()) {
-                case ADD:
-                    numAddedFiles++;
-                    break;
-                case DELETE:
-                    numDeletedFiles++;
-                    break;
-                default:
-                    throw new UnsupportedOperationException("Unknown entry 
kind: " + entry.kind());
-            }
-            schemaId = Math.max(schemaId, entry.file().schemaId());
-            minBucket = Math.min(minBucket, entry.bucket());
-            maxBucket = Math.max(maxBucket, entry.bucket());
-            minLevel = Math.min(minLevel, entry.level());
-            maxLevel = Math.max(maxLevel, entry.level());
-            if (rowIdStats != null) {
-                Long firstRowId = entry.file().firstRowId();
-                if (firstRowId == null) {
-                    rowIdStats = null;
-                } else {
-                    rowIdStats.collect(firstRowId, entry.file().rowCount());
+            @Override
+            public Path newPath() {
+                if (created) {
+                    throw new IllegalStateException(
+                            "Cannot create more than one fixed-path manifest 
file.");
                 }
+                created = true;
+                return manifestPath;
             }
 
-            partitionStatsCollector.collect(entry.partition());
-        }
-
-        @Override
-        public ManifestFileMeta result() throws IOException {
-            return new ManifestFileMeta(
-                    path.getName(),
-                    outputBytes(),
-                    numAddedFiles,
-                    numDeletedFiles,
-                    
partitionStatsSerializer.toBinaryAllMode(partitionStatsCollector.extract()),
-                    numAddedFiles + numDeletedFiles > 0
-                            ? schemaId
-                            : schemaManager.latest().get().id(),
-                    minBucket,
-                    maxBucket,
-                    minLevel,
-                    maxLevel,
-                    rowIdStats == null ? null : rowIdStats.minRowId,
-                    rowIdStats == null ? null : rowIdStats.maxRowId);
-        }
-    }
-
-    private static class RowIdStats {
-
-        private long minRowId = Long.MAX_VALUE;
-        private long maxRowId = Long.MIN_VALUE;
-
-        private void collect(long firstRowId, long rowCount) {
-            minRowId = Math.min(minRowId, firstRowId);
-            maxRowId = Math.max(maxRowId, firstRowId + rowCount - 1);
-        }
+            @Override
+            public Path toPath(String fileName) {
+                return pathFactory.toPath(fileName);
+            }
+        };
     }
 
     /** Creator of {@link ManifestFile}. */
@@ -411,13 +350,12 @@ public class ManifestFile extends 
ObjectsFile<ManifestEntry> {
         }
 
         public ManifestFile create() {
-            RowType entryType = ManifestEntry.MANIFEST_ROW_TYPE;
             return new ManifestFile(
                     fileIO,
                     schemaManager,
                     partitionType,
+                    (AvroFileFormat) fileFormat,
                     new ManifestEntrySerializer(),
-                    fileFormat.createWriterFactory(entryType),
                     compression,
                     pathFactory.manifestFileFactory(),
                     suggestedFileSize,
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java
 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java
index 58866adb82..5c9b83772e 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java
@@ -24,9 +24,9 @@ import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.data.GenericRow;
 import org.apache.paimon.data.serializer.InternalRowSerializer;
 import org.apache.paimon.disk.IOManager;
-import org.apache.paimon.io.RollingFileWriter;
 import org.apache.paimon.manifest.CompactFileIdentifierSet;
 import org.apache.paimon.manifest.FileEntry.ReusableIdentifier;
+import org.apache.paimon.manifest.ManifestAvroWriter;
 import org.apache.paimon.manifest.ManifestEntry;
 import org.apache.paimon.manifest.ManifestFile;
 import org.apache.paimon.manifest.ManifestFileMeta;
@@ -233,8 +233,7 @@ public class ManifestEntryExternalSort {
                 return Collections.emptyList();
             }
 
-            RollingFileWriter<ManifestEntry, ManifestFileMeta> writer =
-                    manifestFile.createRollingWriter();
+            ManifestAvroWriter writer = manifestFile.createAvroWriter();
             Exception exception = null;
             try {
                 MutableObjectIterator<BinaryRow> iterator = 
sortBuffer.sortedIterator();
@@ -267,10 +266,8 @@ public class ManifestEntryExternalSort {
                 return Pair.of(Collections.emptyList(), 
Collections.emptyList());
             }
 
-            RollingFileWriter<ManifestEntry, ManifestFileMeta> addWriter =
-                    manifestFile.createRollingWriter();
-            RollingFileWriter<ManifestEntry, ManifestFileMeta> deleteWriter =
-                    manifestFile.createRollingWriter();
+            ManifestAvroWriter addWriter = manifestFile.createAvroWriter();
+            ManifestAvroWriter deleteWriter = manifestFile.createAvroWriter();
             CompactFileIdentifierSet matchedEntries = new 
CompactFileIdentifierSet();
             CompactFileIdentifierSet emittedDeletes = new 
CompactFileIdentifierSet();
             ReusableIdentifier identifier = new ReusableIdentifier();
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
index 0313b2b12b..7c5019f1e3 100644
--- 
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
+++ 
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
@@ -21,8 +21,8 @@ package org.apache.paimon.operation;
 import org.apache.paimon.CoreOptions;
 import org.apache.paimon.data.BinaryRow;
 import org.apache.paimon.disk.IOManager;
-import org.apache.paimon.io.RollingFileWriter;
 import org.apache.paimon.manifest.FileEntry;
+import org.apache.paimon.manifest.ManifestAvroWriter;
 import org.apache.paimon.manifest.ManifestEntry;
 import org.apache.paimon.manifest.ManifestFile;
 import org.apache.paimon.manifest.ManifestFileMeta;
@@ -264,8 +264,7 @@ public class ManifestFileMerger {
             return Optional.empty();
         }
 
-        RollingFileWriter<ManifestEntry, ManifestFileMeta> writer =
-                manifestFile.createRollingWriter();
+        ManifestAvroWriter writer = manifestFile.createAvroWriter();
         Function<ManifestFileMeta, List<FullCompactionReadResult>> reader =
                 file ->
                         singletonList(
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileTest.java 
b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileTest.java
index 0846cb2697..3bd052ab7a 100644
--- a/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/manifest/ManifestFileTest.java
@@ -85,6 +85,105 @@ public class ManifestFileTest {
         assertThat(actualEntries).isEqualTo(entries);
     }
 
+    @Test
+    void testWriteManifestFileToExplicitPath() throws Exception {
+        List<ManifestEntry> entries = generateData();
+        ManifestFile manifestFile = createManifestFile(tempDir.toString());
+        Path path = new Path(tempDir.toString() + 
"/manifest/explicit-manifest");
+
+        ManifestAvroWriter writer = manifestFile.createAvroWriter(path);
+        writer.write(entries);
+        writer.close();
+
+        assertThat(writer.result())
+                .singleElement()
+                .extracting(ManifestFileMeta::fileName)
+                .isEqualTo("explicit-manifest");
+        assertThat(manifestFile.read("explicit-manifest")).isEqualTo(entries);
+    }
+
+    @Test
+    void testAbortedManifestWriterDoesNotExposeResults() throws Exception {
+        ManifestAvroWriter writer = 
createManifestFile(tempDir.toString()).createAvroWriter();
+        writer.write(gen.next());
+
+        writer.abort();
+
+        assertThatThrownBy(writer::result)
+                .isInstanceOf(IllegalStateException.class)
+                .hasMessageContaining("before closing");
+    }
+
+    @Test
+    void testWriteEncodedMixedBlockCountsDeletes() throws Exception {
+        assertEncodedBlockCounts(FileKind.ADD, FileKind.DELETE);
+    }
+
+    @Test
+    void testWriteEncodedDeleteOnlyBlockCountsDeletes() throws Exception {
+        assertEncodedBlockCounts(FileKind.DELETE, FileKind.DELETE);
+    }
+
+    @Test
+    void testWriteEncodedRecords() throws Exception {
+        ManifestEntry source = gen.next();
+        List<ManifestEntry> entries =
+                Arrays.asList(
+                        ManifestEntry.create(
+                                FileKind.ADD,
+                                source.partition(),
+                                source.bucket(),
+                                source.totalBuckets(),
+                                source.file().newFirstRowId(10L)),
+                        ManifestEntry.create(
+                                FileKind.DELETE,
+                                source.partition(),
+                                source.bucket(),
+                                source.totalBuckets(),
+                                source.file().newFirstRowId(20L)));
+        ManifestFile manifestFile = createManifestFile(tempDir.toString(), 
Long.MAX_VALUE);
+        ManifestFileMeta sourceMeta = writeSingleManifest(manifestFile, 
entries);
+        ManifestAvroWriter writer = manifestFile.createAvroWriter();
+        ManifestAvroWriter.EncodedEntry metadata = new 
ManifestAvroWriter.EncodedEntry();
+        int position = 0;
+
+        try (ManifestAvroReader reader = openManifestReader(sourceMeta)) {
+            while (reader.hasNext()) {
+                ManifestAvroReader.RowIterator rows =
+                        reader.next().toRows(ManifestEntry.MANIFEST_ROW_TYPE);
+                while (rows.hasNext()) {
+                    rows.next();
+                    ManifestEntry entry = entries.get(position++);
+                    writer.writeEncoded(
+                            rows.encodedRecord(),
+                            metadata.replace(
+                                    entry.kind().toByteValue(),
+                                    entry.partition(),
+                                    entry.bucket(),
+                                    entry.level(),
+                                    entry.file().schemaId(),
+                                    entry.file().firstRowId(),
+                                    entry.file().rowCount()));
+                }
+            }
+        }
+        writer.close();
+
+        assertThat(position).isEqualTo(entries.size());
+        ManifestFileMeta result = writer.result().get(0);
+        
assertThat(result.numAddedFiles()).isEqualTo(sourceMeta.numAddedFiles());
+        
assertThat(result.numDeletedFiles()).isEqualTo(sourceMeta.numDeletedFiles());
+        
assertThat(result.partitionStats()).isEqualTo(sourceMeta.partitionStats());
+        assertThat(result.schemaId()).isEqualTo(sourceMeta.schemaId());
+        assertThat(result.minBucket()).isEqualTo(sourceMeta.minBucket());
+        assertThat(result.maxBucket()).isEqualTo(sourceMeta.maxBucket());
+        assertThat(result.minLevel()).isEqualTo(sourceMeta.minLevel());
+        assertThat(result.maxLevel()).isEqualTo(sourceMeta.maxLevel());
+        assertThat(result.minRowId()).isEqualTo(sourceMeta.minRowId());
+        assertThat(result.maxRowId()).isEqualTo(sourceMeta.maxRowId());
+        
assertThat(manifestFile.read(result.fileName())).containsExactlyElementsOf(entries);
+    }
+
     @Test
     void testReadMissingManifestFile() {
         ManifestFile manifestFile = createManifestFile(tempDir.toString());
@@ -767,6 +866,62 @@ public class ManifestFileTest {
         return entries;
     }
 
+    private void assertEncodedBlockCounts(FileKind... kinds) throws Exception {
+        ManifestEntry source = gen.next();
+        List<ManifestEntry> entries = new ArrayList<>();
+        long firstRowId = 10;
+        for (FileKind kind : kinds) {
+            DataFileMeta file = source.file().newFirstRowId(firstRowId);
+            entries.add(
+                    ManifestEntry.create(
+                            kind,
+                            source.partition(),
+                            source.bucket(),
+                            source.totalBuckets(),
+                            file));
+            firstRowId += file.rowCount();
+        }
+
+        ManifestFile manifestFile = createManifestFile(tempDir.toString(), 
Long.MAX_VALUE);
+        ManifestFileMeta sourceMeta = writeSingleManifest(manifestFile, 
entries);
+        ManifestAvroWriter writer = manifestFile.createAvroWriter();
+        try (ManifestAvroReader reader = openManifestReader(sourceMeta)) {
+            assertThat(reader.hasNext()).isTrue();
+            ManifestAvroReader.RawBlock block = reader.next();
+            assertThat(block.recordCount()).isEqualTo(entries.size());
+            writer.writeEncodedBlock(block.encodedBlock(), 
encodedBlock(sourceMeta, entries));
+            assertThat(reader.hasNext()).isFalse();
+        }
+        writer.close();
+
+        ManifestFileMeta result = writer.result().get(0);
+        assertThat(result.numAddedFiles())
+                .isEqualTo(entries.stream().filter(entry -> entry.kind() == 
FileKind.ADD).count());
+        assertThat(result.numDeletedFiles())
+                .isEqualTo(
+                        entries.stream().filter(entry -> entry.kind() == 
FileKind.DELETE).count());
+        
assertThat(manifestFile.read(result.fileName())).containsExactlyElementsOf(entries);
+    }
+
+    private ManifestAvroWriter.EncodedBlock encodedBlock(
+            ManifestFileMeta meta, List<ManifestEntry> entries) {
+        boolean nullPartition = entries.get(0).partition().isNullAt(0);
+        return new ManifestAvroWriter.EncodedBlock(
+                meta.numAddedFiles(),
+                meta.numDeletedFiles(),
+                meta.schemaId(),
+                meta.minBucket(),
+                meta.maxBucket(),
+                meta.minLevel(),
+                meta.maxLevel(),
+                meta.minRowId(),
+                meta.maxRowId(),
+                nullPartition ? entries.get(0).partition() : null,
+                nullPartition ? entries.size() : 0,
+                nullPartition ? null : entries.get(0).partition(),
+                nullPartition ? null : entries.get(0).partition());
+    }
+
     private ManifestAvroReader openManifestReader(ManifestFileMeta manifest) 
throws IOException {
         FileIO fileIO = LocalFileIO.create();
         Path path = new Path(new Path(tempDir.toUri()), "manifest/" + 
manifest.fileName());
diff --git 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/copy/CopyManifestFileOperator.java
 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/copy/CopyManifestFileOperator.java
index 1abcccd0bb..ba6094a44f 100644
--- 
a/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/copy/CopyManifestFileOperator.java
+++ 
b/paimon-flink/paimon-flink-common/src/main/java/org/apache/paimon/flink/copy/CopyManifestFileOperator.java
@@ -24,9 +24,9 @@ import org.apache.paimon.catalog.Identifier;
 import org.apache.paimon.flink.FlinkCatalogFactory;
 import org.apache.paimon.fs.FileIO;
 import org.apache.paimon.fs.Path;
+import org.apache.paimon.manifest.ManifestAvroWriter;
 import org.apache.paimon.manifest.ManifestEntry;
 import org.apache.paimon.manifest.ManifestFile;
-import org.apache.paimon.manifest.ManifestFile.ManifestEntryWriter;
 import org.apache.paimon.options.Options;
 import org.apache.paimon.table.FileStoreTable;
 import org.apache.paimon.utils.FileStorePathFactory;
@@ -158,10 +158,9 @@ public class CopyManifestFileOperator extends 
AbstractStreamOperator<CopyFileInf
                                     
manifestEntry.file().newExternalPath(null));
                     targetManifestEntries.add(newManifestEntry);
                 }
-                ManifestEntryWriter manifestEntryWriter =
-                        manifestFile.createManifestEntryWriter(targetPath);
-                manifestEntryWriter.write(targetManifestEntries);
-                manifestEntryWriter.close();
+                ManifestAvroWriter writer = 
manifestFile.createAvroWriter(targetPath);
+                writer.write(targetManifestEntries);
+                writer.close();
             } else {
                 // copy it
                 IOUtils.copyBytes(
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroBlockWriter.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroBlockWriter.java
new file mode 100644
index 0000000000..377a7862d0
--- /dev/null
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroBlockWriter.java
@@ -0,0 +1,63 @@
+/*
+ * 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.InternalRow;
+import org.apache.paimon.format.FormatWriter;
+import org.apache.paimon.fs.PositionOutputStream;
+
+import org.apache.avro.file.DataFileWriter;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+/** Avro writer which accepts normal rows, encoded records and compressed 
blocks. */
+public final class AvroBlockWriter implements FormatWriter {
+
+    private final DataFileWriter<InternalRow> writer;
+    private final PositionOutputStream out;
+
+    public AvroBlockWriter(DataFileWriter<InternalRow> writer, 
PositionOutputStream out) {
+        this.writer = writer;
+        this.out = out;
+    }
+
+    @Override
+    public void addElement(InternalRow element) throws IOException {
+        writer.append(element);
+    }
+
+    public void addEncoded(ByteBuffer record) throws IOException {
+        writer.appendEncoded(record);
+    }
+
+    public void addEncodedBlock(AvroRawBlock block) throws IOException {
+        writer.appendAllFrom(block.asStream(), false);
+    }
+
+    @Override
+    public boolean reachTargetSize(boolean suggestedCheck, long targetSize) 
throws IOException {
+        return suggestedCheck && out.getPos() >= targetSize;
+    }
+
+    @Override
+    public void close() throws IOException {
+        writer.close();
+    }
+}
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroBuilder.java 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroBuilder.java
deleted file mode 100644
index 8c39dc64ae..0000000000
--- a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroBuilder.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * 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.avro.file.DataFileWriter;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.Serializable;
-
-/** A builder to create an {@link AvroBulkWriter} from an {@link 
OutputStream}. */
-@FunctionalInterface
-public interface AvroBuilder<T> extends Serializable {
-
-    /** Creates and configures an Avro writer to the given output file. */
-    DataFileWriter<T> createWriter(OutputStream outputStream, String 
compression)
-            throws IOException;
-}
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroBulkWriter.java 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroBulkWriter.java
deleted file mode 100644
index 3257638647..0000000000
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroBulkWriter.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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.avro.file.DataFileWriter;
-
-import java.io.Closeable;
-import java.io.IOException;
-
-/** A simple writer implementation that wraps an Avro {@link DataFileWriter}. 
*/
-public class AvroBulkWriter<T> implements Closeable {
-
-    /** The underlying Avro writer. */
-    private final DataFileWriter<T> dataFileWriter;
-
-    /**
-     * Create a new AvroBulkWriter wrapping the given Avro {@link 
DataFileWriter}.
-     *
-     * @param dataFileWriter The underlying Avro writer.
-     */
-    public AvroBulkWriter(DataFileWriter<T> dataFileWriter) {
-        this.dataFileWriter = dataFileWriter;
-    }
-
-    public void addElement(T element) throws IOException {
-        dataFileWriter.append(element);
-    }
-
-    public void flush() throws IOException {
-        dataFileWriter.flush();
-    }
-
-    @Override
-    public void close() throws IOException {
-        dataFileWriter.close();
-    }
-}
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroFileFormat.java 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroFileFormat.java
index 43105fd1cd..73b08eec47 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroFileFormat.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroFileFormat.java
@@ -25,6 +25,7 @@ import org.apache.paimon.format.FormatReaderFactory;
 import org.apache.paimon.format.FormatWriter;
 import org.apache.paimon.format.FormatWriterFactory;
 import org.apache.paimon.format.SimpleStatsExtractor;
+import org.apache.paimon.fs.CloseShieldOutputStream;
 import org.apache.paimon.fs.PositionOutputStream;
 import org.apache.paimon.options.ConfigOption;
 import org.apache.paimon.options.ConfigOptions;
@@ -85,6 +86,18 @@ public class AvroFileFormat extends FileFormat {
         return new RowAvroWriterFactory(type);
     }
 
+    public AvroBlockWriter createBlockWriter(
+            PositionOutputStream out, RowType rowType, String compression) 
throws IOException {
+        Schema schema =
+                AvroSchemaConverter.convertToSchema(rowType, 
options.get(AVRO_ROW_NAME_MAPPING));
+        AvroRowDatumWriter datumWriter = new AvroRowDatumWriter(rowType);
+        DataFileWriter<InternalRow> writer = new DataFileWriter<>(datumWriter);
+        writer.setCodec(createCodecFactory(compression));
+        writer.setFlushOnEveryBlock(false);
+        writer.create(schema, new CloseShieldOutputStream(out));
+        return new AvroBlockWriter(writer, out);
+    }
+
     @Override
     public Optional<SimpleStatsExtractor> createStatsExtractor(
             RowType type, SimpleColStatsCollector.Factory[] statsCollectors) {
@@ -113,50 +126,16 @@ public class AvroFileFormat extends FileFormat {
     /** A {@link FormatWriterFactory} to write {@link InternalRow}. */
     private class RowAvroWriterFactory implements FormatWriterFactory {
 
-        private final AvroWriterFactory<InternalRow> factory;
+        private final RowType rowType;
 
         private RowAvroWriterFactory(RowType rowType) {
-            this.factory =
-                    new AvroWriterFactory<>(
-                            (out, compression) -> {
-                                Schema schema =
-                                        AvroSchemaConverter.convertToSchema(
-                                                rowType, 
options.get(AVRO_ROW_NAME_MAPPING));
-                                AvroRowDatumWriter datumWriter = new 
AvroRowDatumWriter(rowType);
-                                DataFileWriter<InternalRow> dataFileWriter =
-                                        new DataFileWriter<>(datumWriter);
-                                
dataFileWriter.setCodec(createCodecFactory(compression));
-                                dataFileWriter.setFlushOnEveryBlock(false);
-                                dataFileWriter.create(schema, out);
-                                return dataFileWriter;
-                            });
+            this.rowType = rowType;
         }
 
         @Override
         public FormatWriter create(PositionOutputStream out, String 
compression)
                 throws IOException {
-            AvroBulkWriter<InternalRow> writer = factory.create(out, 
compression);
-            return new FormatWriter() {
-
-                @Override
-                public void addElement(InternalRow element) throws IOException 
{
-                    writer.addElement(element);
-                }
-
-                @Override
-                public void close() throws IOException {
-                    writer.close();
-                }
-
-                @Override
-                public boolean reachTargetSize(boolean suggestedCheck, long 
targetSize)
-                        throws IOException {
-                    if (out != null) {
-                        return suggestedCheck && out.getPos() >= targetSize;
-                    }
-                    throw new IOException("Failed to get stream length: no 
open stream");
-                }
-            };
+            return createBlockWriter(out, rowType, compression);
         }
     }
 }
diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroWriterFactory.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroWriterFactory.java
deleted file mode 100644
index 5cbfaca7ee..0000000000
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/avro/AvroWriterFactory.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.fs.CloseShieldOutputStream;
-import org.apache.paimon.fs.PositionOutputStream;
-
-import org.apache.avro.file.DataFileWriter;
-
-import java.io.IOException;
-
-/**
- * A factory that creates an {@link AvroBulkWriter}.
- *
- * @param <T> The type of record to write.
- */
-public class AvroWriterFactory<T> {
-
-    /** The builder to construct the Avro {@link DataFileWriter}. */
-    private final AvroBuilder<T> avroBuilder;
-
-    /** Creates a new AvroWriterFactory using the given builder to assemble 
the ParquetWriter. */
-    public AvroWriterFactory(AvroBuilder<T> avroBuilder) {
-        this.avroBuilder = avroBuilder;
-    }
-
-    public AvroBulkWriter<T> create(PositionOutputStream out, String 
compression)
-            throws IOException {
-        return new AvroBulkWriter<>(
-                avroBuilder.createWriter(new CloseShieldOutputStream(out), 
compression));
-    }
-}

Reply via email to