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 a22663a  feat(manifest): add manifest entry, file metadata and 
serialization utilities (#86)
a22663a is described below

commit a22663ac5ddd9c7bfc1883f62ef184c34a517d24
Author: Yonghao Fang <[email protected]>
AuthorDate: Thu Jun 18 10:16:42 2026 +0800

    feat(manifest): add manifest entry, file metadata and serialization 
utilities (#86)
---
 src/paimon/core/manifest/file_entry.h              | 160 +++++++++++++++++
 src/paimon/core/manifest/file_entry_test.cpp       | 193 +++++++++++++++++++++
 src/paimon/core/manifest/file_kind.cpp             |  47 +++++
 src/paimon/core/manifest/file_kind.h               |  46 +++++
 src/paimon/core/manifest/file_kind_test.cpp        |  40 +++++
 src/paimon/core/manifest/file_source.cpp           |  32 ++++
 src/paimon/core/manifest/file_source.h             |  80 +++++++++
 src/paimon/core/manifest/file_source_test.cpp      |  35 ++++
 src/paimon/core/manifest/manifest_entry.cpp        |  57 ++++++
 src/paimon/core/manifest/manifest_entry.h          | 160 +++++++++++++++++
 .../core/manifest/manifest_entry_serializer.cpp    |  79 +++++++++
 .../core/manifest/manifest_entry_serializer.h      |  63 +++++++
 .../manifest/manifest_entry_serializer_test.cpp    |  63 +++++++
 src/paimon/core/manifest/manifest_entry_writer.cpp |  80 +++++++++
 src/paimon/core/manifest/manifest_entry_writer.h   |  81 +++++++++
 .../core/manifest/manifest_entry_writer_test.cpp   | 192 ++++++++++++++++++++
 src/paimon/core/manifest/manifest_file_meta.cpp    | 101 +++++++++++
 src/paimon/core/manifest/manifest_file_meta.h      | 111 ++++++++++++
 .../manifest/manifest_file_meta_serializer.cpp     | 141 +++++++++++++++
 .../core/manifest/manifest_file_meta_serializer.h  |  62 +++++++
 .../manifest_file_meta_serializer_test.cpp         | 107 ++++++++++++
 21 files changed, 1930 insertions(+)

diff --git a/src/paimon/core/manifest/file_entry.h 
b/src/paimon/core/manifest/file_entry.h
new file mode 100644
index 0000000..30705a8
--- /dev/null
+++ b/src/paimon/core/manifest/file_entry.h
@@ -0,0 +1,160 @@
+/*
+ * 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 <optional>
+#include <string>
+#include <variant>
+#include <vector>
+
+#include "fmt/format.h"
+#include "paimon/common/data/binary_row.h"
+#include "paimon/common/utils/linked_hash_map.h"
+#include "paimon/common/utils/murmurhash_utils.h"
+#include "paimon/core/manifest/file_kind.h"
+#include "paimon/status.h"
+
+namespace paimon {
+
+/// Entry representing a file.
+class FileEntry {
+ public:
+    /// The same `Identifier` indicates that the `ManifestEntry` refers to the 
same data
+    /// file.
+    struct Identifier {
+        Identifier(const BinaryRow& _partition, int32_t _bucket, int32_t 
_level,
+                   const std::string& _file_name, const 
std::optional<std::string>& _external_path)
+            : partition(_partition),
+              bucket(_bucket),
+              level(_level),
+              file_name(_file_name),
+              external_path(_external_path) {}
+
+        bool operator==(const Identifier& other) const {
+            return partition == other.partition && bucket == other.bucket && 
level == other.level &&
+                   file_name == other.file_name && external_path == 
other.external_path;
+        }
+
+        size_t HashCode() const {
+            if (hash_ == static_cast<size_t>(-1)) {
+                hash_ = partition.HashCode();
+                hash_ =
+                    MurmurHashUtils::HashUnsafeBytes(reinterpret_cast<const 
void*>(&bucket),
+                                                     /*offset=*/0, 
sizeof(bucket), /*seed=*/hash_);
+                hash_ = 
MurmurHashUtils::HashUnsafeBytes(reinterpret_cast<const void*>(&level),
+                                                         /*offset=*/0, 
sizeof(level),
+                                                         /*seed=*/hash_);
+                hash_ = MurmurHashUtils::HashUnsafeBytes(
+                    reinterpret_cast<const void*>(file_name.data()),
+                    /*offset=*/0, file_name.size(),
+                    /*seed=*/hash_);
+                if (external_path) {
+                    hash_ = MurmurHashUtils::HashUnsafeBytes(
+                        reinterpret_cast<const 
void*>(external_path.value().data()),
+                        /*offset=*/0, external_path.value().size(),
+                        /*seed=*/hash_);
+                }
+            }
+            return hash_;
+        }
+
+        std::string ToString() const {
+            std::string external_path_str =
+                external_path == std::nullopt ? "null" : external_path.value();
+            return fmt::format("{{{}, {}, {}, {}, {}}}", partition.ToString(), 
bucket, level,
+                               file_name, external_path_str);
+        }
+
+        BinaryRow partition;
+        int32_t bucket;
+        int32_t level;
+        std::string file_name;
+        std::optional<std::string> external_path;
+
+     private:
+        mutable size_t hash_ = -1;
+    };
+
+ public:
+    virtual ~FileEntry() = default;
+    virtual const FileKind& Kind() const = 0;
+    virtual const BinaryRow& Partition() const = 0;
+    virtual int32_t Bucket() const = 0;
+    virtual int32_t Level() const = 0;
+    virtual const std::string& FileName() const = 0;
+    virtual const std::optional<std::string>& ExternalPath() const = 0;
+    virtual Identifier CreateIdentifier() const = 0;
+    virtual const BinaryRow& MinKey() const = 0;
+    virtual const BinaryRow& MaxKey() const = 0;
+
+    template <typename T>
+    static Status MergeEntries(const std::vector<T>& unmerged_entries,
+                               std::vector<T>* merged_entries) {
+        LinkedHashMap<Identifier, T> merged_map;
+        PAIMON_RETURN_NOT_OK(MergeEntries(unmerged_entries, &merged_map));
+        for (const auto& [identifier, entry] : merged_map) {
+            merged_entries->emplace_back(entry);
+        }
+        return Status::OK();
+    }
+
+    template <typename T>
+    static Status MergeEntries(const std::vector<T>& unmerged_entries,
+                               LinkedHashMap<Identifier, T>* merged_map_ptr) {
+        auto& merged_map = *merged_map_ptr;
+        for (const auto& entry : unmerged_entries) {
+            Identifier identifier = entry.CreateIdentifier();
+            const auto& kind = entry.Kind();
+            auto iter = merged_map.find(identifier);
+            if (kind == FileKind::Add()) {
+                if (iter != merged_map.end()) {
+                    return Status::Invalid(fmt::format(
+                        "Trying to add file {} which is already added.", 
identifier.ToString()));
+                }
+                merged_map.insert(identifier, entry);
+            } else if (kind == FileKind::Delete()) {
+                // each dataFile will only be added once and deleted once,
+                // if we know that it is added before then both add and delete 
entry can be
+                // removed because there won't be further operations on this 
file,
+                // otherwise we have to keep the delete entry because the add 
entry must be
+                // in the previous manifest files
+                if (iter != merged_map.end()) {
+                    merged_map.erase(identifier);
+                } else {
+                    merged_map.insert(identifier, entry);
+                }
+            } else {
+                return Status::Invalid("Unknown value kind ",
+                                       
std::to_string(static_cast<int32_t>(kind.ToByteValue())));
+            }
+        }
+        return Status::OK();
+    }
+};
+}  // namespace paimon
+
+namespace std {
+template <>
+struct hash<paimon::FileEntry::Identifier> {
+    size_t operator()(const paimon::FileEntry::Identifier& identifier) const {
+        return identifier.HashCode();
+    }
+};
+}  // namespace std
diff --git a/src/paimon/core/manifest/file_entry_test.cpp 
b/src/paimon/core/manifest/file_entry_test.cpp
new file mode 100644
index 0000000..83f5929
--- /dev/null
+++ b/src/paimon/core/manifest/file_entry_test.cpp
@@ -0,0 +1,193 @@
+/*
+ * 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/core/manifest/file_entry.h"
+
+#include <cassert>
+#include <list>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "paimon/common/data/binary_row_writer.h"
+#include "paimon/common/data/binary_string.h"
+#include "paimon/core/manifest/file_kind.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+class MockFileEntry : public FileEntry {
+ public:
+    MockFileEntry(FileKind kind, BinaryRow partition, int32_t bucket, int32_t 
level,
+                  const std::string& file_name,
+                  const std::optional<std::string>& external_path = 
std::nullopt)
+        : kind_(kind),
+          partition_(partition),
+          bucket_(bucket),
+          level_(level),
+          file_name_(file_name),
+          external_path_(external_path),
+          min_key_(BinaryRow::EmptyRow()),
+          max_key_(BinaryRow::EmptyRow()) {}
+
+    const FileKind& Kind() const override {
+        return kind_;
+    }
+    const BinaryRow& Partition() const override {
+        return partition_;
+    }
+    int32_t Bucket() const override {
+        return bucket_;
+    }
+    int32_t Level() const override {
+        return level_;
+    }
+    const std::string& FileName() const override {
+        return file_name_;
+    }
+    const std::optional<std::string>& ExternalPath() const override {
+        return external_path_;
+    }
+    Identifier CreateIdentifier() const override {
+        return Identifier(partition_, bucket_, level_, file_name_, 
external_path_);
+    }
+    const BinaryRow& MinKey() const override {
+        assert(false);
+        return min_key_;
+    }
+    const BinaryRow& MaxKey() const override {
+        assert(false);
+        return max_key_;
+    }
+
+ private:
+    FileKind kind_;
+    BinaryRow partition_;
+    int32_t bucket_;
+    int32_t level_;
+    std::string file_name_;
+    std::optional<std::string> external_path_;
+    BinaryRow min_key_;
+    BinaryRow max_key_;
+};
+
+class FileEntryTest : public testing::Test {
+ public:
+    void SetUp() override {
+        pool_ = GetDefaultPool();
+    }
+
+    BinaryRow GetPartition(const std::string& part_str) {
+        BinaryRow part(/*arity=*/1);
+        BinaryRowWriter writer(&part, /*initial_size=*/20, pool_.get());
+        writer.WriteString(0, BinaryString::FromString(part_str, pool_.get()));
+        return part;
+    }
+
+ private:
+    std::shared_ptr<MemoryPool> pool_;
+};
+
+TEST_F(FileEntryTest, TestMergeEntriesSimple) {
+    std::vector<MockFileEntry> entries;
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file1");
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file2");
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file3");
+    entries.emplace_back(FileKind::Delete(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file1");
+
+    std::vector<MockFileEntry> merged_entries;
+    ASSERT_OK(FileEntry::MergeEntries(entries, &merged_entries));
+    ASSERT_EQ(2u, merged_entries.size());
+    ASSERT_EQ("file2", merged_entries[0].FileName());
+    ASSERT_EQ("file3", merged_entries[1].FileName());
+}
+
+TEST_F(FileEntryTest, TestMergeEntriesAddSameFile) {
+    std::vector<MockFileEntry> entries;
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file1");
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file1");
+    std::vector<MockFileEntry> merged_entries;
+    ASSERT_NOK_WITH_MSG(FileEntry::MergeEntries(entries, &merged_entries),
+                        "which is already added.");
+}
+
+TEST_F(FileEntryTest, TestMergeEntriesAddSameFileWithDiffPart) {
+    std::vector<MockFileEntry> entries;
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file1");
+    entries.emplace_back(FileKind::Add(), GetPartition("1"), /*bucket=*/0, 
/*level=*/0, "file1");
+    std::vector<MockFileEntry> merged_entries;
+    ASSERT_OK(FileEntry::MergeEntries(entries, &merged_entries));
+    ASSERT_EQ(2u, merged_entries.size());
+    ASSERT_EQ("file1", merged_entries[0].FileName());
+    ASSERT_EQ("file1", merged_entries[1].FileName());
+}
+
+TEST_F(FileEntryTest, TestMergeEntriesAddSameFileWithDiffBucket) {
+    std::vector<MockFileEntry> entries;
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file1");
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/1, 
/*level=*/0, "file1");
+    std::vector<MockFileEntry> merged_entries;
+    ASSERT_OK(FileEntry::MergeEntries(entries, &merged_entries));
+    ASSERT_EQ(2u, merged_entries.size());
+    ASSERT_EQ("file1", merged_entries[0].FileName());
+    ASSERT_EQ("file1", merged_entries[1].FileName());
+}
+
+TEST_F(FileEntryTest, TestMergeEntriesAddSameFileWithDiffLevel) {
+    std::vector<MockFileEntry> entries;
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file1");
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/0, 
/*level=*/1, "file1");
+    std::vector<MockFileEntry> merged_entries;
+    ASSERT_OK(FileEntry::MergeEntries(entries, &merged_entries));
+    ASSERT_EQ(2u, merged_entries.size());
+    ASSERT_EQ("file1", merged_entries[0].FileName());
+    ASSERT_EQ("file1", merged_entries[1].FileName());
+}
+
+TEST_F(FileEntryTest, TestMergeEntriesAddSameFileWithDiffExternalPath) {
+    std::vector<MockFileEntry> entries;
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file1",
+                         "/tmp/external_path1");
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file1",
+                         "/tmp/external_path2");
+    std::vector<MockFileEntry> merged_entries;
+    ASSERT_OK(FileEntry::MergeEntries(entries, &merged_entries));
+    ASSERT_EQ(2u, merged_entries.size());
+    ASSERT_EQ("file1", merged_entries[0].FileName());
+    ASSERT_EQ("file1", merged_entries[1].FileName());
+}
+
+TEST_F(FileEntryTest, TestMergeEntriesAddAndDelete) {
+    std::vector<MockFileEntry> entries;
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file1");
+    entries.emplace_back(FileKind::Delete(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file1");
+    std::vector<MockFileEntry> merged_entries;
+    ASSERT_OK(FileEntry::MergeEntries(entries, &merged_entries));
+    ASSERT_EQ(0u, merged_entries.size());
+}
+
+TEST_F(FileEntryTest, TestMergeEntriesDeleteAndAdd) {
+    std::vector<MockFileEntry> entries;
+    entries.emplace_back(FileKind::Delete(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file1");
+    entries.emplace_back(FileKind::Add(), GetPartition("0"), /*bucket=*/0, 
/*level=*/0, "file1");
+    std::vector<MockFileEntry> merged_entries;
+    ASSERT_NOK(FileEntry::MergeEntries(entries, &merged_entries));
+}
+
+}  // namespace paimon::test
diff --git a/src/paimon/core/manifest/file_kind.cpp 
b/src/paimon/core/manifest/file_kind.cpp
new file mode 100644
index 0000000..82cfc18
--- /dev/null
+++ b/src/paimon/core/manifest/file_kind.cpp
@@ -0,0 +1,47 @@
+/*
+ * 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/core/manifest/file_kind.h"
+
+#include "fmt/format.h"
+#include "paimon/status.h"
+
+namespace paimon {
+
+const FileKind& FileKind::Add() {
+    static const FileKind file_kind = FileKind(static_cast<int8_t>(0));
+    return file_kind;
+}
+
+const FileKind& FileKind::Delete() {
+    static const FileKind file_kind = FileKind(static_cast<int8_t>(1));
+    return file_kind;
+}
+
+Result<FileKind> FileKind::FromByteValue(int8_t value) {
+    switch (value) {
+        case 0:
+            return Add();
+        case 1:
+            return Delete();
+        default:
+            return Status::Invalid(fmt::format("Unsupported byte value {} for 
file kind.",
+                                               static_cast<int32_t>(value)));
+    }
+}
+
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/file_kind.h 
b/src/paimon/core/manifest/file_kind.h
new file mode 100644
index 0000000..7532893
--- /dev/null
+++ b/src/paimon/core/manifest/file_kind.h
@@ -0,0 +1,46 @@
+/*
+ * 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 "paimon/result.h"
+
+namespace paimon {
+/// Kind of a file.
+class FileKind {
+ public:
+    FileKind() = default;
+    explicit FileKind(int8_t value) : value_(value) {}
+    int8_t ToByteValue() const {
+        return value_;
+    }
+
+    static const FileKind& Add();
+    static const FileKind& Delete();
+
+    static Result<FileKind> FromByteValue(int8_t value);
+
+    bool operator==(const FileKind& other) const {
+        return value_ == other.value_;
+    }
+
+ private:
+    int8_t value_{-1};
+};
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/file_kind_test.cpp 
b/src/paimon/core/manifest/file_kind_test.cpp
new file mode 100644
index 0000000..190efec
--- /dev/null
+++ b/src/paimon/core/manifest/file_kind_test.cpp
@@ -0,0 +1,40 @@
+/*
+ * 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/core/manifest/file_kind.h"
+
+#include <memory>
+
+#include "gtest/gtest.h"
+#include "paimon/status.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+TEST(FileKindTest, FromByteValueValidCases) {
+    ASSERT_OK_AND_ASSIGN(auto add_result, FileKind::FromByteValue(0));
+    ASSERT_EQ(add_result, FileKind::Add());
+
+    ASSERT_OK_AND_ASSIGN(auto delete_result, FileKind::FromByteValue(1));
+    ASSERT_EQ(delete_result, FileKind::Delete());
+}
+
+TEST(FileKindTest, FromByteValueInvalidCase) {
+    ASSERT_NOK_WITH_MSG(FileKind::FromByteValue(2), "Unsupported byte value");
+}
+
+}  // namespace paimon::test
diff --git a/src/paimon/core/manifest/file_source.cpp 
b/src/paimon/core/manifest/file_source.cpp
new file mode 100644
index 0000000..1712dd8
--- /dev/null
+++ b/src/paimon/core/manifest/file_source.cpp
@@ -0,0 +1,32 @@
+/*
+ * 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/core/manifest/file_source.h"
+
+namespace paimon {
+
+const FileSource& FileSource::Append() {
+    static const FileSource file_source = FileSource(static_cast<int8_t>(0));
+    return file_source;
+}
+
+const FileSource& FileSource::Compact() {
+    static const FileSource file_source = FileSource(static_cast<int8_t>(1));
+    return file_source;
+}
+
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/file_source.h 
b/src/paimon/core/manifest/file_source.h
new file mode 100644
index 0000000..262b0b8
--- /dev/null
+++ b/src/paimon/core/manifest/file_source.h
@@ -0,0 +1,80 @@
+/*
+ * 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 <string>
+
+#include "fmt/format.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace paimon {
+/// The Source of a file.
+class FileSource {
+ public:
+    /// The file from new input.
+    static const FileSource& Append();
+    /// The file from compaction.
+    static const FileSource& Compact();
+
+    int8_t ToByteValue() const {
+        return value_;
+    }
+
+    static Result<FileSource> FromByteValue(int8_t value) {
+        switch (value) {
+            case 0:
+                return Append();
+            case 1:
+                return Compact();
+            default:
+                return Status::Invalid(
+                    fmt::format("Unsupported byte value {} for value kind.", 
value));
+        }
+    }
+
+    bool operator==(const FileSource& other) const {
+        if (this == &other) {
+            return true;
+        }
+        return value_ == other.value_;
+    }
+
+    bool operator!=(const FileSource& other) const {
+        return !(*this == other);
+    }
+
+    std::string ToString() const {
+        switch (value_) {
+            case 0:
+                return "APPEND";
+            case 1:
+                return "COMPACT";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+ private:
+    explicit FileSource(int8_t value) : value_(value) {}
+
+ private:
+    int8_t value_;
+};
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/file_source_test.cpp 
b/src/paimon/core/manifest/file_source_test.cpp
new file mode 100644
index 0000000..c9011b3
--- /dev/null
+++ b/src/paimon/core/manifest/file_source_test.cpp
@@ -0,0 +1,35 @@
+/*
+ * 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/core/manifest/file_source.h"
+
+#include "gtest/gtest.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+TEST(FileSourceTest, TestSimple) {
+    ASSERT_OK_AND_ASSIGN(FileSource append, FileSource::FromByteValue(0));
+    ASSERT_OK_AND_ASSIGN(FileSource compact, FileSource::FromByteValue(1));
+    ASSERT_NOK(FileSource::FromByteValue(2));
+    ASSERT_EQ(append.ToString(), "APPEND");
+    ASSERT_EQ(compact.ToString(), "COMPACT");
+    ASSERT_NE(append, compact);
+    ASSERT_EQ(append, append);
+}
+
+}  // namespace paimon::test
diff --git a/src/paimon/core/manifest/manifest_entry.cpp 
b/src/paimon/core/manifest/manifest_entry.cpp
new file mode 100644
index 0000000..c19e32a
--- /dev/null
+++ b/src/paimon/core/manifest/manifest_entry.cpp
@@ -0,0 +1,57 @@
+/*
+ * 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/core/manifest/manifest_entry.h"
+
+#include "arrow/type_fwd.h"
+
+namespace arrow {
+class DataType;
+}  // namespace arrow
+
+namespace paimon {
+const std::shared_ptr<arrow::DataType>& ManifestEntry::DataType() {
+    static std::shared_ptr<arrow::DataType> data_type =
+        arrow::struct_({arrow::field("_KIND", arrow::int8(), 
/*nullable=*/false),
+                        arrow::field("_PARTITION", arrow::binary(), 
/*nullable=*/false),
+                        arrow::field("_BUCKET", arrow::int32(), 
/*nullable=*/false),
+                        arrow::field("_TOTAL_BUCKETS", arrow::int32(), 
/*nullable=*/false),
+                        arrow::field("_FILE", DataFileMeta::DataType(), 
/*nullable=*/false)});
+    return data_type;
+}
+
+int64_t ManifestEntry::RecordCountAdd(const std::vector<ManifestEntry>& 
entries) {
+    int64_t record_count = 0;
+    for (const auto& entry : entries) {
+        if (entry.Kind() == FileKind::Add()) {
+            record_count += entry.File()->row_count;
+        }
+    }
+    return record_count;
+}
+
+int64_t ManifestEntry::RecordCountDelete(const std::vector<ManifestEntry>& 
entries) {
+    int64_t record_count = 0;
+    for (const auto& entry : entries) {
+        if (entry.Kind() == FileKind::Delete()) {
+            record_count += entry.File()->row_count;
+        }
+    }
+    return record_count;
+}
+
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/manifest_entry.h 
b/src/paimon/core/manifest/manifest_entry.h
new file mode 100644
index 0000000..85e387a
--- /dev/null
+++ b/src/paimon/core/manifest/manifest_entry.h
@@ -0,0 +1,160 @@
+/*
+ * 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 <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "fmt/format.h"
+#include "paimon/common/data/binary_row.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/manifest/file_entry.h"
+#include "paimon/core/manifest/file_kind.h"
+
+namespace arrow {
+class DataType;
+}  // namespace arrow
+
+namespace paimon {
+/// Entry of a manifest file, representing an addition / deletion of a data 
file.
+class ManifestEntry : public FileEntry {
+ public:
+    static const std::shared_ptr<arrow::DataType>& DataType();
+    static int64_t RecordCountAdd(const std::vector<ManifestEntry>& 
manifest_entries);
+    static int64_t RecordCountDelete(const std::vector<ManifestEntry>& 
manifest_entries);
+
+    ManifestEntry(const FileKind& kind, const BinaryRow& partition, int32_t 
bucket,
+                  int32_t total_buckets, const std::shared_ptr<DataFileMeta>& 
file)
+        : kind_(kind),
+          partition_(partition),
+          bucket_(bucket),
+          total_buckets_(total_buckets),
+          file_(file) {}
+
+    ManifestEntry(const ManifestEntry& other) noexcept {
+        *this = other;
+    }
+
+    ManifestEntry& operator=(const ManifestEntry& other) noexcept {
+        if (&other == this) {
+            return *this;
+        }
+        kind_ = other.kind_;
+        partition_ = other.partition_;
+        bucket_ = other.bucket_;
+        total_buckets_ = other.total_buckets_;
+        file_ = other.file_;
+        return *this;
+    }
+
+    ManifestEntry(ManifestEntry&& other) noexcept {
+        *this = std::move(other);
+    }
+
+    ManifestEntry& operator=(ManifestEntry&& other) noexcept {
+        if (&other == this) {
+            return *this;
+        }
+        kind_ = other.kind_;
+        partition_ = other.partition_;
+        bucket_ = other.bucket_;
+        total_buckets_ = other.total_buckets_;
+        file_ = std::move(other.file_);
+        return *this;
+    }
+
+    const FileKind& Kind() const override {
+        return kind_;
+    }
+
+    const BinaryRow& Partition() const override {
+        return partition_;
+    }
+
+    int32_t Bucket() const override {
+        return bucket_;
+    }
+
+    int32_t Level() const override {
+        return file_->level;
+    }
+
+    const std::string& FileName() const override {
+        return file_->file_name;
+    }
+
+    const std::optional<std::string>& ExternalPath() const override {
+        return file_->external_path;
+    }
+
+    const BinaryRow& MinKey() const override {
+        return file_->min_key;
+    }
+
+    const BinaryRow& MaxKey() const override {
+        return file_->max_key;
+    }
+
+    FileEntry::Identifier CreateIdentifier() const override {
+        return FileEntry::Identifier(partition_, bucket_, file_->level, 
file_->file_name,
+                                     file_->external_path);
+    }
+
+    int32_t TotalBuckets() const {
+        return total_buckets_;
+    }
+
+    const std::shared_ptr<DataFileMeta>& File() const {
+        return file_;
+    }
+
+    bool operator==(const ManifestEntry& other) const {
+        if (this == &other) {
+            return true;
+        }
+        return kind_ == other.kind_ && partition_ == other.partition_ && 
bucket_ == other.bucket_ &&
+               total_buckets_ == other.total_buckets_ && *file_ == 
*(other.file_);
+    }
+
+    std::string ToString() const {
+        return fmt::format("{{{}, {}, {}, {}, {}}}", 
static_cast<int32_t>(kind_.ToByteValue()),
+                           partition_.ToString(), bucket_, total_buckets_, 
file_->ToString());
+    }
+
+    void AssignSequenceNumber(int64_t min_sequence_number, int64_t 
max_sequence_number) {
+        file_->AssignSequenceNumber(min_sequence_number, max_sequence_number);
+    }
+
+    void AssignFirstRowId(int64_t first_row_id) {
+        file_->AssignFirstRowId(first_row_id);
+    }
+
+ private:
+    FileKind kind_;
+    // for tables without partition this field should be a row with 0 columns 
(not null)
+    BinaryRow partition_;
+    int32_t bucket_;
+    int32_t total_buckets_;
+    std::shared_ptr<DataFileMeta> file_;
+};
+
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/manifest_entry_serializer.cpp 
b/src/paimon/core/manifest/manifest_entry_serializer.cpp
new file mode 100644
index 0000000..b8176d8
--- /dev/null
+++ b/src/paimon/core/manifest/manifest_entry_serializer.cpp
@@ -0,0 +1,79 @@
+/*
+ * 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/core/manifest/manifest_entry_serializer.h"
+
+#include <string>
+#include <utility>
+
+#include "fmt/format.h"
+#include "paimon/common/data/binary_row.h"
+#include "paimon/common/data/internal_row.h"
+#include "paimon/common/utils/serialization_utils.h"
+#include "paimon/core/manifest/file_kind.h"
+#include "paimon/status.h"
+
+namespace paimon {
+class MemoryPool;
+struct DataFileMeta;
+
+Result<ManifestEntry> ManifestEntrySerializer::ConvertFrom(int32_t version,
+                                                           const InternalRow& 
row) const {
+    if (version != VERSION_2) {
+        if (version == VERSION_1) {
+            return Status::Invalid(
+                fmt::format("The current version {} is not compatible with the 
version {}, "
+                            "please recreate the table.",
+                            GetVersion(), version));
+        }
+        return Status::Invalid("Unsupported version", std::to_string(version));
+    }
+    auto kind = row.GetByte(0);
+    PAIMON_ASSIGN_OR_RAISE(FileKind file_kind, FileKind::FromByteValue(kind));
+    auto partition_bytes = row.GetBinary(1);
+    PAIMON_ASSIGN_OR_RAISE(BinaryRow partition,
+                           
SerializationUtils::DeserializeBinaryRow(partition_bytes));
+    auto bucket = row.GetInt(2);
+    auto total_buckets = row.GetInt(3);
+    auto file = row.GetRow(4, data_file_meta_serializer_.NumFields());
+    if (!file) {
+        return Status::Invalid("ManifestEntry convert from row failed, with 
null DataFileMeta");
+    }
+    PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<DataFileMeta> meta,
+                           data_file_meta_serializer_.FromRow(*file))
+    return ManifestEntry(file_kind, partition, bucket, total_buckets, meta);
+}
+
+Result<BinaryRow> ManifestEntrySerializer::ToRow(const ManifestEntry& record) 
const {
+    BinaryRow row(GetDataType()->num_fields());
+    BinaryRowWriter writer(&row, 32 * 1024, pool_.get());
+
+    writer.WriteInt(0, GetVersion());
+    writer.WriteByte(1, record.Kind().ToByteValue());
+    auto partition_bytes = 
SerializationUtils::SerializeBinaryRow(record.Partition(), pool_.get());
+    assert(partition_bytes);
+    writer.WriteBinary(2, *partition_bytes);
+    writer.WriteInt(3, record.Bucket());
+    writer.WriteInt(4, record.TotalBuckets());
+    PAIMON_ASSIGN_OR_RAISE(BinaryRow data_file_meta_row,
+                           data_file_meta_serializer_.ToRow(record.File()));
+    writer.WriteRow(5, data_file_meta_row);
+    writer.Complete();
+    return row;
+}
+
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/manifest_entry_serializer.h 
b/src/paimon/core/manifest/manifest_entry_serializer.h
new file mode 100644
index 0000000..7438895
--- /dev/null
+++ b/src/paimon/core/manifest/manifest_entry_serializer.h
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+#include "arrow/api.h"
+#include "arrow/c/bridge.h"
+#include "paimon/common/utils/arrow/mem_utils.h"
+#include "paimon/core/io/data_file_meta_serializer.h"
+#include "paimon/core/manifest/manifest_entry.h"
+#include "paimon/core/utils/versioned_object_serializer.h"
+#include "paimon/result.h"
+
+namespace arrow {
+class ArrayBuilder;
+}  // namespace arrow
+struct ArrowArray;
+
+namespace paimon {
+class InternalRow;
+class MemoryPool;
+
+/// Serializer for `ManifestEntry`.
+class ManifestEntrySerializer : public 
VersionedObjectSerializer<ManifestEntry> {
+ public:
+    explicit ManifestEntrySerializer(const std::shared_ptr<MemoryPool>& pool)
+        : VersionedObjectSerializer<ManifestEntry>(pool),
+          arrow_pool_(GetArrowPool(pool_)),
+          data_file_meta_serializer_(pool) {}
+
+    int32_t GetVersion() const override {
+        return VERSION_2;
+    }
+
+    Result<ManifestEntry> ConvertFrom(int32_t version, const InternalRow& row) 
const override;
+
+    Result<BinaryRow> ToRow(const ManifestEntry& record) const override;
+
+ private:
+    static constexpr int32_t VERSION_1 = 1;
+    static constexpr int32_t VERSION_2 = 2;
+    std::unique_ptr<arrow::MemoryPool> arrow_pool_;
+    DataFileMetaSerializer data_file_meta_serializer_;
+};
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/manifest_entry_serializer_test.cpp 
b/src/paimon/core/manifest/manifest_entry_serializer_test.cpp
new file mode 100644
index 0000000..3fa2346
--- /dev/null
+++ b/src/paimon/core/manifest/manifest_entry_serializer_test.cpp
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "paimon/core/manifest/manifest_entry_serializer.h"
+
+#include <optional>
+#include <string>
+
+#include "gtest/gtest.h"
+#include "paimon/common/data/binary_row.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/manifest/file_kind.h"
+#include "paimon/core/manifest/manifest_entry.h"
+#include "paimon/core/stats/simple_stats.h"
+#include "paimon/data/timestamp.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/testing/utils/binary_row_generator.h"
+#include "paimon/testing/utils/testharness.h"
+namespace paimon::test {
+
+class ManifestEntrySerializerTest : public testing::Test {
+ private:
+    std::shared_ptr<DataFileMeta> GetDataFileMeta() {
+        return std::make_shared<DataFileMeta>(
+            "some_file_name", 1024, 8, DataFileMeta::EmptyMinKey(), 
DataFileMeta::EmptyMaxKey(),
+            SimpleStats::EmptyStats(), SimpleStats::EmptyStats(), 
/*min_seq_no=*/16,
+            /*max_seq_no=*/32,
+            /*schema_id=*/1, /*level=*/2, 
/*extra_files=*/std::vector<std::optional<std::string>>(),
+            /*creation_time=*/Timestamp(0, 0), /*delete_row_count=*/3,
+            /*embedded_index=*/nullptr, /*file_source=*/std::nullopt,
+            /*value_stats_cols=*/std::nullopt, 
/*external_path=*/std::optional<std::string>(),
+            /*first_row_id=*/std::nullopt, /*write_cols=*/std::nullopt);
+    }
+};
+TEST_F(ManifestEntrySerializerTest, TestToFromRow) {
+    auto pool = GetDefaultPool();
+    std::vector<ManifestEntry> entries = {
+        ManifestEntry(FileKind::Add(), BinaryRow::EmptyRow(), 0, 2, 
GetDataFileMeta()),
+        ManifestEntry(FileKind::Add(), BinaryRowGenerator::GenerateRow({10}, 
pool.get()), 1, 2,
+                      GetDataFileMeta())};
+    ManifestEntrySerializer serializer(pool);
+    for (const auto& entry : entries) {
+        ASSERT_OK_AND_ASSIGN(auto row, serializer.ToRow(entry));
+        ASSERT_OK_AND_ASSIGN(auto result_entry, serializer.FromRow(row));
+        ASSERT_EQ(entry, result_entry);
+        ASSERT_EQ(entry.ToString(), result_entry.ToString());
+    }
+}
+}  // namespace paimon::test
diff --git a/src/paimon/core/manifest/manifest_entry_writer.cpp 
b/src/paimon/core/manifest/manifest_entry_writer.cpp
new file mode 100644
index 0000000..aa25ada
--- /dev/null
+++ b/src/paimon/core/manifest/manifest_entry_writer.cpp
@@ -0,0 +1,80 @@
+/*
+ * 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/core/manifest/manifest_entry_writer.h"
+
+#include <algorithm>
+#include <cassert>
+#include <utility>
+#include <vector>
+
+#include "fmt/format.h"
+#include "paimon/common/utils/path_util.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/manifest/file_kind.h"
+#include "paimon/core/manifest/manifest_entry.h"
+#include "paimon/core/stats/simple_stats.h"
+#include "paimon/core/stats/simple_stats_converter.h"
+
+namespace paimon {
+class ColumnStats;
+
+Status ManifestEntryWriter::Write(const ManifestEntry& entry) {
+    PAIMON_RETURN_NOT_OK(SingleFileWriter::Write(entry));
+    if (entry.Kind() == FileKind::Add()) {
+        num_added_files_++;
+    } else if (entry.Kind() == FileKind::Delete()) {
+        num_deleted_files_++;
+    } else {
+        return Status::NotImplemented(fmt::format(
+            "Unknown entry kind: {}", 
static_cast<int32_t>(entry.Kind().ToByteValue())));
+    }
+    schema_id_ = std::max(schema_id_, entry.File()->schema_id);
+    min_bucket_ = std::min(min_bucket_, entry.Bucket());
+    max_bucket_ = std::max(max_bucket_, entry.Bucket());
+    min_level_ = std::min(min_level_, entry.Level());
+    max_level_ = std::max(max_level_, entry.Level());
+
+    if (row_id_stats_) {
+        std::optional<int64_t> first_row_id = entry.File()->first_row_id;
+        if (first_row_id) {
+            row_id_stats_.value().Collect(first_row_id.value(), 
entry.File()->row_count);
+        } else {
+            row_id_stats_ = std::nullopt;
+        }
+    }
+
+    assert(partition_stats_collector_);
+    
PAIMON_RETURN_NOT_OK(partition_stats_collector_->Collect(entry.Partition()));
+    return Status::OK();
+}
+
+Result<ManifestFileMeta> ManifestEntryWriter::GetResult() {
+    PAIMON_ASSIGN_OR_RAISE(std::vector<std::shared_ptr<ColumnStats>> col_stats,
+                           partition_stats_collector_->GetResult());
+    PAIMON_ASSIGN_OR_RAISE(SimpleStats stats,
+                           SimpleStatsConverter::ToBinary(col_stats, 
pool_.get()));
+    return ManifestFileMeta(
+        PathUtil::GetName(path_), output_bytes_, num_added_files_, 
num_deleted_files_, stats,
+        schema_id_, min_bucket_, max_bucket_, min_level_, max_level_,
+        (row_id_stats_ == std::nullopt ? std::nullopt
+                                       : 
std::optional<int64_t>(row_id_stats_.value().min_row_id)),
+        (row_id_stats_ == std::nullopt ? std::nullopt
+                                       : 
std::optional<int64_t>(row_id_stats_.value().max_row_id)));
+}
+
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/manifest_entry_writer.h 
b/src/paimon/core/manifest/manifest_entry_writer.h
new file mode 100644
index 0000000..04e7090
--- /dev/null
+++ b/src/paimon/core/manifest/manifest_entry_writer.h
@@ -0,0 +1,81 @@
+/*
+ * 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 <algorithm>
+#include <cstdint>
+#include <functional>
+#include <limits>
+#include <memory>
+#include <optional>
+#include <string>
+
+#include "paimon/core/io/single_file_writer.h"
+#include "paimon/core/manifest/manifest_entry.h"
+#include "paimon/core/manifest/manifest_file_meta.h"
+#include "paimon/core/stats/simple_stats_collector.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace arrow {
+class Schema;
+}  // namespace arrow
+struct ArrowArray;
+
+namespace paimon {
+class ManifestEntry;
+class MemoryPool;
+
+class ManifestEntryWriter : public SingleFileWriter<const ManifestEntry&, 
ManifestFileMeta> {
+ public:
+    ManifestEntryWriter(const std::string& compression,
+                        std::function<Status(const ManifestEntry&, 
ArrowArray*)> converter,
+                        const std::shared_ptr<MemoryPool>& pool,
+                        const std::shared_ptr<arrow::Schema>& partition_type)
+        : SingleFileWriter<const ManifestEntry&, 
ManifestFileMeta>(compression, converter),
+          pool_(pool),
+          
partition_stats_collector_(std::make_shared<SimpleStatsCollector>(partition_type))
 {}
+
+    Status Write(const ManifestEntry& entry) override;
+    Result<ManifestFileMeta> GetResult() override;
+
+ private:
+    struct RowIdStats {
+        void Collect(int64_t first_row_id, int64_t row_count) {
+            min_row_id = std::min(min_row_id, first_row_id);
+            max_row_id = std::max(max_row_id, first_row_id + row_count - 1);
+        }
+
+        int64_t min_row_id = std::numeric_limits<int64_t>::max();
+        int64_t max_row_id = std::numeric_limits<int64_t>::min();
+    };
+
+    std::shared_ptr<MemoryPool> pool_;
+    std::shared_ptr<SimpleStatsCollector> partition_stats_collector_;
+
+    int64_t num_added_files_ = 0;
+    int64_t num_deleted_files_ = 0;
+    int64_t schema_id_ = 0;
+    int32_t min_bucket_ = std::numeric_limits<int32_t>::max();
+    int32_t max_bucket_ = std::numeric_limits<int32_t>::min();
+    int32_t min_level_ = std::numeric_limits<int32_t>::max();
+    int32_t max_level_ = std::numeric_limits<int32_t>::min();
+    std::optional<RowIdStats> row_id_stats_ = RowIdStats();
+};
+
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/manifest_entry_writer_test.cpp 
b/src/paimon/core/manifest/manifest_entry_writer_test.cpp
new file mode 100644
index 0000000..3c3d00d
--- /dev/null
+++ b/src/paimon/core/manifest/manifest_entry_writer_test.cpp
@@ -0,0 +1,192 @@
+/*
+ * 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/core/manifest/manifest_entry_writer.h"
+
+#include <map>
+#include <optional>
+#include <utility>
+#include <variant>
+#include <vector>
+
+#include "arrow/api.h"
+#include "arrow/c/abi.h"
+#include "arrow/c/bridge.h"
+#include "gtest/gtest.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/core/core_options.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/io/meta_to_arrow_array_converter.h"
+#include "paimon/core/manifest/file_kind.h"
+#include "paimon/core/manifest/file_source.h"
+#include "paimon/core/manifest/manifest_entry.h"
+#include "paimon/core/manifest/manifest_entry_serializer.h"
+#include "paimon/core/utils/versioned_object_serializer.h"
+#include "paimon/data/timestamp.h"
+#include "paimon/defs.h"
+#include "paimon/format/file_format.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/testing/utils/binary_row_generator.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon {
+class WriterBuilder;
+}  // namespace paimon
+
+namespace paimon::test {
+class ManifestEntryWriterTest : public ::testing::Test {
+ public:
+    ManifestEntry CreateEntry(const std::optional<int64_t>& first_row_id, 
int64_t row_count) const {
+        auto meta = std::make_shared<DataFileMeta>(
+            "data-d7725088-6bd4-4e70-9ce6-714ae93b47cc-0.orc", 
/*file_size=*/863, row_count,
+            /*min_key=*/BinaryRow::EmptyRow(),
+            /*max_key=*/BinaryRow::EmptyRow(),
+            /*key_stats=*/
+            SimpleStats::EmptyStats(),
+            /*value_stats=*/SimpleStats::EmptyStats(),
+            /*min_sequence_number=*/0, /*max_sequence_number=*/row_count, 
/*schema_id=*/0,
+            /*level=*/0, 
/*extra_files=*/std::vector<std::optional<std::string>>(),
+            /*creation_time=*/Timestamp(1743525392885ll, 0),
+            /*delete_row_count=*/0, /*embedded_index=*/nullptr, 
FileSource::Append(),
+            /*value_stats_cols=*/std::nullopt,
+            /*external_path=*/std::nullopt, first_row_id,
+            /*write_cols=*/std::nullopt);
+        return {FileKind::Add(), BinaryRowGenerator::GenerateRow({10}, 
pool_.get()), /*bucket=*/0,
+                /*total_buckets=*/-1, meta};
+    }
+
+    std::unique_ptr<ManifestEntryWriter> CreateEntryWriter(
+        const std::string& entry_file_name) const {
+        std::shared_ptr<arrow::Schema> part_type =
+            arrow::schema(arrow::FieldVector({arrow::field("f1", 
arrow::int32())}));
+        auto serializer = std::make_shared<ManifestEntrySerializer>(pool_);
+        EXPECT_OK_AND_ASSIGN(std::shared_ptr<MetaToArrowArrayConverter> 
to_array_converter,
+                             
MetaToArrowArrayConverter::Create(serializer->GetDataType(), pool_));
+
+        auto converter = [serializer, to_array_converter](ManifestEntry entry,
+                                                          ::ArrowArray* dest) 
-> Status {
+            PAIMON_ASSIGN_OR_RAISE(BinaryRow entry_row, 
serializer->ToRow(entry));
+            PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> array,
+                                   to_array_converter->NextBatch({entry_row}));
+            PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, dest));
+            return Status::OK();
+        };
+        auto writer = std::make_unique<ManifestEntryWriter>("zstd", converter, 
pool_, part_type);
+
+        EXPECT_OK_AND_ASSIGN(CoreOptions options,
+                             CoreOptions::FromMap({{Options::FILE_FORMAT, 
"orc"}}));
+        auto file_format = options.GetWriteFileFormat(/*level=*/0);
+        std::shared_ptr<arrow::DataType> data_type =
+            
VersionedObjectSerializer<ManifestEntry>::VersionType(ManifestEntry::DataType());
+        ArrowSchema arrow_schema;
+        EXPECT_TRUE(arrow::ExportType(*data_type, &arrow_schema).ok());
+        EXPECT_OK_AND_ASSIGN(std::shared_ptr<WriterBuilder> writer_builder,
+                             file_format->CreateWriterBuilder(&arrow_schema, 
/*batch_size=*/100));
+        EXPECT_OK(writer->Init(options.GetFileSystem(), entry_file_name, 
writer_builder));
+        return writer;
+    }
+
+ private:
+    std::shared_ptr<MemoryPool> pool_ = GetDefaultPool();
+};
+
+TEST_F(ManifestEntryWriterTest, TestSimple) {
+    auto dir = UniqueTestDirectory::Create();
+    ASSERT_TRUE(dir);
+    std::string entry_file_name = dir->Str() + 
"/manifest/my-manifest-file-name";
+    auto writer = CreateEntryWriter(entry_file_name);
+
+    auto meta1 = std::make_shared<DataFileMeta>(
+        "data-d7725088-6bd4-4e70-9ce6-714ae93b47cc-0.orc", /*file_size=*/863, 
/*row_count=*/1,
+        /*min_key=*/BinaryRowGenerator::GenerateRow({std::string("Alice"), 1}, 
pool_.get()),
+        /*max_key=*/BinaryRowGenerator::GenerateRow({std::string("Alice"), 1}, 
pool_.get()),
+        /*key_stats=*/
+        BinaryRowGenerator::GenerateStats({std::string("Alice"), 1}, 
{std::string("Alice"), 1},
+                                          {0, 0}, pool_.get()),
+        /*value_stats=*/
+        BinaryRowGenerator::GenerateStats({std::string("Alice"), 10, 1, 11.1},
+                                          {std::string("Alice"), 10, 1, 11.1}, 
{0, 0, 0, 0},
+                                          pool_.get()),
+        /*min_sequence_number=*/0, /*max_sequence_number=*/0, /*schema_id=*/0,
+        /*level=*/4, /*extra_files=*/std::vector<std::optional<std::string>>(),
+        /*creation_time=*/Timestamp(1743525392885ll, 0),
+        /*delete_row_count=*/0, /*embedded_index=*/nullptr, 
FileSource::Append(),
+        /*value_stats_cols=*/std::nullopt,
+        /*external_path=*/std::nullopt, /*first_row_id=*/std::nullopt, 
/*write_cols=*/std::nullopt);
+
+    auto meta2 = std::make_shared<DataFileMeta>(
+        "data-5858a84b-7081-4618-b828-ae3918c5e1f6-0.orc", /*file_size=*/943, 
/*row_count=*/4,
+        /*min_key=*/BinaryRowGenerator::GenerateRow({std::string("Alex"), 0}, 
pool_.get()),
+        /*max_key=*/BinaryRowGenerator::GenerateRow({std::string("Tony"), 0}, 
pool_.get()),
+        /*key_stats=*/
+        BinaryRowGenerator::GenerateStats({std::string("Alex"), 0}, 
{std::string("Tony"), 0},
+                                          {0, 0}, pool_.get()),
+        /*value_stats=*/
+        BinaryRowGenerator::GenerateStats({std::string("Alex"), 20, 0, 12.1},
+                                          {std::string("Tony"), 20, 0, 16.1}, 
{0, 0, 0, 0},
+                                          pool_.get()),
+        /*min_sequence_number=*/0, /*max_sequence_number=*/3, /*schema_id=*/0,
+        /*level=*/5, /*extra_files=*/std::vector<std::optional<std::string>>(),
+        /*creation_time=*/Timestamp(1743525392921ll, 0),
+        /*delete_row_count=*/0, /*embedded_index=*/nullptr, 
FileSource::Append(),
+        /*value_stats_cols=*/std::nullopt,
+        /*external_path=*/std::nullopt, /*first_row_id=*/std::nullopt, 
/*write_cols=*/std::nullopt);
+
+    auto entry1 = ManifestEntry(FileKind::Add(), 
BinaryRowGenerator::GenerateRow({10}, pool_.get()),
+                                0, 2, meta1);
+    auto entry2 = ManifestEntry(FileKind::Add(), 
BinaryRowGenerator::GenerateRow({20}, pool_.get()),
+                                1, 2, meta2);
+    ASSERT_OK(writer->Write(entry1));
+    ASSERT_OK(writer->Write(entry2));
+    ASSERT_EQ(writer->min_bucket_, 0);
+    ASSERT_EQ(writer->max_bucket_, 1);
+    ASSERT_EQ(writer->min_level_, 4);
+    ASSERT_EQ(writer->max_level_, 5);
+
+    // check partition stats
+    ASSERT_OK_AND_ASSIGN(ManifestFileMeta meta, writer->GetResult());
+    auto partition_stats = meta.PartitionStats();
+    ASSERT_EQ(partition_stats, BinaryRowGenerator::GenerateStats({10}, {20}, 
{0}, pool_.get()));
+}
+
+TEST_F(ManifestEntryWriterTest, TestWithRowIds) {
+    auto dir = UniqueTestDirectory::Create();
+    ASSERT_TRUE(dir);
+    std::string entry_file_name = dir->Str() + 
"/manifest/my-manifest-file-name";
+    auto writer = CreateEntryWriter(entry_file_name);
+    // [10, 30)
+    auto entry1 = CreateEntry(/*first_row_id=*/10, /*row_count=*/20);
+    ASSERT_OK(writer->Write(entry1));
+    // [50, 80)
+    auto entry2 = CreateEntry(/*first_row_id=*/50, /*row_count=*/30);
+    ASSERT_OK(writer->Write(entry2));
+    ASSERT_TRUE(writer->row_id_stats_);
+    ASSERT_EQ(writer->row_id_stats_.value().min_row_id, 10);
+    ASSERT_EQ(writer->row_id_stats_.value().max_row_id, 79);
+    // null first row id forces row_id_stats_ to be always null
+    auto entry3 = CreateEntry(/*first_row_id=*/std::nullopt, /*row_count=*/30);
+    ASSERT_OK(writer->Write(entry3));
+    // [100, 110)
+    auto entry4 = CreateEntry(/*first_row_id=*/100, /*row_count=*/10);
+    ASSERT_OK(writer->Write(entry4));
+    ASSERT_FALSE(writer->row_id_stats_);
+
+    ASSERT_OK_AND_ASSIGN(ManifestFileMeta meta, writer->GetResult());
+    ASSERT_FALSE(meta.MinRowId());
+    ASSERT_FALSE(meta.MaxRowId());
+}
+}  // namespace paimon::test
diff --git a/src/paimon/core/manifest/manifest_file_meta.cpp 
b/src/paimon/core/manifest/manifest_file_meta.cpp
new file mode 100644
index 0000000..4174f24
--- /dev/null
+++ b/src/paimon/core/manifest/manifest_file_meta.cpp
@@ -0,0 +1,101 @@
+/*
+ * 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/core/manifest/manifest_file_meta.h"
+
+#include "arrow/type_fwd.h"
+#include "fmt/format.h"
+
+namespace arrow {
+class DataType;
+}  // namespace arrow
+
+namespace paimon {
+
+ManifestFileMeta::ManifestFileMeta(
+    const std::string& file_name, int64_t file_size, int64_t num_added_files,
+    int64_t num_deleted_files, const SimpleStats& partition_stats, int64_t 
schema_id,
+    const std::optional<int32_t>& min_bucket, const std::optional<int32_t>& 
max_bucket,
+    const std::optional<int32_t>& min_level, const std::optional<int32_t>& 
max_level,
+    const std::optional<int64_t>& min_row_id, const std::optional<int64_t>& 
max_row_id)
+    : file_name_(file_name),
+      file_size_(file_size),
+      num_added_files_(num_added_files),
+      num_deleted_files_(num_deleted_files),
+      partition_stats_(partition_stats),
+      schema_id_(schema_id),
+      min_bucket_(min_bucket),
+      max_bucket_(max_bucket),
+      min_level_(min_level),
+      max_level_(max_level),
+      min_row_id_(min_row_id),
+      max_row_id_(max_row_id) {}
+
+bool ManifestFileMeta::operator==(const ManifestFileMeta& other) const {
+    if (this == &other) {
+        return true;
+    }
+    return file_name_ == other.file_name_ && file_size_ == other.file_size_ &&
+           num_added_files_ == other.num_added_files_ &&
+           num_deleted_files_ == other.num_deleted_files_ &&
+           partition_stats_ == other.partition_stats_ && schema_id_ == 
other.schema_id_ &&
+           min_bucket_ == other.min_bucket_ && max_bucket_ == 
other.max_bucket_ &&
+           min_level_ == other.min_level_ && max_level_ == other.max_level_ &&
+           min_row_id_ == other.min_row_id_ && max_row_id_ == 
other.max_row_id_;
+}
+
+const std::shared_ptr<arrow::DataType>& ManifestFileMeta::DataType() {
+    static std::shared_ptr<arrow::DataType> data_type = arrow::struct_({
+        arrow::field("_FILE_NAME", arrow::utf8(), /*nullable=*/false),
+        arrow::field("_FILE_SIZE", arrow::int64(), /*nullable=*/false),
+        arrow::field("_NUM_ADDED_FILES", arrow::int64(), /*nullable=*/false),
+        arrow::field("_NUM_DELETED_FILES", arrow::int64(), /*nullable=*/false),
+        arrow::field("_PARTITION_STATS", SimpleStats::DataType(), 
/*nullable=*/false),
+        arrow::field("_SCHEMA_ID", arrow::int64(), /*nullable=*/false),
+        arrow::field("_MIN_BUCKET", arrow::int32(), /*nullable=*/true),
+        arrow::field("_MAX_BUCKET", arrow::int32(), /*nullable=*/true),
+        arrow::field("_MIN_LEVEL", arrow::int32(), /*nullable=*/true),
+        arrow::field("_MAX_LEVEL", arrow::int32(), /*nullable=*/true),
+        arrow::field("_MIN_ROW_ID", arrow::int64(), /*nullable=*/true),
+        arrow::field("_MAX_ROW_ID", arrow::int64(), /*nullable=*/true),
+    });
+    return data_type;
+}
+
+std::string ManifestFileMeta::ToString() const {
+    std::string min_bucket_str =
+        min_bucket_ != std::nullopt ? std::to_string(min_bucket_.value()) : 
"null";
+    std::string max_bucket_str =
+        max_bucket_ != std::nullopt ? std::to_string(max_bucket_.value()) : 
"null";
+
+    std::string min_level_str =
+        min_level_ != std::nullopt ? std::to_string(min_level_.value()) : 
"null";
+    std::string max_level_str =
+        max_level_ != std::nullopt ? std::to_string(max_level_.value()) : 
"null";
+
+    std::string min_row_id_str =
+        min_row_id_ != std::nullopt ? std::to_string(min_row_id_.value()) : 
"null";
+    std::string max_row_id_str =
+        max_row_id_ != std::nullopt ? std::to_string(max_row_id_.value()) : 
"null";
+
+    return fmt::format("{{{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}}}", 
file_name_, file_size_,
+                       num_added_files_, num_deleted_files_, 
partition_stats_.ToString(),
+                       schema_id_, min_bucket_str, max_bucket_str, 
min_level_str, max_level_str,
+                       min_row_id_str, max_row_id_str);
+}
+
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/manifest_file_meta.h 
b/src/paimon/core/manifest/manifest_file_meta.h
new file mode 100644
index 0000000..0cc0478
--- /dev/null
+++ b/src/paimon/core/manifest/manifest_file_meta.h
@@ -0,0 +1,111 @@
+/*
+ * 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 <memory>
+#include <optional>
+#include <string>
+
+#include "paimon/core/stats/simple_stats.h"
+#include "paimon/visibility.h"
+
+namespace arrow {
+class DataType;
+}  // namespace arrow
+
+namespace paimon {
+
+/// Metadata of a manifest file.
+class PAIMON_EXPORT ManifestFileMeta {
+ public:
+    static const std::shared_ptr<arrow::DataType>& DataType();
+
+    ManifestFileMeta(const std::string& file_name, int64_t file_size, int64_t 
num_added_files,
+                     int64_t num_deleted_files, const SimpleStats& 
partition_stats,
+                     int64_t schema_id, const std::optional<int32_t>& 
min_bucket,
+                     const std::optional<int32_t>& max_bucket,
+                     const std::optional<int32_t>& min_level,
+                     const std::optional<int32_t>& max_level,
+                     const std::optional<int64_t>& min_row_id,
+                     const std::optional<int64_t>& max_row_id);
+
+    const std::string& FileName() const {
+        return file_name_;
+    }
+
+    int64_t FileSize() const {
+        return file_size_;
+    }
+
+    int64_t NumAddedFiles() const {
+        return num_added_files_;
+    }
+
+    int64_t NumDeletedFiles() const {
+        return num_deleted_files_;
+    }
+
+    SimpleStats PartitionStats() const {
+        return partition_stats_;
+    }
+
+    int64_t SchemaId() const {
+        return schema_id_;
+    }
+
+    const std::optional<int32_t>& MinBucket() const {
+        return min_bucket_;
+    }
+    const std::optional<int32_t>& MaxBucket() const {
+        return max_bucket_;
+    }
+    const std::optional<int32_t>& MinLevel() const {
+        return min_level_;
+    }
+    const std::optional<int32_t>& MaxLevel() const {
+        return max_level_;
+    }
+
+    const std::optional<int64_t>& MinRowId() const {
+        return min_row_id_;
+    }
+    const std::optional<int64_t>& MaxRowId() const {
+        return max_row_id_;
+    }
+
+    std::string ToString() const;
+
+    bool operator==(const ManifestFileMeta& other) const;
+
+ private:
+    std::string file_name_;
+    int64_t file_size_;
+    int64_t num_added_files_;
+    int64_t num_deleted_files_;
+    SimpleStats partition_stats_;
+    int64_t schema_id_;
+    std::optional<int32_t> min_bucket_;
+    std::optional<int32_t> max_bucket_;
+    std::optional<int32_t> min_level_;
+    std::optional<int32_t> max_level_;
+    std::optional<int64_t> min_row_id_;
+    std::optional<int64_t> max_row_id_;
+};
+
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/manifest_file_meta_serializer.cpp 
b/src/paimon/core/manifest/manifest_file_meta_serializer.cpp
new file mode 100644
index 0000000..5018aea
--- /dev/null
+++ b/src/paimon/core/manifest/manifest_file_meta_serializer.cpp
@@ -0,0 +1,141 @@
+/*
+ * 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/core/manifest/manifest_file_meta_serializer.h"
+
+#include <optional>
+#include <string>
+#include <utility>
+
+#include "fmt/format.h"
+#include "paimon/common/data/binary_string.h"
+#include "paimon/common/data/internal_row.h"
+#include "paimon/core/stats/simple_stats.h"
+#include "paimon/status.h"
+
+namespace paimon {
+
+Result<BinaryRow> ManifestFileMetaSerializer::ToRow(const ManifestFileMeta& 
record) const {
+    BinaryRow row(GetDataType()->num_fields());
+    BinaryRowWriter writer(&row, 32 * 1024, pool_.get());
+
+    writer.WriteInt(0, GetVersion());
+    writer.WriteString(1, BinaryString::FromString(record.FileName(), 
pool_.get()));
+    writer.WriteLong(2, record.FileSize());
+    writer.WriteLong(3, record.NumAddedFiles());
+    writer.WriteLong(4, record.NumDeletedFiles());
+    writer.WriteRow(5, record.PartitionStats().ToRow());
+    writer.WriteLong(6, record.SchemaId());
+
+    auto min_bucket = record.MinBucket();
+    if (!min_bucket) {
+        writer.SetNullAt(7);
+    } else {
+        writer.WriteInt(7, min_bucket.value());
+    }
+
+    auto max_bucket = record.MaxBucket();
+    if (!max_bucket) {
+        writer.SetNullAt(8);
+    } else {
+        writer.WriteInt(8, max_bucket.value());
+    }
+
+    auto min_level = record.MinLevel();
+    if (!min_level) {
+        writer.SetNullAt(9);
+    } else {
+        writer.WriteInt(9, min_level.value());
+    }
+
+    auto max_level = record.MaxLevel();
+    if (!max_level) {
+        writer.SetNullAt(10);
+    } else {
+        writer.WriteInt(10, max_level.value());
+    }
+    auto min_row_id = record.MinRowId();
+    if (!min_row_id) {
+        writer.SetNullAt(11);
+    } else {
+        writer.WriteInt(11, min_row_id.value());
+    }
+
+    auto max_row_id = record.MaxRowId();
+    if (!max_row_id) {
+        writer.SetNullAt(12);
+    } else {
+        writer.WriteInt(12, max_row_id.value());
+    }
+    writer.Complete();
+    return row;
+}
+
+Result<ManifestFileMeta> ManifestFileMetaSerializer::ConvertFrom(int32_t 
version,
+                                                                 const 
InternalRow& row) const {
+    if (version != VERSION_2) {
+        if (version == VERSION_1) {
+            return Status::Invalid(
+                fmt::format("The current version {} is not compatible with the 
"
+                            "version {}, please recreate the table.",
+                            GetVersion(), version));
+        }
+        return Status::Invalid(fmt::format("Unsupported version: {}", 
version));
+    }
+    auto file_name = row.GetString(0);
+    auto file_size = row.GetLong(1);
+    auto num_added_files = row.GetLong(2);
+    auto num_deleted_files = row.GetLong(3);
+    auto partition_stats_row = row.GetRow(4, 3);
+    if (partition_stats_row == nullptr) {
+        return Status::Invalid(
+            "ManifestFileMeta convert from row failed, with null partition 
stats");
+    }
+    PAIMON_ASSIGN_OR_RAISE(SimpleStats partition_stats,
+                           SimpleStats::FromRow(partition_stats_row.get(), 
pool_.get()));
+
+    auto schema_id = row.GetLong(5);
+    std::optional<int32_t> min_bucket;
+    if (!row.IsNullAt(6)) {
+        min_bucket = row.GetInt(6);
+    }
+    std::optional<int32_t> max_bucket;
+    if (!row.IsNullAt(7)) {
+        max_bucket = row.GetInt(7);
+    }
+    std::optional<int32_t> min_level;
+    if (!row.IsNullAt(8)) {
+        min_level = row.GetInt(8);
+    }
+    std::optional<int32_t> max_level;
+    if (!row.IsNullAt(9)) {
+        max_level = row.GetInt(9);
+    }
+    std::optional<int64_t> min_row_id;
+    if (!row.IsNullAt(10)) {
+        min_row_id = row.GetLong(10);
+    }
+    std::optional<int64_t> max_row_id;
+    if (!row.IsNullAt(11)) {
+        max_row_id = row.GetLong(11);
+    }
+    return ManifestFileMeta(file_name.ToString(), file_size, num_added_files, 
num_deleted_files,
+                            partition_stats, schema_id, min_bucket, 
max_bucket, min_level,
+                            max_level, min_row_id, max_row_id);
+}
+
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/manifest_file_meta_serializer.h 
b/src/paimon/core/manifest/manifest_file_meta_serializer.h
new file mode 100644
index 0000000..b726065
--- /dev/null
+++ b/src/paimon/core/manifest/manifest_file_meta_serializer.h
@@ -0,0 +1,62 @@
+/*
+ * 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 <memory>
+#include <vector>
+
+#include "arrow/api.h"
+#include "paimon/common/utils/arrow/mem_utils.h"
+#include "paimon/core/manifest/manifest_entry.h"
+#include "paimon/core/manifest/manifest_file_meta.h"
+#include "paimon/core/utils/versioned_object_serializer.h"
+#include "paimon/result.h"
+
+struct ArrowArray;
+
+namespace arrow {
+class ArrayBuilder;
+}  // namespace arrow
+
+namespace paimon {
+class InternalRow;
+class MemoryPool;
+
+/// Serializer for `ManifestFileMeta`.
+class ManifestFileMetaSerializer : public 
VersionedObjectSerializer<ManifestFileMeta> {
+ public:
+    explicit ManifestFileMetaSerializer(const std::shared_ptr<MemoryPool>& 
pool)
+        : VersionedObjectSerializer<ManifestFileMeta>(pool), 
arrow_pool_(GetArrowPool(pool_)) {}
+
+    int32_t GetVersion() const override {
+        return VERSION_2;
+    }
+
+    Result<ManifestFileMeta> ConvertFrom(int32_t version, const InternalRow& 
row) const override;
+
+    Result<BinaryRow> ToRow(const ManifestFileMeta& record) const override;
+
+ private:
+    static constexpr int32_t VERSION_2 = 2;
+    static constexpr int32_t VERSION_1 = 1;
+
+    std::unique_ptr<arrow::MemoryPool> arrow_pool_;
+};
+
+}  // namespace paimon
diff --git a/src/paimon/core/manifest/manifest_file_meta_serializer_test.cpp 
b/src/paimon/core/manifest/manifest_file_meta_serializer_test.cpp
new file mode 100644
index 0000000..98e79c1
--- /dev/null
+++ b/src/paimon/core/manifest/manifest_file_meta_serializer_test.cpp
@@ -0,0 +1,107 @@
+/*
+ * 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/core/manifest/manifest_file_meta_serializer.h"
+
+#include <optional>
+#include <string>
+#include <variant>
+
+#include "gtest/gtest.h"
+#include "paimon/common/data/binary_row.h"
+#include "paimon/common/data/binary_string.h"
+#include "paimon/common/data/data_define.h"
+#include "paimon/common/data/generic_row.h"
+#include "paimon/core/manifest/manifest_file_meta.h"
+#include "paimon/core/stats/simple_stats.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/status.h"
+#include "paimon/testing/utils/binary_row_generator.h"
+#include "paimon/testing/utils/testharness.h"
+namespace paimon::test {
+TEST(ManifestFileMetaSerializerTest, TestToFromRow) {
+    ManifestFileMeta meta1(/*file_name=*/"meta1", /*file_size=*/10, 
/*num_added_files=*/15,
+                           /*num_deleted_files=*/20, 
SimpleStats::EmptyStats(), /*schema_id=*/0,
+                           /*min_bucket=*/std::nullopt, 
/*max_bucket=*/std::nullopt,
+                           /*min_level=*/std::nullopt, 
/*max_level=*/std::nullopt,
+                           /*min_row_id=*/std::nullopt, 
/*max_row_id=*/std::nullopt);
+    ManifestFileMeta meta2(/*file_name=*/"meta2", /*file_size=*/25, 
/*num_added_files=*/30,
+                           /*num_deleted_files=*/35, 
SimpleStats::EmptyStats(), /*schema_id=*/1,
+                           /*min_bucket=*/std::nullopt, 
/*max_bucket=*/std::nullopt,
+                           /*min_level=*/std::nullopt, 
/*max_level=*/std::nullopt,
+                           /*min_row_id=*/200, /*max_row_id=*/233);
+
+    std::vector<ManifestFileMeta> metas = {meta1, meta2};
+    ManifestFileMetaSerializer serializer(GetDefaultPool());
+    for (const auto& meta : metas) {
+        ASSERT_OK_AND_ASSIGN(auto row, serializer.ToRow(meta));
+        ASSERT_OK_AND_ASSIGN(auto result_meta, serializer.FromRow(row));
+        ASSERT_EQ(result_meta, meta);
+        ASSERT_EQ(result_meta.ToString(), meta.ToString());
+    }
+}
+
+TEST(ManifestFileMetaSerializerTest, TestInvalidCase) {
+    auto pool = GetDefaultPool();
+
+    {
+        GenericRow row(12);
+        row.SetField(0, BinaryString::FromString("meta1", pool.get()));
+        row.SetField(1, 10L);
+        row.SetField(2, 20L);
+        row.SetField(3, 30L);
+        auto simple_stats = BinaryRowGenerator::GenerateStats(
+            {"Alex", 10, 0, 12.1}, {"Emily", 10, 0, 16.1}, {0, 0, 0, 0}, 
pool.get());
+        auto simple_stats_row = 
std::make_shared<BinaryRow>(simple_stats.ToRow());
+        row.SetField(4, simple_stats_row);
+        row.SetField(5, 2L);
+        row.SetField(6, NullType());
+        row.SetField(7, NullType());
+        row.SetField(8, NullType());
+        row.SetField(9, NullType());
+        row.SetField(10, NullType());
+        row.SetField(11, NullType());
+
+        ManifestFileMetaSerializer serializer(pool);
+        ASSERT_NOK_WITH_MSG(serializer.ConvertFrom(/*version=*/1, row),
+                            "The current version 2 is not compatible with the 
version 1, please "
+                            "recreate the table.");
+        ASSERT_NOK_WITH_MSG(serializer.ConvertFrom(/*version=*/3, row), 
"Unsupported version: 3");
+        ASSERT_OK(serializer.ConvertFrom(/*version=*/2, row));
+    }
+    {
+        GenericRow row(12);
+        row.SetField(0, BinaryString::FromString("meta1", pool.get()));
+        row.SetField(1, 10L);
+        row.SetField(2, 20L);
+        row.SetField(3, 30L);
+        row.SetField(4, std::shared_ptr<BinaryRow>());
+        row.SetField(5, 2L);
+        row.SetField(6, NullType());
+        row.SetField(7, NullType());
+        row.SetField(8, NullType());
+        row.SetField(9, NullType());
+        row.SetField(10, NullType());
+        row.SetField(11, NullType());
+
+        ManifestFileMetaSerializer serializer(pool);
+        ASSERT_NOK_WITH_MSG(serializer.ConvertFrom(/*version=*/2, row),
+                            "ManifestFileMeta convert from row failed, with 
null partition stats");
+    }
+}
+
+}  // namespace paimon::test

Reply via email to