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

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


The following commit(s) were added to refs/heads/main by this push:
     new 91dd359  feat(format): introduce avro writer components (#88)
91dd359 is described below

commit 91dd3592d8899c1508d40565c4a65ae3b6c28a6e
Author: Zhang Jiawei <[email protected]>
AuthorDate: Mon Jun 22 17:33:24 2026 +0800

    feat(format): introduce avro writer components (#88)
---
 src/paimon/format/avro/avro_file_batch_reader.cpp  | 212 +++++++++++
 src/paimon/format/avro/avro_file_batch_reader.h    | 103 ++++++
 .../format/avro/avro_file_batch_reader_test.cpp    | 410 +++++++++++++++++++++
 src/paimon/format/avro/avro_format_writer.cpp      | 136 +++++++
 src/paimon/format/avro/avro_format_writer.h        |  86 +++++
 src/paimon/format/avro/avro_format_writer_test.cpp | 256 +++++++++++++
 src/paimon/format/avro/avro_stats_extractor.cpp    | 127 +++++++
 src/paimon/format/avro/avro_stats_extractor.h      |  73 ++++
 .../format/avro/avro_stats_extractor_test.cpp      | 220 +++++++++++
 src/paimon/format/avro/avro_writer_builder.h       | 105 ++++++
 .../format/avro/avro_writer_builder_test.cpp       | 150 ++++++++
 11 files changed, 1878 insertions(+)

diff --git a/src/paimon/format/avro/avro_file_batch_reader.cpp 
b/src/paimon/format/avro/avro_file_batch_reader.cpp
new file mode 100644
index 0000000..d2d2f53
--- /dev/null
+++ b/src/paimon/format/avro/avro_file_batch_reader.cpp
@@ -0,0 +1,212 @@
+/*
+ * 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.
+ */
+
+#include "paimon/format/avro/avro_file_batch_reader.h"
+
+#include <memory>
+#include <utility>
+
+#include "arrow/c/bridge.h"
+#include "fmt/format.h"
+#include "paimon/common/metrics/metrics_impl.h"
+#include "paimon/common/utils/arrow/arrow_utils.h"
+#include "paimon/common/utils/arrow/mem_utils.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/scope_guard.h"
+#include "paimon/format/avro/avro_input_stream_impl.h"
+#include "paimon/format/avro/avro_schema_converter.h"
+#include "paimon/reader/batch_reader.h"
+
+namespace paimon::avro {
+
+AvroFileBatchReader::AvroFileBatchReader(const std::shared_ptr<InputStream>& 
input_stream,
+                                         const 
std::shared_ptr<::arrow::DataType>& file_data_type,
+                                         
std::unique_ptr<::avro::DataFileReaderBase>&& reader,
+                                         
std::unique_ptr<arrow::ArrayBuilder>&& array_builder,
+                                         std::unique_ptr<arrow::MemoryPool>&& 
arrow_pool,
+                                         int32_t batch_size,
+                                         const std::shared_ptr<MemoryPool>& 
pool)
+    : pool_(pool),
+      arrow_pool_(std::move(arrow_pool)),
+      input_stream_(input_stream),
+      file_data_type_(file_data_type),
+      reader_(std::move(reader)),
+      array_builder_(std::move(array_builder)),
+      batch_size_(batch_size),
+      metrics_(std::make_shared<MetricsImpl>()) {}
+
+AvroFileBatchReader::~AvroFileBatchReader() {
+    DoClose();
+}
+
+void AvroFileBatchReader::DoClose() {
+    if (!close_) {
+        reader_->close();
+        close_ = true;
+    }
+}
+
+Result<std::unique_ptr<AvroFileBatchReader>> AvroFileBatchReader::Create(
+    const std::shared_ptr<InputStream>& input_stream, int32_t batch_size,
+    const std::shared_ptr<MemoryPool>& pool) {
+    if (batch_size <= 0) {
+        return Status::Invalid(
+            fmt::format("invalid batch size {}, must be larger than 0", 
batch_size));
+    }
+    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<::avro::DataFileReaderBase> reader,
+                           CreateDataFileReader(input_stream, pool));
+    const auto& avro_file_schema = reader->dataSchema();
+    PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<::arrow::DataType> file_data_type,
+                           
AvroSchemaConverter::AvroSchemaToArrowDataType(avro_file_schema));
+    auto arrow_pool = GetArrowPool(pool);
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::unique_ptr<arrow::ArrayBuilder> 
array_builder,
+                                      arrow::MakeBuilder(file_data_type, 
arrow_pool.get()));
+    return std::unique_ptr<AvroFileBatchReader>(
+        new AvroFileBatchReader(input_stream, file_data_type, 
std::move(reader),
+                                std::move(array_builder), 
std::move(arrow_pool), batch_size, pool));
+}
+
+Result<std::unique_ptr<::avro::DataFileReaderBase>> 
AvroFileBatchReader::CreateDataFileReader(
+    const std::shared_ptr<InputStream>& input_stream, const 
std::shared_ptr<MemoryPool>& pool) {
+    PAIMON_RETURN_NOT_OK(input_stream->Seek(0, SeekOrigin::FS_SEEK_SET));
+    try {
+        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<::avro::InputStream> in,
+                               AvroInputStreamImpl::Create(input_stream, 
BUFFER_SIZE, pool));
+        auto reader = 
std::make_unique<::avro::DataFileReaderBase>(std::move(in));
+        reader->init();
+        return reader;
+    } catch (const ::avro::Exception& e) {
+        return Status::Invalid(fmt::format("build avro reader failed. {}", 
e.what()));
+    } catch (const std::exception& e) {
+        return Status::Invalid(fmt::format("build avro reader failed. {}", 
e.what()));
+    } catch (...) {
+        return Status::Invalid("build avro reader failed. unknown error");
+    }
+}
+
+Result<BatchReader::ReadBatch> AvroFileBatchReader::NextBatch() {
+    if (next_row_to_read_ == std::numeric_limits<uint64_t>::max()) {
+        next_row_to_read_ = 0;
+    }
+    try {
+        while (array_builder_->length() < batch_size_) {
+            if (!reader_->hasMore()) {
+                break;
+            }
+            reader_->decr();
+            PAIMON_RETURN_NOT_OK(AvroDirectDecoder::DecodeAvroToBuilder(
+                reader_->dataSchema().root(), read_fields_projection_, 
&reader_->decoder(),
+                array_builder_.get(), &decode_context_));
+        }
+        previous_first_row_ = next_row_to_read_;
+        next_row_to_read_ += array_builder_->length();
+        if (array_builder_->length() == 0) {
+            return BatchReader::MakeEofBatch();
+        }
+        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
+                                          array_builder_->Finish());
+        std::unique_ptr<ArrowArray> c_array = std::make_unique<ArrowArray>();
+        std::unique_ptr<ArrowSchema> c_schema = 
std::make_unique<ArrowSchema>();
+        PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, 
c_array.get(), c_schema.get()));
+        return make_pair(std::move(c_array), std::move(c_schema));
+    } catch (const ::avro::Exception& e) {
+        return Status::Invalid(fmt::format("avro reader next batch failed. 
{}", e.what()));
+    } catch (const std::exception& e) {
+        return Status::Invalid(fmt::format("avro reader next batch failed. 
{}", e.what()));
+    } catch (...) {
+        return Status::Invalid("avro reader next batch failed. unknown error");
+    }
+}
+
+Status AvroFileBatchReader::SetReadSchema(::ArrowSchema* read_schema,
+                                          const std::shared_ptr<Predicate>& 
predicate,
+                                          const 
std::optional<RoaringBitmap32>& selection_bitmap) {
+    if (!read_schema) {
+        return Status::Invalid("SetReadSchema failed: read schema cannot be 
nullptr");
+    }
+    // TODO(menglingda.mld): support predicate
+    if (selection_bitmap) {
+        // TODO(menglingda.mld): support bitmap
+    }
+    previous_first_row_ = std::numeric_limits<uint64_t>::max();
+    next_row_to_read_ = std::numeric_limits<uint64_t>::max();
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Schema> 
arrow_read_schema,
+                                      arrow::ImportSchema(read_schema));
+    PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema> file_schema,
+                           ArrowUtils::DataTypeToSchema(file_data_type_));
+    PAIMON_ASSIGN_OR_RAISE(read_fields_projection_,
+                           CalculateReadFieldsProjection(file_schema, 
arrow_read_schema->fields()));
+    array_builder_->Reset();
+    std::shared_ptr<::arrow::DataType> read_data_type = 
arrow::struct_(arrow_read_schema->fields());
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(array_builder_,
+                                      arrow::MakeBuilder(read_data_type, 
arrow_pool_.get()));
+    return Status::OK();
+}
+
+Result<std::set<size_t>> AvroFileBatchReader::CalculateReadFieldsProjection(
+    const std::shared_ptr<::arrow::Schema>& file_schema, const 
arrow::FieldVector& read_fields) {
+    std::set<size_t> projection_set;
+    PAIMON_ASSIGN_OR_RAISE(std::vector<int32_t> projection,
+                           ArrowUtils::CreateProjection(file_schema, 
read_fields));
+    int32_t prev_index = -1;
+    for (auto& index : projection) {
+        if (index <= prev_index) {
+            return Status::Invalid(
+                "SetReadSchema failed: read schema fields order is different 
from file schema");
+        }
+        prev_index = index;
+        projection_set.insert(index);
+    }
+    return projection_set;
+}
+
+Result<std::unique_ptr<::ArrowSchema>> AvroFileBatchReader::GetFileSchema() 
const {
+    assert(reader_);
+    auto c_schema = std::make_unique<::ArrowSchema>();
+    PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportType(*file_data_type_, 
c_schema.get()));
+    return c_schema;
+}
+
+Result<uint64_t> AvroFileBatchReader::GetNumberOfRows() const {
+    if (!total_rows_) {
+        PAIMON_ASSIGN_OR_RAISE(int64_t current_pos, input_stream_->GetPos());
+        ScopeGuard stream_guard([this, current_pos]() -> void {
+            // reset input stream position to original position
+            Status status = input_stream_->Seek(current_pos, 
SeekOrigin::FS_SEEK_SET);
+            (void)status;
+        });
+        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<::avro::DataFileReaderBase> 
reader,
+                               CreateDataFileReader(input_stream_, pool_));
+        ScopeGuard reader_guard([&reader]() -> void { reader->close(); });
+        try {
+            while (reader->hasMore()) {
+                reader->decr();
+                total_rows_ = total_rows_.value_or(0) + 1;
+            }
+        } catch (const ::avro::Exception& e) {
+            return Status::Invalid(fmt::format("avro reader GetNumberOfRows 
failed. {}", e.what()));
+        } catch (const std::exception& e) {
+            return Status::Invalid(fmt::format("avro reader GetNumberOfRows 
failed. {}", e.what()));
+        } catch (...) {
+            return Status::Invalid("avro reader GetNumberOfRows failed. 
unknown error");
+        }
+    }
+    return *total_rows_;
+}
+
+}  // namespace paimon::avro
diff --git a/src/paimon/format/avro/avro_file_batch_reader.h 
b/src/paimon/format/avro/avro_file_batch_reader.h
new file mode 100644
index 0000000..e54a936
--- /dev/null
+++ b/src/paimon/format/avro/avro_file_batch_reader.h
@@ -0,0 +1,103 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <memory>
+#include <set>
+#include <utility>
+#include <vector>
+
+#include "avro/DataFile.hh"
+#include "paimon/format/avro/avro_direct_decoder.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/metrics.h"
+#include "paimon/reader/file_batch_reader.h"
+#include "paimon/result.h"
+
+namespace paimon::avro {
+
+class AvroFileBatchReader : public FileBatchReader {
+ public:
+    static Result<std::unique_ptr<AvroFileBatchReader>> Create(
+        const std::shared_ptr<InputStream>& input_stream, int32_t batch_size,
+        const std::shared_ptr<MemoryPool>& pool);
+
+    ~AvroFileBatchReader() override;
+
+    Result<BatchReader::ReadBatch> NextBatch() override;
+
+    Result<std::unique_ptr<::ArrowSchema>> GetFileSchema() const override;
+
+    Status SetReadSchema(::ArrowSchema* read_schema, const 
std::shared_ptr<Predicate>& predicate,
+                         const std::optional<RoaringBitmap32>& 
selection_bitmap) override;
+
+    Result<uint64_t> GetPreviousBatchFirstRowNumber() const override {
+        return previous_first_row_;
+    }
+
+    Result<uint64_t> GetNumberOfRows() const override;
+
+    std::shared_ptr<Metrics> GetReaderMetrics() const override {
+        return metrics_;
+    }
+
+    void Close() override {
+        DoClose();
+    }
+
+    bool SupportPreciseBitmapSelection() const override {
+        return false;
+    }
+
+ private:
+    void DoClose();
+
+    static Result<std::unique_ptr<::avro::DataFileReaderBase>> 
CreateDataFileReader(
+        const std::shared_ptr<InputStream>& input_stream, const 
std::shared_ptr<MemoryPool>& pool);
+
+    static Result<std::set<size_t>> CalculateReadFieldsProjection(
+        const std::shared_ptr<::arrow::Schema>& file_schema, const 
arrow::FieldVector& read_fields);
+
+    AvroFileBatchReader(const std::shared_ptr<InputStream>& input_stream,
+                        const std::shared_ptr<::arrow::DataType>& 
file_data_type,
+                        std::unique_ptr<::avro::DataFileReaderBase>&& reader,
+                        std::unique_ptr<arrow::ArrayBuilder>&& array_builder,
+                        std::unique_ptr<arrow::MemoryPool>&& arrow_pool, 
int32_t batch_size,
+                        const std::shared_ptr<MemoryPool>& pool);
+
+    static constexpr size_t BUFFER_SIZE = 1024 * 1024;  // 1M
+
+    std::shared_ptr<MemoryPool> pool_;
+    std::unique_ptr<arrow::MemoryPool> arrow_pool_;
+    std::shared_ptr<InputStream> input_stream_;
+    std::shared_ptr<::arrow::DataType> file_data_type_;
+    std::unique_ptr<::avro::DataFileReaderBase> reader_;
+    std::unique_ptr<arrow::ArrayBuilder> array_builder_;
+    std::optional<std::set<size_t>> read_fields_projection_;
+    uint64_t previous_first_row_ = std::numeric_limits<uint64_t>::max();
+    uint64_t next_row_to_read_ = std::numeric_limits<uint64_t>::max();
+    mutable std::optional<uint64_t> total_rows_ = std::nullopt;
+    const int32_t batch_size_;
+    bool close_ = false;
+    std::shared_ptr<Metrics> metrics_;
+    // Decode context for reusing scratch buffers
+    AvroDirectDecoder::DecodeContext decode_context_;
+};
+
+}  // namespace paimon::avro
diff --git a/src/paimon/format/avro/avro_file_batch_reader_test.cpp 
b/src/paimon/format/avro/avro_file_batch_reader_test.cpp
new file mode 100644
index 0000000..fa50679
--- /dev/null
+++ b/src/paimon/format/avro/avro_file_batch_reader_test.cpp
@@ -0,0 +1,410 @@
+/*
+ * 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.
+ */
+
+#include "paimon/format/avro/avro_file_batch_reader.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "arrow/api.h"
+#include "arrow/c/bridge.h"
+#include "arrow/ipc/api.h"
+#include "gtest/gtest.h"
+#include "paimon/common/utils/path_util.h"
+#include "paimon/core/manifest/manifest_file.h"
+#include "paimon/core/manifest/manifest_list.h"
+#include "paimon/format/file_format.h"
+#include "paimon/format/file_format_factory.h"
+#include "paimon/fs/local/local_file_system.h"
+#include "paimon/testing/utils/read_result_collector.h"
+#include "paimon/testing/utils/testharness.h"
+#include "paimon/testing/utils/timezone_guard.h"
+
+namespace paimon::avro::test {
+
+class AvroFileBatchReaderTest : public ::testing::Test, public 
::testing::WithParamInterface<bool> {
+ public:
+    void SetUp() override {
+        ASSERT_OK_AND_ASSIGN(file_format_,
+                             FileFormatFactory::Get("avro", 
{{Options::FILE_FORMAT, "avro"}}));
+        fs_ = std::make_shared<LocalFileSystem>();
+        dir_ = ::paimon::test::UniqueTestDirectory::Create();
+        ASSERT_TRUE(dir_);
+        pool_ = GetDefaultPool();
+    }
+    void TearDown() override {}
+
+    void WriteData(const std::shared_ptr<arrow::Array>& src_array, const 
std::string& file_path,
+                   const std::string& compression) {
+        arrow::Schema src_schema(src_array->type()->fields());
+        ::ArrowSchema c_schema;
+        ASSERT_TRUE(arrow::ExportSchema(src_schema, &c_schema).ok());
+        ASSERT_OK_AND_ASSIGN(auto writer_builder,
+                             file_format_->CreateWriterBuilder(&c_schema, 
/*batch_size=*/-1));
+        ASSERT_OK_AND_ASSIGN(std::shared_ptr<OutputStream> out,
+                             fs_->Create(file_path, /*overwrite=*/false));
+        ASSERT_OK_AND_ASSIGN(auto writer, writer_builder->Build(out, 
compression));
+
+        ::ArrowArray arrow_array;
+        ASSERT_TRUE(arrow::ExportArray(*src_array, &arrow_array).ok());
+        ASSERT_OK(writer->AddBatch(&arrow_array));
+        ASSERT_OK(writer->Flush());
+        ASSERT_OK(writer->Finish());
+        ASSERT_OK(out->Flush());
+        ASSERT_OK(out->Close());
+    }
+
+    std::pair<std::unique_ptr<FileBatchReader>, 
std::shared_ptr<arrow::ChunkedArray>> ReadData(
+        const std::string& file_path, int32_t read_batch_size) {
+        EXPECT_OK_AND_ASSIGN(auto reader_builder,
+                             
file_format_->CreateReaderBuilder(read_batch_size));
+        EXPECT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, 
fs_->Open(file_path));
+        EXPECT_OK_AND_ASSIGN(auto batch_reader, reader_builder->Build(in));
+        EXPECT_OK_AND_ASSIGN(auto result_array, 
::paimon::test::ReadResultCollector::CollectResult(
+                                                    batch_reader.get()));
+        return std::make_pair(std::move(batch_reader), result_array);
+    }
+
+ private:
+    std::shared_ptr<MemoryPool> pool_;
+    std::shared_ptr<FileFormat> file_format_;
+    std::shared_ptr<FileSystem> fs_;
+    std::unique_ptr<paimon::test::UniqueTestDirectory> dir_;
+};
+
+TEST_F(AvroFileBatchReaderTest, TestReadDataWithNull) {
+    std::string path = paimon::test::GetDataDir() + 
"/avro/data/avro_with_null";
+    auto [reader_holder, result_array] = ReadData(path, 
/*read_batch_size=*/1024);
+
+    arrow::FieldVector fields = {
+        arrow::field("_KEY_f0", arrow::utf8(), /*nullable=*/true),
+        arrow::field("_SEQUENCE_NUMBER", arrow::int64(), /*nullable=*/true),
+        arrow::field("_VALUE_KIND", arrow::int32(), /*nullable=*/true),
+        arrow::field("f0", arrow::utf8(), /*nullable=*/true),
+        arrow::field("f1", arrow::utf8(), /*nullable=*/true),
+        arrow::field("f2", arrow::int32(), /*nullable=*/true),
+        arrow::field("f3", arrow::float64(), /*nullable=*/true)};
+
+    auto arrow_data_type = arrow::struct_(fields);
+
+    std::shared_ptr<arrow::ChunkedArray> expected_array;
+    auto array_status = 
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow_data_type, {R"([
+        ["Alex", 2, 3, "Alex", "20250326", 18,   10.1],
+        ["Bob",  3, 3, "Bob",  "20250326", 19,   11.1],
+        ["Evan", 1, 0, "Evan", "20250326", null, 14.1]
+    ])"},
+                                                                         
&expected_array);
+    ASSERT_TRUE(array_status.ok()) << array_status.ToString();
+    ASSERT_TRUE(result_array->Equals(expected_array));
+    ASSERT_TRUE(expected_array->Equals(result_array));
+    auto read_metrics = reader_holder->GetReaderMetrics();
+    ASSERT_TRUE(read_metrics);
+}
+
+TEST_F(AvroFileBatchReaderTest, TestReadWithDifferentBatchSize) {
+    std::string file_path = PathUtil::JoinPath(dir_->Str(), "file.avro");
+
+    arrow::FieldVector fields = {
+        arrow::field("f0", arrow::boolean()), arrow::field("f1", 
arrow::int32()),
+        arrow::field("f2", arrow::int64()),   arrow::field("f3", 
arrow::float32()),
+        arrow::field("f4", arrow::float64()), arrow::field("f5", 
arrow::utf8()),
+        arrow::field("f6", arrow::binary())};
+    auto arrow_data_type = arrow::struct_(fields);
+
+    size_t length = 600;
+    std::string data_str = "[";
+    for (size_t i = 0; i < length; i++) {
+        if (i % 3 == 0) {
+            data_str.append(fmt::format(R"([{}, {}, {}, {}, {}, "str_{}", 
"bin_{}"])", "true", i,
+                                        i * 100000000000L, i * 0.12, i * 
123.45678901, i, i));
+        } else if (i % 3 == 1) {
+            data_str.append(fmt::format(R"([{}, -{}, -{}, -{}, -{}, 
"string_{}", "binary_{}"])",
+                                        "false", i, i * 100000000000L, i * 
0.12, i * 123.45678901,
+                                        i, i));
+        } else {
+            data_str.append("[null, null, null, null, null, null, null]");
+        }
+        if (i != length - 1) {
+            data_str.append(",");
+        }
+    }
+    data_str.append("]");
+
+    std::shared_ptr<arrow::Array> src_array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, 
data_str).ValueOrDie();
+    ASSERT_TRUE(src_array);
+    WriteData(src_array, file_path, /*compression=*/"zstd");
+
+    for (int32_t batch_size : {1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1}) {
+        auto [reader_holder, result_array] = ReadData(file_path, batch_size);
+        std::shared_ptr<arrow::ChunkedArray> expected_array;
+        auto array_status = arrow::ipc::internal::json::ChunkedArrayFromJSON(
+            arrow_data_type, {data_str}, &expected_array);
+        ASSERT_TRUE(array_status.ok()) << array_status.ToString();
+        ASSERT_TRUE(result_array->Equals(expected_array));
+        ASSERT_TRUE(expected_array->Equals(result_array));
+    }
+}
+
+TEST_F(AvroFileBatchReaderTest, TestReadAllTypes) {
+    std::string path = paimon::test::GetDataDir() + 
"/avro/data/avro_all_types";
+    auto [reader_holder, result_array] = ReadData(path, 
/*read_batch_size=*/1024);
+
+    arrow::FieldVector fields = {
+        arrow::field("f0", arrow::boolean()),
+        arrow::field("f1", arrow::int32()),
+        arrow::field("f2", arrow::int32()),
+        arrow::field("f3", arrow::int32()),
+        arrow::field("f4", arrow::int64()),
+        arrow::field("f5", arrow::float32()),
+        arrow::field("f6", arrow::float64()),
+        arrow::field("f7", arrow::utf8()),
+        arrow::field("f8", arrow::binary()),
+        arrow::field("f10", arrow::list(arrow::float32())),
+        arrow::field("f11", arrow::struct_({arrow::field("f0", 
arrow::boolean()),
+                                            arrow::field("f1", 
arrow::int64())})),
+        arrow::field("f12", arrow::timestamp(arrow::TimeUnit::MICRO)),
+        arrow::field("f13", arrow::date32()),
+        arrow::field("f14", arrow::decimal128(2, 2)),
+        arrow::field("f15", arrow::decimal128(10, 10)),
+        arrow::field("f16", arrow::decimal128(19, 19))};
+
+    auto arrow_data_type = arrow::struct_(fields);
+    std::shared_ptr<arrow::ChunkedArray> expected_array;
+    auto array_status = 
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow_data_type, {R"([
+        [true, 127, 32767, 2147483647, 9999999999999, 1234.56, 
1234567890.0987654321, "aa", "qq", [0.1, 0.2], [true, null], "1970-01-01 
00:02:03.123123", 2456, "0.22", "0.1234567890", "0.1234567890987654321"],
+        [false, -128, -32768, -2147483648, -9999999999999, -1234.56, 
-1234567890.0987654321, null, "ww", [-0.1, -0.2, null, 0.3, 0.4], [null, 2], 
"1970-01-01 00:16:39.999999", null, "-0.22", "-0.1234567890", null],
+        [null, null, null, null, null, null, null, null, null, null, null, 
null, null, null, null, null]
+    ])"},
+                                                                         
&expected_array);
+    ASSERT_TRUE(array_status.ok()) << array_status.ToString();
+    ASSERT_TRUE(result_array->Equals(expected_array)) << 
result_array->ToString();
+    ASSERT_TRUE(expected_array->Equals(result_array)) << 
result_array->ToString();
+}
+
+TEST_P(AvroFileBatchReaderTest, TestReadTimestampTypes) {
+    auto enable_tz = GetParam();
+    std::string timezone_str = enable_tz ? "Asia/Tokyo" : "Asia/Shanghai";
+    paimon::test::TimezoneGuard tz_guard(timezone_str);
+
+    std::string path = paimon::test::GetDataDir() +
+                       
"/avro/append_with_multiple_ts_precision_and_timezone.db/"
+                       
"append_with_multiple_ts_precision_and_timezone/bucket-0/"
+                       "data-441e233b-529d-4a8f-a0a4-25c2c84fb965-0.avro";
+
+    ASSERT_OK_AND_ASSIGN(auto reader_builder,
+                         
file_format_->CreateReaderBuilder(/*batch_size=*/1024));
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, fs_->Open(path));
+    ASSERT_OK_AND_ASSIGN(auto batch_reader, reader_builder->Build(in));
+
+    auto timezone = DateTimeUtils::GetLocalTimezoneName();
+    arrow::FieldVector read_fields = {
+        arrow::field("ts_sec", arrow::timestamp(arrow::TimeUnit::SECOND)),
+        arrow::field("ts_milli", arrow::timestamp(arrow::TimeUnit::MILLI)),
+        arrow::field("ts_micro", arrow::timestamp(arrow::TimeUnit::MICRO)),
+        arrow::field("ts_tz_sec", arrow::timestamp(arrow::TimeUnit::SECOND, 
timezone)),
+        arrow::field("ts_tz_milli", arrow::timestamp(arrow::TimeUnit::MILLI, 
timezone)),
+        arrow::field("ts_tz_micro", arrow::timestamp(arrow::TimeUnit::MICRO, 
timezone)),
+    };
+    auto read_schema = arrow::schema(read_fields);
+    std::unique_ptr<ArrowSchema> c_schema = std::make_unique<ArrowSchema>();
+    ASSERT_TRUE(arrow::ExportSchema(*read_schema, c_schema.get()).ok());
+    EXPECT_OK(batch_reader->SetReadSchema(c_schema.get(), 
/*predicate=*/nullptr,
+                                          /*selection_bitmap=*/std::nullopt));
+
+    // check array
+    ASSERT_OK_AND_ASSIGN(auto result_array,
+                         
::paimon::test::ReadResultCollector::CollectResult(batch_reader.get()));
+    std::shared_ptr<arrow::ChunkedArray> expected_array;
+    auto array_status =
+        
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow::struct_(read_fields), 
{R"([
+        
["1970-01-01T00:00:01","1970-01-01T00:00:00.001","1970-01-01T00:00:00.000001","1970-01-01T00:00:02","1970-01-01T00:00:00.002","1970-01-01T00:00:00.000002"],
+        
[null,"1970-01-01T00:00:00.003",null,null,"1970-01-01T00:00:00.004",null],
+        
["1970-01-01T00:00:05",null,"1970-01-01T00:00:00.000005","1970-01-01T00:00:06",null,"1970-01-01T00:00:00.000006"]
+    ])"},
+                                                         &expected_array);
+    ASSERT_TRUE(array_status.ok()) << array_status.ToString();
+    ASSERT_TRUE(result_array->Equals(expected_array)) << 
result_array->ToString();
+    ASSERT_TRUE(expected_array->Equals(result_array));
+}
+
+TEST_F(AvroFileBatchReaderTest, TestReadMapTypes) {
+    std::string path = paimon::test::GetDataDir() +
+                       "/avro/append_with_multiple_map.db/"
+                       "append_with_multiple_map/bucket-0/"
+                       "data-72442742-e49e-48a4-a736-a2475aac2d2c-0.avro";
+
+    ASSERT_OK_AND_ASSIGN(auto reader_builder,
+                         
file_format_->CreateReaderBuilder(/*batch_size=*/1024));
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, fs_->Open(path));
+    ASSERT_OK_AND_ASSIGN(auto batch_reader, reader_builder->Build(in));
+
+    arrow::FieldVector read_fields = {
+        arrow::field("f0", arrow::map(arrow::int32(), arrow::int32())),
+        arrow::field("f1", arrow::map(arrow::float64(), arrow::float64())),
+        arrow::field("f2", arrow::map(arrow::utf8(), arrow::utf8())),
+        arrow::field("f3", arrow::map(arrow::utf8(), arrow::binary())),
+        arrow::field("f4", arrow::map(arrow::timestamp(arrow::TimeUnit::MICRO),
+                                      
arrow::timestamp(arrow::TimeUnit::MICRO))),
+        arrow::field("f5", arrow::map(arrow::utf8(), 
arrow::list(arrow::float64()))),
+        arrow::field("f6", arrow::map(arrow::utf8(), 
arrow::map(arrow::float64(), arrow::utf8()))),
+        arrow::field("f7", arrow::map(arrow::int64(),
+                                      arrow::struct_({field("f0", 
arrow::int32()),
+                                                      field("f1", 
arrow::utf8()),
+                                                      field("f2", 
arrow::decimal128(5, 2))})))};
+    auto read_schema = arrow::schema(read_fields);
+    std::unique_ptr<ArrowSchema> c_schema = std::make_unique<ArrowSchema>();
+    ASSERT_TRUE(arrow::ExportSchema(*read_schema, c_schema.get()).ok());
+    EXPECT_OK(batch_reader->SetReadSchema(c_schema.get(), 
/*predicate=*/nullptr,
+                                          /*selection_bitmap=*/std::nullopt));
+
+    // check array
+    ASSERT_OK_AND_ASSIGN(auto result_array,
+                         
::paimon::test::ReadResultCollector::CollectResult(batch_reader.get()));
+    std::shared_ptr<arrow::ChunkedArray> expected_array;
+    auto array_status =
+        
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow::struct_(read_fields), 
{R"([
+        [
+            [[1,10],[2,20]],
+            [[1.1,10.1],[2.2,20.2]],
+            [["key1","val1"],["key2","val2"]],
+            [["123456","abcdef"]],
+            [["2023-01-01 12:00:00.123000","2023-01-01 
12:00:00.123000"],["2023-01-02 13:30:00.456000","2023-01-02 13:30:00.456000"]],
+            [["arr_key",[1.5, 2.5, 3.5]]],
+            [["outer_key",[[99.9,"nested_val"]]]],
+            [[1000, [42, "row_str", "123.45"]]]
+        ]
+    ])"},
+                                                         &expected_array);
+    ASSERT_TRUE(array_status.ok()) << array_status.ToString();
+    ASSERT_TRUE(result_array->Equals(expected_array)) << 
result_array->ToString() << std::endl;
+    ASSERT_TRUE(expected_array->Equals(result_array));
+}
+
+TEST_F(AvroFileBatchReaderTest, TestGetPreviousBatchFirstRowNumber) {
+    std::string path = paimon::test::GetDataDir() +
+                       "/avro/append_simple.db/"
+                       "append_simple/bucket-0/"
+                       "data-d7d1c416-6e34-4834-af87-341d09418f0c-0.avro";
+
+    ASSERT_OK_AND_ASSIGN(auto reader_builder, 
file_format_->CreateReaderBuilder(/*batch_size=*/1));
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, fs_->Open(path));
+    ASSERT_OK_AND_ASSIGN(auto reader, reader_builder->Build(in));
+
+    arrow::FieldVector read_fields = {
+        arrow::field("f0", arrow::int32()), arrow::field("f1", 
arrow::float64()),
+        arrow::field("f2", arrow::utf8()),
+        arrow::field("f3",
+                     arrow::struct_({arrow::field("f0", 
arrow::map(arrow::utf8(), arrow::int32())),
+                                     arrow::field("f1", 
arrow::list(arrow::int32()))}))};
+
+    auto read_schema = arrow::schema(read_fields);
+    std::unique_ptr<ArrowSchema> c_schema = std::make_unique<ArrowSchema>();
+    ASSERT_TRUE(arrow::ExportSchema(*read_schema, c_schema.get()).ok());
+    EXPECT_OK(reader->SetReadSchema(c_schema.get(), /*predicate=*/nullptr,
+                                    /*selection_bitmap=*/std::nullopt));
+
+    ASSERT_OK_AND_ASSIGN(auto num_rows, reader->GetNumberOfRows());
+    ASSERT_EQ(4, num_rows);
+    ASSERT_EQ(std::numeric_limits<uint64_t>::max(),
+              reader->GetPreviousBatchFirstRowNumber().value());
+    ASSERT_OK_AND_ASSIGN(auto batch1, reader->NextBatch());
+    ArrowArrayRelease(batch1.first.get());
+    ArrowSchemaRelease(batch1.second.get());
+    ASSERT_EQ(0, reader->GetPreviousBatchFirstRowNumber().value());
+    ASSERT_OK_AND_ASSIGN(auto batch2, reader->NextBatch());
+    ASSERT_EQ(1, reader->GetPreviousBatchFirstRowNumber().value());
+    ArrowArrayRelease(batch2.first.get());
+    ArrowSchemaRelease(batch2.second.get());
+    ASSERT_OK_AND_ASSIGN(auto batch3, reader->NextBatch());
+    ASSERT_EQ(2, reader->GetPreviousBatchFirstRowNumber().value());
+    ArrowArrayRelease(batch3.first.get());
+    ArrowSchemaRelease(batch3.second.get());
+    ASSERT_OK_AND_ASSIGN(auto batch4, reader->NextBatch());
+    ASSERT_EQ(3, reader->GetPreviousBatchFirstRowNumber().value());
+    ArrowArrayRelease(batch4.first.get());
+    ArrowSchemaRelease(batch4.second.get());
+    ASSERT_OK_AND_ASSIGN(auto batch5, reader->NextBatch());
+    ASSERT_EQ(4, reader->GetPreviousBatchFirstRowNumber().value());
+    ASSERT_TRUE(BatchReader::IsEofBatch(batch5));
+}
+
+TEST_F(AvroFileBatchReaderTest, TestGetNumberOfRows) {
+    std::string file_path = PathUtil::JoinPath(dir_->Str(), "file.avro");
+
+    arrow::FieldVector fields = {
+        arrow::field("f0", arrow::boolean()), arrow::field("f1", 
arrow::int32()),
+        arrow::field("f2", arrow::int64()),   arrow::field("f3", 
arrow::float32()),
+        arrow::field("f4", arrow::float64()), arrow::field("f5", 
arrow::utf8()),
+        arrow::field("f6", arrow::binary())};
+    auto arrow_data_type = arrow::struct_(fields);
+
+    size_t length = 102400;
+    std::string data_str = "[";
+    for (size_t i = 0; i < length; i++) {
+        data_str.append(fmt::format(R"([{}, {}, {}, {}, {}, "str_{}", 
"bin_{}"])", "true", i,
+                                    i * 100000000000L, i * 0.12, i * 
123.45678901, i, i));
+        if (i != length - 1) {
+            data_str.append(",");
+        }
+    }
+    data_str.append("]");
+
+    std::shared_ptr<arrow::Array> src_array =
+        arrow::ipc::internal::json::ArrayFromJSON(arrow_data_type, 
data_str).ValueOrDie();
+    ASSERT_TRUE(src_array);
+    WriteData(src_array, file_path, /*compression=*/"null");
+
+    ASSERT_OK_AND_ASSIGN(auto reader_builder, 
file_format_->CreateReaderBuilder(25600));
+
+    // check GetNumberOfRows can be called at any position, and continue read
+    int32_t expected_batches = 4;
+    for (int32_t pos = 0; pos < expected_batches; pos++) {
+        ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> in, 
fs_->Open(file_path));
+        ASSERT_OK_AND_ASSIGN(auto reader, reader_builder->Build(in));
+
+        arrow::ArrayVector result_array_vector;
+        for (int32_t i = 0; i < pos; i++) {
+            ASSERT_OK_AND_ASSIGN(auto batch, reader->NextBatch());
+            auto result_array =
+                arrow::ImportArray(batch.first.get(), 
batch.second.get()).ValueOrDie();
+            result_array_vector.push_back(result_array);
+        }
+        // check number of rows, and continue read
+        ASSERT_OK_AND_ASSIGN(auto num_rows, reader->GetNumberOfRows());
+        ASSERT_EQ(length, num_rows);
+        for (int32_t i = pos; i < expected_batches; i++) {
+            ASSERT_OK_AND_ASSIGN(auto batch, reader->NextBatch());
+            auto result_array =
+                arrow::ImportArray(batch.first.get(), 
batch.second.get()).ValueOrDie();
+            result_array_vector.push_back(result_array);
+        }
+        ASSERT_OK_AND_ASSIGN(auto eof_batch, reader->NextBatch());
+        ASSERT_TRUE(BatchReader::IsEofBatch(eof_batch));
+        ASSERT_OK_AND_ASSIGN(num_rows, reader->GetNumberOfRows());
+        ASSERT_EQ(length, num_rows);
+
+        auto result_array = arrow::ChunkedArray(result_array_vector);
+        ASSERT_TRUE(result_array.Equals(arrow::ChunkedArray(src_array)));
+    }
+}
+
+INSTANTIATE_TEST_SUITE_P(TestParam, AvroFileBatchReaderTest, 
::testing::Values(false, true));
+
+}  // namespace paimon::avro::test
diff --git a/src/paimon/format/avro/avro_format_writer.cpp 
b/src/paimon/format/avro/avro_format_writer.cpp
new file mode 100644
index 0000000..e4ad454
--- /dev/null
+++ b/src/paimon/format/avro/avro_format_writer.cpp
@@ -0,0 +1,136 @@
+/*
+ * 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.
+ */
+
+#include "paimon/format/avro/avro_format_writer.h"
+
+#include <cassert>
+#include <exception>
+#include <memory>
+#include <utility>
+
+#include "arrow/api.h"
+#include "arrow/c/bridge.h"
+#include "avro/Compiler.hh"  // IWYU pragma: keep
+#include "avro/DataFile.hh"
+#include "avro/Exception.hh"
+#include "avro/Generic.hh"   // IWYU pragma: keep
+#include "avro/Specific.hh"  // IWYU pragma: keep
+#include "avro/ValidSchema.hh"
+#include "fmt/format.h"
+#include "paimon/common/metrics/metrics_impl.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/format/avro/avro_schema_converter.h"
+
+namespace arrow {
+class Array;
+}  // namespace arrow
+struct ArrowArray;
+
+namespace paimon::avro {
+
+AvroFormatWriter::AvroFormatWriter(std::unique_ptr<::avro::DataFileWriterBase>&&
 file_writer,
+                                   const ::avro::ValidSchema& avro_schema,
+                                   const std::shared_ptr<arrow::DataType>& 
data_type,
+                                   AvroOutputStreamImpl* avro_output_stream)
+    : writer_(std::move(file_writer)),
+      avro_schema_(avro_schema),
+      data_type_(data_type),
+      metrics_(std::make_shared<MetricsImpl>()),
+      avro_output_stream_(avro_output_stream) {}
+
+Result<std::unique_ptr<AvroFormatWriter>> AvroFormatWriter::Create(
+    std::unique_ptr<AvroOutputStreamImpl> out, const 
std::shared_ptr<arrow::Schema>& schema,
+    const ::avro::Codec codec, std::optional<int32_t> compression_level) {
+    try {
+        PAIMON_ASSIGN_OR_RAISE(::avro::ValidSchema avro_schema,
+                               
AvroSchemaConverter::ArrowSchemaToAvroSchema(schema));
+        AvroOutputStreamImpl* avro_output_stream = out.get();
+        auto writer = std::make_unique<::avro::DataFileWriterBase>(
+            std::move(out), avro_schema, DEFAULT_SYNC_INTERVAL, codec, 
::avro::Metadata(),
+            compression_level);
+        auto data_type = arrow::struct_(schema->fields());
+        return std::unique_ptr<AvroFormatWriter>(
+            new AvroFormatWriter(std::move(writer), avro_schema, data_type, 
avro_output_stream));
+    } catch (const ::avro::Exception& e) {
+        return Status::Invalid(fmt::format("avro format writer create failed. 
{}", e.what()));
+    } catch (const std::exception& e) {
+        return Status::Invalid(fmt::format("avro format writer create failed: 
{}", e.what()));
+    } catch (...) {
+        return Status::Invalid("avro format writer create failed: unknown 
exception");
+    }
+}
+
+Status AvroFormatWriter::Flush() {
+    try {
+        writer_->flush();
+    } catch (const ::avro::Exception& e) {
+        return Status::Invalid(fmt::format("avro writer flush failed. {}", 
e.what()));
+    } catch (const std::exception& e) {
+        return Status::Invalid(fmt::format("avro writer flush failed: {}", 
e.what()));
+    } catch (...) {
+        return Status::Invalid("avro writer flush failed: unknown exception");
+    }
+
+    return Status::OK();
+}
+
+Status AvroFormatWriter::Finish() {
+    try {
+        avro_output_stream_->FlushBuffer();  // we need flush buffer before 
close writer
+        writer_->close();
+    } catch (const ::avro::Exception& e) {
+        return Status::Invalid(fmt::format("avro writer close failed. {}", 
e.what()));
+    } catch (const std::exception& e) {
+        return Status::Invalid(fmt::format("avro writer close failed: {}", 
e.what()));
+    } catch (...) {
+        return Status::Invalid("avro writer close failed: unknown exception");
+    }
+    return Status::OK();
+}
+
+Result<bool> AvroFormatWriter::ReachTargetSize(bool suggested_check, int64_t 
target_size) const {
+    if (suggested_check) {
+        uint64_t current_size = writer_->getCurrentBlockStart();
+        return current_size >= static_cast<uint64_t>(target_size);
+    }
+    return false;
+}
+
+Status AvroFormatWriter::AddBatch(ArrowArray* batch) {
+    assert(batch);
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> 
arrow_array,
+                                      arrow::ImportArray(batch, data_type_));
+    try {
+        for (int64_t row_index = 0; row_index < arrow_array->length(); 
++row_index) {
+            writer_->syncIfNeeded();
+            PAIMON_RETURN_NOT_OK(AvroDirectEncoder::EncodeArrowToAvro(
+                avro_schema_.root(), *arrow_array, row_index, 
&writer_->encoder(), &encode_ctx_));
+            writer_->incr();
+        }
+    } catch (const ::avro::Exception& e) {
+        return Status::Invalid(fmt::format("avro writer add batch failed. {}", 
e.what()));
+    } catch (const std::exception& e) {
+        return Status::Invalid(fmt::format("avro writer add batch failed: {}", 
e.what()));
+    } catch (...) {
+        return Status::Invalid("avro writer add batch failed: unknown 
exception");
+    }
+    PAIMON_RETURN_NOT_OK(Flush());
+    return Status::OK();
+}
+
+}  // namespace paimon::avro
diff --git a/src/paimon/format/avro/avro_format_writer.h 
b/src/paimon/format/avro/avro_format_writer.h
new file mode 100644
index 0000000..64427c3
--- /dev/null
+++ b/src/paimon/format/avro/avro_format_writer.h
@@ -0,0 +1,86 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <optional>
+
+#include "arrow/api.h"
+#include "avro/DataFile.hh"
+#include "avro/ValidSchema.hh"
+#include "paimon/format/avro/avro_direct_encoder.h"
+#include "paimon/format/avro/avro_output_stream_impl.h"
+#include "paimon/format/format_writer.h"
+#include "paimon/metrics.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace arrow {
+class DataType;
+class Schema;
+}  // namespace arrow
+namespace avro {
+class GenericDatum;
+}  // namespace avro
+namespace paimon {
+class Metrics;
+}  // namespace paimon
+struct ArrowArray;
+
+namespace paimon::avro {
+
+/// A `FormatWriter` implementation that writes data in Avro format.
+class AvroFormatWriter : public FormatWriter {
+ public:
+    static Result<std::unique_ptr<AvroFormatWriter>> Create(
+        std::unique_ptr<AvroOutputStreamImpl> out, const 
std::shared_ptr<arrow::Schema>& schema,
+        const ::avro::Codec codec, std::optional<int32_t> compression_level);
+
+    Status AddBatch(ArrowArray* batch) override;
+
+    Status Flush() override;
+
+    Status Finish() override;
+
+    Result<bool> ReachTargetSize(bool suggested_check, int64_t target_size) 
const override;
+
+    std::shared_ptr<Metrics> GetWriterMetrics() const override {
+        return metrics_;
+    }
+
+ private:
+    static constexpr size_t DEFAULT_SYNC_INTERVAL = 64 * 1024;
+
+    AvroFormatWriter(std::unique_ptr<::avro::DataFileWriterBase>&& file_writer,
+                     const ::avro::ValidSchema& avro_schema,
+                     const std::shared_ptr<arrow::DataType>& data_type,
+                     AvroOutputStreamImpl* avro_output_stream);
+
+    std::unique_ptr<::avro::DataFileWriterBase> writer_;
+    ::avro::ValidSchema avro_schema_;
+    std::shared_ptr<arrow::DataType> data_type_;
+    std::shared_ptr<Metrics> metrics_;
+    AvroOutputStreamImpl* avro_output_stream_;
+    // Encode context for reusing scratch buffers
+    AvroDirectEncoder::EncodeContext encode_ctx_;
+};
+
+}  // namespace paimon::avro
diff --git a/src/paimon/format/avro/avro_format_writer_test.cpp 
b/src/paimon/format/avro/avro_format_writer_test.cpp
new file mode 100644
index 0000000..965c183
--- /dev/null
+++ b/src/paimon/format/avro/avro_format_writer_test.cpp
@@ -0,0 +1,256 @@
+/*
+ * 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.
+ */
+
+#include "paimon/format/avro/avro_format_writer.h"
+
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "arrow/api.h"
+#include "arrow/c/abi.h"
+#include "arrow/c/bridge.h"
+#include "arrow/io/file.h"
+#include "arrow/memory_pool.h"
+#include "gtest/gtest.h"
+#include "paimon/common/utils/arrow/mem_utils.h"
+#include "paimon/common/utils/path_util.h"
+#include "paimon/format/avro/avro_file_batch_reader.h"
+#include "paimon/format/file_format.h"
+#include "paimon/format/file_format_factory.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/fs/local/local_file_system.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/metrics.h"
+#include "paimon/record_batch.h"
+#include "paimon/testing/utils/read_result_collector.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::avro::test {
+
+class AvroFormatWriterTest : public ::testing::Test {
+ public:
+    void SetUp() override {
+        dir_ = paimon::test::UniqueTestDirectory::Create();
+        ASSERT_TRUE(dir_);
+        fs_ = std::make_shared<LocalFileSystem>();
+        pool_ = GetDefaultPool();
+        arrow_pool_ = GetArrowPool(pool_);
+    }
+    void TearDown() override {}
+
+    std::pair<std::shared_ptr<arrow::Schema>, 
std::shared_ptr<arrow::DataType>> PrepareArrowSchema()
+        const {
+        auto string_field = arrow::field("col1", arrow::utf8());
+        auto int_field = arrow::field("col2", arrow::int32());
+        auto bool_field = arrow::field("col3", arrow::boolean());
+        auto struct_type = arrow::struct_({string_field, int_field, 
bool_field});
+        return std::make_pair(
+            arrow::schema(arrow::FieldVector({string_field, int_field, 
bool_field})), struct_type);
+    }
+
+    std::shared_ptr<FormatWriter> CreateFormatWriter(const 
std::shared_ptr<arrow::Schema>& schema,
+                                                     const 
std::shared_ptr<OutputStream>& out,
+                                                     int32_t batch_size) {
+        ::ArrowSchema c_schema;
+        EXPECT_TRUE(arrow::ExportSchema(*schema, &c_schema).ok());
+        EXPECT_OK_AND_ASSIGN(auto file_format,
+                             FileFormatFactory::Get("avro", 
{{Options::FILE_FORMAT, "avro"}}));
+        EXPECT_OK_AND_ASSIGN(auto writer_builder,
+                             file_format->CreateWriterBuilder(&c_schema, 
batch_size));
+        EXPECT_OK_AND_ASSIGN(std::shared_ptr<FormatWriter> writer,
+                             writer_builder->Build(out, "zstd"));
+        return writer;
+    }
+
+    std::shared_ptr<arrow::Array> PrepareArray(const 
std::shared_ptr<arrow::DataType>& data_type,
+                                               int32_t record_batch_size,
+                                               int32_t offset = 0) const {
+        arrow::StructBuilder struct_builder(
+            data_type, arrow::default_memory_pool(),
+            {std::make_shared<arrow::StringBuilder>(), 
std::make_shared<arrow::Int32Builder>(),
+             std::make_shared<arrow::BooleanBuilder>()});
+        auto string_builder = 
static_cast<arrow::StringBuilder*>(struct_builder.field_builder(0));
+        auto int_builder = 
static_cast<arrow::Int32Builder*>(struct_builder.field_builder(1));
+        auto bool_builder = 
static_cast<arrow::BooleanBuilder*>(struct_builder.field_builder(2));
+        for (int32_t i = 0 + offset; i < record_batch_size + offset; ++i) {
+            EXPECT_TRUE(struct_builder.Append().ok());
+            EXPECT_TRUE(string_builder->Append("str_" + 
std::to_string(i)).ok());
+            if (i % 3 == 0) {
+                // test null
+                EXPECT_TRUE(int_builder->AppendNull().ok());
+            } else {
+                EXPECT_TRUE(int_builder->Append(i).ok());
+            }
+            EXPECT_TRUE(bool_builder->Append(static_cast<bool>(i % 2)).ok());
+        }
+        std::shared_ptr<arrow::Array> array;
+        EXPECT_TRUE(struct_builder.Finish(&array).ok());
+        return array;
+    }
+
+    void AddRecordBatchOnce(const std::shared_ptr<FormatWriter>& format_writer,
+                            const std::shared_ptr<arrow::DataType>& 
struct_type,
+                            int32_t record_batch_size, int32_t offset) const {
+        auto array = PrepareArray(struct_type, record_batch_size, offset);
+        auto arrow_array = std::make_unique<ArrowArray>();
+        ASSERT_TRUE(arrow::ExportArray(*array, arrow_array.get()).ok());
+        auto batch = std::make_shared<RecordBatch>(
+            /*partition=*/std::map<std::string, std::string>(), /*bucket=*/-1,
+            /*row_kinds=*/std::vector<RecordBatch::RowKind>(), 
arrow_array.get());
+        ASSERT_OK(format_writer->AddBatch(batch->GetData()));
+    }
+
+    void CheckResult(const std::string& file_path, int32_t row_count) const {
+        ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> input_stream, 
fs_->Open(file_path));
+        ASSERT_OK_AND_ASSIGN(auto file_reader,
+                             AvroFileBatchReader::Create(input_stream, 1024, 
pool_));
+        ASSERT_OK_AND_ASSIGN(uint64_t num_rows, 
file_reader->GetNumberOfRows());
+        ASSERT_EQ(num_rows, row_count);
+
+        ASSERT_OK_AND_ASSIGN(auto result_array,
+                             
::paimon::test::ReadResultCollector::CollectResult(file_reader.get()));
+        const auto& struct_array =
+            
std::static_pointer_cast<arrow::StructArray>(result_array->chunk(0));
+        const auto& string_array =
+            
std::static_pointer_cast<arrow::StringArray>(struct_array->field(0));
+        ASSERT_TRUE(string_array);
+        const auto& int_array = 
std::static_pointer_cast<arrow::Int32Array>(struct_array->field(1));
+        ASSERT_TRUE(int_array);
+        const auto& bool_array =
+            
std::static_pointer_cast<arrow::BooleanArray>(struct_array->field(2));
+        ASSERT_TRUE(bool_array);
+        ASSERT_EQ(string_array->null_count(), 0);
+        ASSERT_EQ(int_array->null_count(), (row_count - 1) / 3 + 1);
+        ASSERT_EQ(bool_array->null_count(), 0);
+
+        for (int32_t i = 0; i < row_count; i++) {
+            ASSERT_EQ("str_" + std::to_string(i), string_array->GetString(i));
+            if (i % 3 == 0) {
+                ASSERT_TRUE(int_array->IsNull(i));
+            } else {
+                ASSERT_FALSE(int_array->IsNull(i));
+                ASSERT_EQ(i, int_array->Value(i));
+            }
+            if (i % 2 == 0) {
+                ASSERT_EQ(false, bool_array->Value(i));
+            } else {
+                ASSERT_EQ(true, bool_array->Value(i));
+            }
+        }
+    }
+
+ private:
+    std::unique_ptr<paimon::test::UniqueTestDirectory> dir_;
+    std::shared_ptr<FileSystem> fs_;
+    std::shared_ptr<MemoryPool> pool_;
+    std::shared_ptr<arrow::MemoryPool> arrow_pool_;
+};
+
+TEST_F(AvroFormatWriterTest, TestWriteWithVariousBatchSize) {
+    auto schema_pair = PrepareArrowSchema();
+    const auto& arrow_schema = schema_pair.first;
+    const auto& struct_type = schema_pair.second;
+    std::map<std::string, std::string> options;
+    for (auto record_batch_size : {1, 2, 3, 5, 20}) {
+        for (auto batch_capacity : {1, 2, 3, 5, 20}) {
+            std::string file_name =
+                std::to_string(record_batch_size) + "_" + 
std::to_string(batch_capacity);
+            std::string file_path = PathUtil::JoinPath(dir_->Str(), file_name);
+            ASSERT_OK_AND_ASSIGN(std::shared_ptr<OutputStream> out,
+                                 fs_->Create(file_path, /*overwrite=*/false));
+            auto format_writer = CreateFormatWriter(arrow_schema, out, 
batch_capacity);
+            auto array = PrepareArray(struct_type, record_batch_size);
+            auto arrow_array = std::make_unique<ArrowArray>();
+            ASSERT_TRUE(arrow::ExportArray(*array, arrow_array.get()).ok());
+
+            auto batch = std::make_shared<RecordBatch>(
+                /*partition=*/std::map<std::string, std::string>(), 
/*bucket=*/-1,
+                /*row_kinds=*/std::vector<RecordBatch::RowKind>(), 
arrow_array.get());
+            ASSERT_OK(format_writer->AddBatch(batch->GetData()));
+            ASSERT_OK(format_writer->Flush());
+            ASSERT_OK(format_writer->Finish());
+            ASSERT_OK(out->Flush());
+            ASSERT_OK(out->Close());
+            CheckResult(file_path, record_batch_size);
+        }
+    }
+}
+
+TEST_F(AvroFormatWriterTest, TestWriteMultipleTimes) {
+    // arrow array length = 6 + 10 + 15 + 6 = 37
+    // avro batch capacity = 10
+    auto schema_pair = PrepareArrowSchema();
+    const auto& arrow_schema = schema_pair.first;
+    const auto& struct_type = schema_pair.second;
+
+    std::string file_path = PathUtil::JoinPath(dir_->Str(), 
"write_multiple_times");
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<OutputStream> out,
+                         fs_->Create(file_path, /*overwrite=*/false));
+    auto format_writer = CreateFormatWriter(arrow_schema, out, 
/*batch_size=*/10);
+
+    // add batch first time, 6 rows
+    AddRecordBatchOnce(format_writer, struct_type, 6, 0);
+    // add batch second times, 10 rows
+    AddRecordBatchOnce(format_writer, struct_type, 10, 6);
+    // add batch third times, 15 rows (expand internal batch)
+    AddRecordBatchOnce(format_writer, struct_type, 15, 16);
+    // add batch fourth times, 6 rows
+    AddRecordBatchOnce(format_writer, struct_type, 6, 31);
+
+    ASSERT_OK(format_writer->Flush());
+    ASSERT_OK(format_writer->Finish());
+    ASSERT_OK(out->Flush());
+    ASSERT_OK(out->Close());
+    CheckResult(file_path, /*row_count=*/37);
+    auto metrics = format_writer->GetWriterMetrics();
+    ASSERT_TRUE(metrics);
+}
+
+TEST_F(AvroFormatWriterTest, TestGetEstimateLength) {
+    auto schema_pair = PrepareArrowSchema();
+    const auto& arrow_schema = schema_pair.first;
+    const auto& struct_type = schema_pair.second;
+
+    std::string file_path = PathUtil::JoinPath(dir_->Str(), 
"get_estimate_length");
+    ASSERT_OK_AND_ASSIGN(std::shared_ptr<OutputStream> out,
+                         fs_->Create(file_path, /*overwrite=*/false));
+    auto format_writer = CreateFormatWriter(arrow_schema, out, 
/*batch_size=*/1024);
+
+    // add batch first time, 1 row
+    AddRecordBatchOnce(format_writer, struct_type, 1, 0);
+    ASSERT_OK_AND_ASSIGN(bool reach_target_size,
+                         
format_writer->ReachTargetSize(/*suggested_check=*/true,
+                                                        
/*target_size=*/102400));
+    ASSERT_FALSE(reach_target_size);
+
+    // add batch second times, 9998 rows
+    AddRecordBatchOnce(format_writer, struct_type, 9998, 1);
+    ASSERT_OK_AND_ASSIGN(reach_target_size, 
format_writer->ReachTargetSize(/*suggested_check=*/true,
+                                                                           
/*target_size=*/102400));
+    ASSERT_FALSE(reach_target_size);
+
+    AddRecordBatchOnce(format_writer, struct_type, 100000, 9999);
+    ASSERT_OK_AND_ASSIGN(reach_target_size, 
format_writer->ReachTargetSize(/*suggested_check=*/true,
+                                                                           
/*target_size=*/102400));
+    ASSERT_TRUE(reach_target_size);
+    ASSERT_OK(format_writer->Finish());
+}
+
+}  // namespace paimon::avro::test
diff --git a/src/paimon/format/avro/avro_stats_extractor.cpp 
b/src/paimon/format/avro/avro_stats_extractor.cpp
new file mode 100644
index 0000000..104983d
--- /dev/null
+++ b/src/paimon/format/avro/avro_stats_extractor.cpp
@@ -0,0 +1,127 @@
+/*
+ * 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.
+ */
+
+#include "paimon/format/avro/avro_stats_extractor.h"
+
+#include <cstdint>
+#include <optional>
+#include <vector>
+
+#include "arrow/api.h"
+#include "arrow/c/abi.h"
+#include "arrow/c/bridge.h"
+#include "arrow/util/checked_cast.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/date_time_utils.h"
+#include "paimon/core/core_options.h"
+#include "paimon/defs.h"
+#include "paimon/format/avro/avro_file_format.h"
+#include "paimon/status.h"
+
+namespace paimon {
+class FileSystem;
+class MemoryPool;
+}  // namespace paimon
+
+namespace paimon::avro {
+
+Result<std::pair<ColumnStatsVector, FormatStatsExtractor::FileInfo>>
+AvroStatsExtractor::ExtractWithFileInfoInternal(const 
std::shared_ptr<FileSystem>& file_system,
+                                                const std::string& path,
+                                                const 
std::shared_ptr<MemoryPool>& pool,
+                                                bool with_file_info) const {
+    PAIMON_ASSIGN_OR_RAISE(CoreOptions core_options, 
CoreOptions::FromMap(options_));
+    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<InputStream> input_stream, 
file_system->Open(path));
+    assert(input_stream);
+    auto avro_file_format = std::make_unique<AvroFileFormat>(options_);
+    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReaderBuilder> avro_reader_builder,
+                           
avro_file_format->CreateReaderBuilder(core_options.GetReadBatchSize()));
+    PAIMON_ASSIGN_OR_RAISE(
+        std::unique_ptr<FileBatchReader> avro_reader,
+        
avro_reader_builder->WithMemoryPool(pool)->Build(std::move(input_stream)));
+
+    PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<::ArrowSchema> c_schema, 
avro_reader->GetFileSchema());
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Schema> 
arrow_schema,
+                                      arrow::ImportSchema(c_schema.get()));
+    ColumnStatsVector result_stats;
+    result_stats.reserve(arrow_schema->num_fields());
+    for (const auto& arrow_field : arrow_schema->fields()) {
+        PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ColumnStats> stats,
+                               FetchColumnStatistics(arrow_field->type()));
+        result_stats.push_back(std::move(stats));
+    }
+    if (!with_file_info) {
+        // Do not return file info if not needed, because GetNumberOfRows in 
avro reader need I/O
+        // and performance is poor.
+        return std::make_pair(result_stats, FileInfo(-1));
+    }
+    PAIMON_ASSIGN_OR_RAISE(uint64_t num_rows, avro_reader->GetNumberOfRows());
+    return std::make_pair(result_stats, FileInfo(num_rows));
+}
+
+Result<std::unique_ptr<ColumnStats>> AvroStatsExtractor::FetchColumnStatistics(
+    const std::shared_ptr<arrow::DataType>& type) const {
+    // TODO(jinli.zjw): support stats in avro
+    arrow::Type::type kind = type->id();
+    switch (kind) {
+        case arrow::Type::type::BOOL:
+            return ColumnStats::CreateBooleanColumnStats(std::nullopt, 
std::nullopt, std::nullopt);
+        case arrow::Type::type::INT8:
+        case arrow::Type::type::INT16:
+            return Status::Invalid(
+                fmt::format("Unexpected: {} type cannot appear in avro 
files.", type->ToString()));
+        case arrow::Type::type::INT32:
+            return ColumnStats::CreateIntColumnStats(std::nullopt, 
std::nullopt, std::nullopt);
+        case arrow::Type::type::INT64:
+            return ColumnStats::CreateBigIntColumnStats(std::nullopt, 
std::nullopt, std::nullopt);
+        case arrow::Type::type::FLOAT:
+            return ColumnStats::CreateFloatColumnStats(std::nullopt, 
std::nullopt, std::nullopt);
+        case arrow::Type::type::DOUBLE:
+            return ColumnStats::CreateDoubleColumnStats(std::nullopt, 
std::nullopt, std::nullopt);
+        case arrow::Type::type::BINARY:
+            return ColumnStats::CreateStringColumnStats(std::nullopt, 
std::nullopt, std::nullopt);
+        case arrow::Type::type::STRING:
+            return ColumnStats::CreateStringColumnStats(std::nullopt, 
std::nullopt, std::nullopt);
+
+        case arrow::Type::type::DATE32:
+            return ColumnStats::CreateDateColumnStats(std::nullopt, 
std::nullopt, std::nullopt);
+        case arrow::Type::type::TIMESTAMP: {
+            auto ts_type = 
arrow::internal::checked_pointer_cast<::arrow::TimestampType>(type);
+            int32_t precision = DateTimeUtils::GetPrecisionFromType(ts_type);
+            return ColumnStats::CreateTimestampColumnStats(std::nullopt, 
std::nullopt, std::nullopt,
+                                                           precision);
+        }
+        case arrow::Type::type::DECIMAL128: {
+            auto decimal_type =
+                
arrow::internal::checked_pointer_cast<::arrow::Decimal128Type>(type);
+            int32_t precision = decimal_type->precision();
+            int32_t scale = decimal_type->scale();
+            return ColumnStats::CreateDecimalColumnStats(std::nullopt, 
std::nullopt, std::nullopt,
+                                                         precision, scale);
+        }
+        case arrow::Type::type::STRUCT:
+            return ColumnStats::CreateNestedColumnStats(FieldType::STRUCT, 
std::nullopt);
+        case arrow::Type::type::LIST:
+            return ColumnStats::CreateNestedColumnStats(FieldType::ARRAY, 
std::nullopt);
+        case arrow::Type::type::MAP:
+            return ColumnStats::CreateNestedColumnStats(FieldType::MAP, 
std::nullopt);
+        default:
+            return Status::Invalid("Unknown or unsupported arrow type: ", 
type->ToString());
+    }
+}
+}  // namespace paimon::avro
diff --git a/src/paimon/format/avro/avro_stats_extractor.h 
b/src/paimon/format/avro/avro_stats_extractor.h
new file mode 100644
index 0000000..746768f
--- /dev/null
+++ b/src/paimon/format/avro/avro_stats_extractor.h
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "arrow/api.h"
+#include "paimon/format/column_stats.h"
+#include "paimon/format/format_stats_extractor.h"
+#include "paimon/result.h"
+#include "paimon/type_fwd.h"
+
+namespace arrow {
+class DataType;
+}  // namespace arrow
+namespace paimon {
+class FileSystem;
+class MemoryPool;
+}  // namespace paimon
+
+namespace paimon::avro {
+
+class AvroStatsExtractor : public FormatStatsExtractor {
+ public:
+    explicit AvroStatsExtractor(const std::map<std::string, std::string>& 
options)
+        : options_(options) {}
+
+    Result<ColumnStatsVector> Extract(const std::shared_ptr<FileSystem>& 
file_system,
+                                      const std::string& path,
+                                      const std::shared_ptr<MemoryPool>& pool) 
override {
+        PAIMON_ASSIGN_OR_RAISE(auto result, 
ExtractWithFileInfoInternal(file_system, path, pool,
+                                                                        
/*with_file_info=*/false));
+        return result.first;
+    }
+
+    Result<std::pair<ColumnStatsVector, FileInfo>> ExtractWithFileInfo(
+        const std::shared_ptr<FileSystem>& file_system, const std::string& 
path,
+        const std::shared_ptr<MemoryPool>& pool) override {
+        return ExtractWithFileInfoInternal(file_system, path, pool, 
/*with_file_info=*/true);
+    }
+
+ private:
+    Result<std::pair<ColumnStatsVector, FileInfo>> ExtractWithFileInfoInternal(
+        const std::shared_ptr<FileSystem>& file_system, const std::string& 
path,
+        const std::shared_ptr<MemoryPool>& pool, bool with_file_info) const;
+
+    Result<std::unique_ptr<ColumnStats>> FetchColumnStatistics(
+        const std::shared_ptr<arrow::DataType>& type) const;
+
+ private:
+    std::map<std::string, std::string> options_;
+};
+
+}  // namespace paimon::avro
diff --git a/src/paimon/format/avro/avro_stats_extractor_test.cpp 
b/src/paimon/format/avro/avro_stats_extractor_test.cpp
new file mode 100644
index 0000000..081c955
--- /dev/null
+++ b/src/paimon/format/avro/avro_stats_extractor_test.cpp
@@ -0,0 +1,220 @@
+/*
+ * 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.
+ */
+
+#include "paimon/format/avro/avro_stats_extractor.h"
+
+#include <vector>
+
+#include "arrow/api.h"
+#include "arrow/c/abi.h"
+#include "arrow/c/bridge.h"
+#include "arrow/ipc/json_simple.h"
+#include "gtest/gtest.h"
+#include "paimon/common/data/binary_row.h"
+#include "paimon/common/utils/date_time_utils.h"
+#include "paimon/core/stats/simple_stats.h"
+#include "paimon/core/stats/simple_stats_converter.h"
+#include "paimon/format/avro/avro_file_format.h"
+#include "paimon/format/avro/avro_format_writer.h"
+#include "paimon/format/file_format_factory.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/fs/local/local_file_system.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/status.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::avro::test {
+
+class AvroStatsExtractorTest : public ::testing::Test {
+ public:
+    void SetUp() override {}
+    void TearDown() override {}
+
+    void WriteAvroFile(const std::string& file_path,
+                       const std::shared_ptr<arrow::ChunkedArray>& 
src_chunk_array,
+                       const std::shared_ptr<arrow::Schema>& schema) const {
+        ::ArrowSchema c_schema;
+        ASSERT_TRUE(arrow::ExportSchema(*schema, &c_schema).ok());
+
+        ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileFormat> file_format,
+                             FileFormatFactory::Get("avro", options_));
+        ASSERT_OK_AND_ASSIGN(auto writer_builder,
+                             file_format->CreateWriterBuilder(&c_schema, 
/*batch_size=*/1024));
+
+        auto fs = std::make_shared<LocalFileSystem>();
+        ASSERT_OK_AND_ASSIGN(std::unique_ptr<OutputStream> output_stream,
+                             fs->Create(file_path, true));
+        ASSERT_OK_AND_ASSIGN(auto writer, 
writer_builder->Build(std::move(output_stream), "null"));
+
+        for (const auto& array : src_chunk_array->chunks()) {
+            ::ArrowArray c_array;
+            ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+            ASSERT_OK(writer->AddBatch(&c_array));
+        }
+        ASSERT_OK(writer->Flush());
+        ASSERT_OK(writer->Finish());
+
+        ASSERT_OK_AND_ASSIGN(auto file_status, fs->GetFileStatus(file_path));
+        ASSERT_GT(file_status->GetLen(), 0);
+    }
+
+ private:
+    std::map<std::string, std::string> options_ = {{Options::FILE_FORMAT, 
"avro"},
+                                                   {Options::MANIFEST_FORMAT, 
"avro"}};
+};
+
+TEST_F(AvroStatsExtractorTest, TestPrimitiveStatsExtractor) {
+    auto timezone = DateTimeUtils::GetLocalTimezoneName();
+    arrow::FieldVector fields = {
+        arrow::field("f1", arrow::int8()),
+        arrow::field("f2", arrow::int16()),
+        arrow::field("f3", arrow::int32()),
+        arrow::field("f4", arrow::int64()),
+        arrow::field("f5", arrow::float32()),
+        arrow::field("f6", arrow::float64()),
+        arrow::field("f7", arrow::utf8()),
+        arrow::field("f8", arrow::binary()),
+        arrow::field("f9", arrow::date32()),
+        arrow::field("f10", arrow::timestamp(arrow::TimeUnit::NANO)),
+        arrow::field("f11", arrow::decimal128(5, 2)),
+        arrow::field("f12", arrow::boolean()),
+        arrow::field("f13", arrow::timestamp(arrow::TimeUnit::SECOND)),
+        arrow::field("f14", arrow::timestamp(arrow::TimeUnit::MILLI)),
+        arrow::field("f15", arrow::timestamp(arrow::TimeUnit::MICRO)),
+        arrow::field("f16", arrow::timestamp(arrow::TimeUnit::NANO)),
+        arrow::field("f17", arrow::timestamp(arrow::TimeUnit::SECOND, 
timezone)),
+        arrow::field("f18", arrow::timestamp(arrow::TimeUnit::MILLI, 
timezone)),
+        arrow::field("f19", arrow::timestamp(arrow::TimeUnit::MICRO, 
timezone)),
+        arrow::field("f20", arrow::timestamp(arrow::TimeUnit::NANO, timezone)),
+    };
+    auto schema = std::make_shared<arrow::Schema>(fields);
+    auto array = std::dynamic_pointer_cast<arrow::StructArray>(
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
+        [1, 11, 111, 1111, 1.1, 1.11, "Hello", "你好", 1234, "2033-05-18 
03:33:20.0", "1.22", true, "2033-05-18 03:33:20", "2033-05-18 03:33:20.0", 
"2033-05-18 03:33:20.0", "2033-05-18 03:33:20.0", "2033-05-18 03:33:20", 
"2033-05-18 03:33:20.0", "2033-05-18 03:33:20.0", "2033-05-18 03:33:20.0"],
+        [2, 22, 222, 2222, 2.2, 2.22, "World", "世界", -1234, "1899-01-01 
00:59:20.001001001", "2.22", false, "1899-01-01 00:59:20", "1899-01-01 
00:59:20", "1899-01-01 00:59:20", "1899-01-01 00:59:20.001001001","1899-01-01 
00:59:20", "1899-01-01 00:59:20", "1899-01-01 00:59:20", "1899-01-01 
00:59:20.001001001"],
+        [null, null, 0, null, null, 0, null, null, null, null, null, null, 
null, null, null, null, null, null, null, null]
+    ])")
+            .ValueOrDie());
+    auto src_chunk_array = 
std::make_shared<arrow::ChunkedArray>(arrow::ArrayVector({array}));
+
+    auto dir = paimon::test::UniqueTestDirectory::Create();
+    ASSERT_TRUE(dir);
+    std::string file_path = dir->Str() + "/test.avro";
+    WriteAvroFile(file_path, src_chunk_array, schema);
+
+    AvroFileFormat format(options_);
+    ::ArrowSchema arrow_schema;
+    ASSERT_TRUE(arrow::ExportSchema(*schema, &arrow_schema).ok());
+    ASSERT_OK_AND_ASSIGN(auto extractor, 
format.CreateStatsExtractor(&arrow_schema));
+    auto fs = std::make_shared<LocalFileSystem>();
+    ASSERT_OK_AND_ASSIGN(auto stats_with_info,
+                         extractor->ExtractWithFileInfo(fs, file_path, 
GetDefaultPool()));
+    const auto& column_stats = stats_with_info.first;
+    const auto& file_stats = stats_with_info.second;
+
+    ASSERT_EQ(column_stats.size(), 20);
+    for (const auto& stats : column_stats) {
+        ASSERT_EQ(stats->ToString(), "min null, max null, null count null");
+    }
+    ASSERT_EQ(3, file_stats.GetRowCount());
+}
+
+TEST_F(AvroStatsExtractorTest, TestNestedType) {
+    arrow::FieldVector fields = {
+        arrow::field("f0", arrow::list(arrow::float32())),
+        arrow::field("f1", arrow::struct_({arrow::field("sub_f0", 
arrow::boolean()),
+                                           arrow::field("sub_f1", 
arrow::int64())}))};
+    auto schema = arrow::schema(fields);
+    auto array = std::dynamic_pointer_cast<arrow::StructArray>(
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
+        [null, [true, 2]],
+        [[0.1, 0.3], [true, 1]],
+        [[1.1, 1.2], null]
+    ])")
+            .ValueOrDie());
+    auto src_chunk_array = 
std::make_shared<arrow::ChunkedArray>(arrow::ArrayVector({array}));
+
+    auto dir = paimon::test::UniqueTestDirectory::Create();
+    ASSERT_TRUE(dir);
+    std::string file_path = dir->Str() + "/test.avro";
+    WriteAvroFile(file_path, src_chunk_array, schema);
+
+    AvroStatsExtractor extractor(options_);
+    auto fs = std::make_shared<LocalFileSystem>();
+    ASSERT_OK_AND_ASSIGN(auto results, extractor.Extract(fs, file_path, 
GetDefaultPool()));
+
+    ASSERT_EQ(results.size(), 2);
+    for (const auto& stats : results) {
+        ASSERT_EQ(stats->ToString(), "min null, max null, null count null");
+    }
+}
+
+TEST_F(AvroStatsExtractorTest, TestNullForAllType) {
+    auto timezone = DateTimeUtils::GetLocalTimezoneName();
+    arrow::FieldVector fields = {
+        arrow::field("f0", arrow::boolean()),
+        arrow::field("f1", arrow::int8()),
+        arrow::field("f2", arrow::int16()),
+        arrow::field("f3", arrow::int32()),
+        arrow::field("f4", arrow::int64()),
+        arrow::field("f5", arrow::float32()),
+        arrow::field("f6", arrow::float64()),
+        arrow::field("f7", arrow::utf8()),
+        arrow::field("f8", arrow::binary()),
+        arrow::field("f9", arrow::list(arrow::struct_({arrow::field("key", 
arrow::int8()),
+                                                       arrow::field("value", 
arrow::int16())}))),
+        arrow::field("f10", arrow::list(arrow::float32())),
+        arrow::field("f11", arrow::struct_({arrow::field("f0", 
arrow::boolean()),
+                                            arrow::field("f1", 
arrow::int64())})),
+        arrow::field("f12", arrow::timestamp(arrow::TimeUnit::NANO)),
+        arrow::field("f13", arrow::date32()),
+        arrow::field("f14", arrow::decimal128(2, 2)),
+        arrow::field("f15", arrow::decimal128(30, 2)),
+        arrow::field("f16", arrow::timestamp(arrow::TimeUnit::SECOND)),
+        arrow::field("f17", arrow::timestamp(arrow::TimeUnit::MILLI)),
+        arrow::field("f18", arrow::timestamp(arrow::TimeUnit::MICRO)),
+        arrow::field("f19", arrow::timestamp(arrow::TimeUnit::NANO)),
+        arrow::field("f20", arrow::timestamp(arrow::TimeUnit::SECOND, 
timezone)),
+        arrow::field("f21", arrow::timestamp(arrow::TimeUnit::MILLI, 
timezone)),
+        arrow::field("f22", arrow::timestamp(arrow::TimeUnit::MICRO, 
timezone)),
+        arrow::field("f23", arrow::timestamp(arrow::TimeUnit::NANO, timezone)),
+    };
+    auto schema = std::make_shared<arrow::Schema>(fields);
+    auto src_array = std::dynamic_pointer_cast<arrow::StructArray>(
+        arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields), R"([
+        [null, null, null, null, null, null, null, null, null, null, null, 
null, null, null, null, null, null, null, null, null, null, null, null, null]
+    ])")
+            .ValueOrDie());
+    auto src_chunk_array = 
std::make_shared<arrow::ChunkedArray>(arrow::ArrayVector({src_array}));
+
+    auto dir = paimon::test::UniqueTestDirectory::Create();
+    ASSERT_TRUE(dir);
+    std::string file_path = dir->Str() + "/test.avro";
+    WriteAvroFile(file_path, src_chunk_array, schema);
+
+    AvroStatsExtractor extractor(options_);
+    auto fs = std::make_shared<LocalFileSystem>();
+    ASSERT_OK_AND_ASSIGN(auto column_stats, extractor.Extract(fs, file_path, 
GetDefaultPool()));
+
+    ASSERT_OK_AND_ASSIGN(auto stats,
+                         SimpleStatsConverter::ToBinary(column_stats, 
GetDefaultPool().get()));
+    ASSERT_EQ(stats.min_values_.HashCode(), 0xf890741a);
+    ASSERT_EQ(stats.max_values_.HashCode(), 0xf890741a);
+}
+
+}  // namespace paimon::avro::test
diff --git a/src/paimon/format/avro/avro_writer_builder.h 
b/src/paimon/format/avro/avro_writer_builder.h
new file mode 100644
index 0000000..dd24c3a
--- /dev/null
+++ b/src/paimon/format/avro/avro_writer_builder.h
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "avro/DataFile.hh"
+#include "avro/Stream.hh"
+#include "paimon/common/utils/options_utils.h"
+#include "paimon/common/utils/string_utils.h"
+#include "paimon/core/core_options.h"
+#include "paimon/format/avro/avro_format_defs.h"
+#include "paimon/format/avro/avro_format_writer.h"
+#include "paimon/format/avro/avro_output_stream_impl.h"
+#include "paimon/format/writer_builder.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace arrow {
+class Schema;
+}  // namespace arrow
+namespace paimon {
+class FormatWriter;
+class OutputStream;
+}  // namespace paimon
+
+namespace paimon::avro {
+
+class AvroWriterBuilder : public WriterBuilder {
+ public:
+    AvroWriterBuilder(const std::shared_ptr<arrow::Schema>& schema, int32_t 
batch_size,
+                      const std::map<std::string, std::string>& options)
+        : pool_(GetDefaultPool()), schema_(schema), options_(options) {}
+
+    WriterBuilder* WithMemoryPool(const std::shared_ptr<MemoryPool>& pool) 
override {
+        pool_ = pool;
+        return this;
+    }
+
+    Result<std::unique_ptr<FormatWriter>> Build(const 
std::shared_ptr<OutputStream>& out,
+                                                const std::string& 
compression) override {
+        auto output_stream = std::make_unique<AvroOutputStreamImpl>(out, 
BUFFER_SIZE, pool_);
+        PAIMON_ASSIGN_OR_RAISE(
+            std::string file_compression,
+            OptionsUtils::GetValueFromMap<std::string>(options_, AVRO_CODEC, 
compression));
+        PAIMON_ASSIGN_OR_RAISE(::avro::Codec codec,
+                               
ToAvroCompressionKind(StringUtils::ToLowerCase(file_compression)));
+        PAIMON_ASSIGN_OR_RAISE(std::optional<int32_t> compression_level,
+                               GetAvroCompressionLevel(codec));
+        return AvroFormatWriter::Create(std::move(output_stream), schema_, 
codec,
+                                        compression_level);
+    }
+
+ private:
+    static constexpr int32_t BUFFER_SIZE = 1024 * 1024;
+
+    static Result<::avro::Codec> ToAvroCompressionKind(const std::string& 
file_compression) {
+        if (file_compression == "zstd" || file_compression == "zstandard") {
+            return ::avro::Codec::ZSTD_CODEC;
+        } else if (file_compression == "snappy") {
+            return ::avro::Codec::SNAPPY_CODEC;
+        } else if (file_compression == "null" || file_compression == "none") {
+            return ::avro::Codec::NULL_CODEC;
+        } else if (file_compression == "deflate") {
+            return ::avro::Codec::DEFLATE_CODEC;
+        } else {
+            return Status::Invalid("unknown compression " + file_compression);
+        }
+    }
+    Result<std::optional<int32_t>> GetAvroCompressionLevel(const 
::avro::Codec& codec) {
+        std::optional<int32_t> compression_level;
+        if (codec == ::avro::Codec::ZSTD_CODEC) {
+            PAIMON_ASSIGN_OR_RAISE(CoreOptions core_options, 
CoreOptions::FromMap(options_));
+            compression_level = core_options.GetFileCompressionZstdLevel();
+        }
+        return compression_level;
+    }
+
+    std::shared_ptr<MemoryPool> pool_;
+    std::shared_ptr<arrow::Schema> schema_;
+    const std::map<std::string, std::string> options_;
+};
+
+}  // namespace paimon::avro
diff --git a/src/paimon/format/avro/avro_writer_builder_test.cpp 
b/src/paimon/format/avro/avro_writer_builder_test.cpp
new file mode 100644
index 0000000..5e309e5
--- /dev/null
+++ b/src/paimon/format/avro/avro_writer_builder_test.cpp
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+
+#include "paimon/format/avro/avro_writer_builder.h"
+
+#include "avro/DataFile.hh"
+#include "gtest/gtest.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::avro::test {
+
+TEST(AvroWriterBuilderTest, HandlesValidCompressions) {
+    ASSERT_OK_AND_ASSIGN(::avro::Codec zstd_codec,
+                         AvroWriterBuilder::ToAvroCompressionKind("zstd"));
+    ASSERT_EQ(zstd_codec, ::avro::Codec::ZSTD_CODEC);
+
+    ASSERT_OK_AND_ASSIGN(::avro::Codec zstandard_codec,
+                         
AvroWriterBuilder::ToAvroCompressionKind("zstandard"));
+    ASSERT_EQ(zstandard_codec, ::avro::Codec::ZSTD_CODEC);
+
+    ASSERT_OK_AND_ASSIGN(::avro::Codec snappy_codec,
+                         AvroWriterBuilder::ToAvroCompressionKind("snappy"));
+    ASSERT_EQ(snappy_codec, ::avro::Codec::SNAPPY_CODEC);
+
+    ASSERT_OK_AND_ASSIGN(::avro::Codec null_codec,
+                         AvroWriterBuilder::ToAvroCompressionKind("null"));
+    ASSERT_EQ(null_codec, ::avro::Codec::NULL_CODEC);
+
+    ASSERT_OK_AND_ASSIGN(::avro::Codec deflate_codec,
+                         AvroWriterBuilder::ToAvroCompressionKind("deflate"));
+    ASSERT_EQ(deflate_codec, ::avro::Codec::DEFLATE_CODEC);
+}
+
+TEST(AvroWriterBuilderTest, HandlesInvalidCompression) {
+    
ASSERT_NOK(AvroWriterBuilder::ToAvroCompressionKind("unknown_compression"));
+}
+
+TEST(AvroWriterBuilderTest, HandlesEmptyString) {
+    ASSERT_NOK(AvroWriterBuilder::ToAvroCompressionKind(""));
+}
+
+TEST(AvroWriterBuilderTest, CheckAvroCodec) {
+    arrow::FieldVector fields = {arrow::field("f0", arrow::int32())};
+    auto schema = std::make_shared<arrow::Schema>(fields);
+    {
+        AvroWriterBuilder builder(schema, -1,
+                                  {{Options::FILE_FORMAT, "avro"}, 
{"avro.codec", "snappy"}});
+        ASSERT_OK_AND_ASSIGN(auto file_writer, builder.Build(nullptr, "zstd"));
+        auto* avro_file_writer = 
dynamic_cast<AvroFormatWriter*>(file_writer.get());
+        ASSERT_EQ(avro_file_writer->writer_->codec_, 
::avro::Codec::SNAPPY_CODEC);
+        ASSERT_EQ(avro_file_writer->writer_->compressionLevel_, std::nullopt);
+    }
+    {
+        AvroWriterBuilder builder(schema, -1,
+                                  {{Options::FILE_FORMAT, "avro"}, 
{"avro.codec", "deflate"}});
+        ASSERT_OK_AND_ASSIGN(auto file_writer, builder.Build(nullptr, "zstd"));
+        auto* avro_file_writer = 
dynamic_cast<AvroFormatWriter*>(file_writer.get());
+        ASSERT_EQ(avro_file_writer->writer_->codec_, 
::avro::Codec::DEFLATE_CODEC);
+        ASSERT_EQ(avro_file_writer->writer_->compressionLevel_, std::nullopt);
+    }
+    {
+        AvroWriterBuilder builder(schema, -1,
+                                  {{Options::FILE_FORMAT, "avro"}, 
{"avro.codec", "zstd"}});
+        ASSERT_OK_AND_ASSIGN(auto file_writer, builder.Build(nullptr, "zstd"));
+        auto* avro_file_writer = 
dynamic_cast<AvroFormatWriter*>(file_writer.get());
+        ASSERT_EQ(avro_file_writer->writer_->codec_, 
::avro::Codec::ZSTD_CODEC);
+        ASSERT_EQ(avro_file_writer->writer_->compressionLevel_, 1);
+    }
+    {
+        AvroWriterBuilder builder(schema, -1,
+                                  {{Options::FILE_FORMAT, "avro"},
+                                   {"avro.codec", "zstd"},
+                                   {Options::FILE_COMPRESSION_ZSTD_LEVEL, 
"3"}});
+        ASSERT_OK_AND_ASSIGN(auto file_writer, builder.Build(nullptr, "zstd"));
+        auto* avro_file_writer = 
dynamic_cast<AvroFormatWriter*>(file_writer.get());
+        ASSERT_EQ(avro_file_writer->writer_->codec_, 
::avro::Codec::ZSTD_CODEC);
+        ASSERT_EQ(avro_file_writer->writer_->compressionLevel_, 3);
+    }
+    {
+        AvroWriterBuilder builder(schema, -1,
+                                  {{Options::FILE_FORMAT, "avro"},
+                                   {"avro.codec", "null"},
+                                   {Options::FILE_COMPRESSION_ZSTD_LEVEL, 
"3"}});
+        ASSERT_OK_AND_ASSIGN(auto file_writer, builder.Build(nullptr, "zstd"));
+        auto* avro_file_writer = 
dynamic_cast<AvroFormatWriter*>(file_writer.get());
+        ASSERT_EQ(avro_file_writer->writer_->codec_, 
::avro::Codec::NULL_CODEC);
+        ASSERT_EQ(avro_file_writer->writer_->compressionLevel_, std::nullopt);
+    }
+    {
+        AvroWriterBuilder builder(schema, -1,
+                                  {{Options::FILE_FORMAT, "avro"},
+                                   {"avro.codec", "test"},
+                                   {Options::FILE_COMPRESSION_ZSTD_LEVEL, 
"3"}});
+        ASSERT_NOK(builder.Build(nullptr, "zstd"));
+    }
+}
+
+TEST(AvroWriterBuilderTest, CheckAvroCompressionLevel) {
+    {
+        AvroWriterBuilder builder(nullptr, -1, {{Options::FILE_FORMAT, 
"avro"}});
+        ASSERT_OK_AND_ASSIGN(std::optional<int32_t> zstd_level,
+                             
builder.GetAvroCompressionLevel(::avro::Codec::ZSTD_CODEC));
+        ASSERT_TRUE(zstd_level.has_value());
+        ASSERT_EQ(zstd_level.value(), 1);
+    }
+    {
+        AvroWriterBuilder builder(nullptr, -1, {{Options::FILE_FORMAT, 
"avro"}});
+        ASSERT_OK_AND_ASSIGN(std::optional<int32_t> compression_level,
+                             
builder.GetAvroCompressionLevel(::avro::Codec::SNAPPY_CODEC));
+        ASSERT_FALSE(compression_level.has_value());
+    }
+    {
+        AvroWriterBuilder builder(nullptr, -1, {{Options::FILE_FORMAT, 
"avro"}});
+        ASSERT_OK_AND_ASSIGN(std::optional<int32_t> compression_level,
+                             
builder.GetAvroCompressionLevel(::avro::Codec::DEFLATE_CODEC));
+        ASSERT_FALSE(compression_level.has_value());
+    }
+    {
+        AvroWriterBuilder builder(nullptr, -1, {{Options::FILE_FORMAT, 
"avro"}});
+        ASSERT_OK_AND_ASSIGN(std::optional<int32_t> compression_level,
+                             
builder.GetAvroCompressionLevel(::avro::Codec::NULL_CODEC));
+        ASSERT_FALSE(compression_level.has_value());
+    }
+    {
+        AvroWriterBuilder builder(
+            nullptr, -1,
+            {{Options::FILE_FORMAT, "avro"}, 
{Options::FILE_COMPRESSION_ZSTD_LEVEL, "3"}});
+        ASSERT_OK_AND_ASSIGN(std::optional<int32_t> zstd_level,
+                             
builder.GetAvroCompressionLevel(::avro::Codec::ZSTD_CODEC));
+        ASSERT_TRUE(zstd_level.has_value());
+        ASSERT_EQ(zstd_level.value(), 3);
+    }
+}
+
+}  // namespace paimon::avro::test


Reply via email to