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 b1a0598  feat: add read-ahead cache, serialization, stream and object 
utils (#75)
b1a0598 is described below

commit b1a05987803b8ebd2081c7eb285b8ccb03397171
Author: dalingmeng <[email protected]>
AuthorDate: Tue Jun 16 09:12:02 2026 +0800

    feat: add read-ahead cache, serialization, stream and object utils (#75)
---
 LICENSE                                            |   3 +
 include/paimon/utils/read_ahead_cache.h            | 183 +++++++++++++++
 src/paimon/common/utils/object_utils.h             | 153 ++++++++++++
 src/paimon/common/utils/object_utils_test.cpp      | 116 +++++++++
 src/paimon/common/utils/read_ahead_cache.cpp       | 258 +++++++++++++++++++++
 src/paimon/common/utils/read_ahead_cache_test.cpp  | 159 +++++++++++++
 src/paimon/common/utils/serialization_utils.h      | 103 ++++++++
 .../common/utils/serialization_utils_test.cpp      |  40 ++++
 src/paimon/common/utils/stream_utils.h             | 102 ++++++++
 src/paimon/common/utils/stream_utils_test.cpp      | 146 ++++++++++++
 10 files changed, 1263 insertions(+)

diff --git a/LICENSE b/LICENSE
index c69084f..175f5e7 100644
--- a/LICENSE
+++ b/LICENSE
@@ -383,6 +383,9 @@ This product includes code from Apache ORC.
 
 * ORC patch file:
   * cmake_modules/orc.diff
+* Adapted read-ahead cache:
+  * include/paimon/utils/read_ahead_cache.h (adapted from Cache.hh)
+  * src/paimon/common/utils/read_ahead_cache.cpp (adapted from Cache.cc)
 
 Copyright: 2013 and onwards The Apache Software Foundation.
 Home page: https://orc.apache.org/
diff --git a/include/paimon/utils/read_ahead_cache.h 
b/include/paimon/utils/read_ahead_cache.h
new file mode 100644
index 0000000..196045b
--- /dev/null
+++ b/include/paimon/utils/read_ahead_cache.h
@@ -0,0 +1,183 @@
+/*
+ * 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.
+ */
+
+// Adapted from Apache ORC
+// https://github.com/apache/orc/blob/main/c%2B%2B/src/io/Cache.hh
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+#include <vector>
+
+#include "paimon/fs/file_system.h"
+#include "paimon/memory/bytes.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+#include "paimon/visibility.h"
+
+namespace paimon {
+
+/// PrefetchCacheMode
+/// Cache prefetch switch modes.
+/// Controls whether to enable cache prefetching under different 
circumstances, such as queries with
+/// predicates or bitmap indexes.
+///
+/// - ALWAYS: Enable cache in all scenarios.
+/// - EXCLUDE_PREDICATE: Disable cache when query has predicates.
+/// - EXCLUDE_BITMAP: Disable cache when using bitmap index.
+/// - EXCLUDE_BITMAP_OR_PREDICATE: Disable cache if query has predicates or 
bitmap index.
+/// - NEVER: Always disable cache.
+enum class PAIMON_EXPORT PrefetchCacheMode {
+    ALWAYS = 1,
+    EXCLUDE_PREDICATE = 2,
+    EXCLUDE_BITMAP = 3,
+    EXCLUDE_BITMAP_OR_PREDICATE = 4,
+    NEVER = 5
+};
+
+/// Configuration parameters for the read-ahead cache behavior.
+///
+/// This struct controls various limits and prefetching strategies used by
+/// ReadAheadCache to balance memory usage, I/O efficiency, and latency hiding.
+class PAIMON_EXPORT CacheConfig {
+ public:
+    CacheConfig();
+    CacheConfig(uint64_t buffer_size_limit, uint64_t range_size_limit, 
uint64_t hole_size_limit,
+                uint64_t pre_buffer_limit);
+
+    /// Returns the maximum total size (in bytes) of cached data.
+    uint64_t GetBufferSizeLimit() const {
+        return buffer_size_limit_;
+    }
+
+    /// Sets the maximum total size (in bytes) of cached data.
+    void SetBufferSizeLimit(uint64_t buffer_size_limit) {
+        buffer_size_limit_ = buffer_size_limit;
+    }
+
+    /// Returns the maximum allowed size (in bytes) for a single cached range.
+    uint64_t GetRangeSizeLimit() const {
+        return range_size_limit_;
+    }
+
+    /// Sets the maximum allowed size (in bytes) for a single cached range.
+    void SetRangeSizeLimit(uint64_t range_size_limit) {
+        range_size_limit_ = range_size_limit;
+    }
+
+    /// Returns the maximum gap size (in bytes) considered mergeable between 
adjacent ranges.
+    uint64_t GetHoleSizeLimit() const {
+        return hole_size_limit_;
+    }
+
+    /// Sets the maximum gap size (in bytes) considered mergeable between 
adjacent ranges.
+    void SetHoleSizeLimit(uint64_t hole_size_limit) {
+        hole_size_limit_ = hole_size_limit;
+    }
+
+    /// Returns the maximum size to pre-buffer ahead of the current read 
position.
+    uint64_t GetPreBufferLimit() const {
+        return pre_buffer_limit_;
+    }
+
+    /// Sets the maximum size to pre-buffer ahead of the current read position.
+    void SetPreBufferLimit(uint64_t pre_buffer_limit) {
+        pre_buffer_limit_ = pre_buffer_limit;
+    }
+
+ private:
+    uint64_t buffer_size_limit_;
+    uint64_t range_size_limit_;
+    uint64_t hole_size_limit_;
+    uint64_t pre_buffer_limit_;
+};
+
+/// A byte range with offset and length.
+struct PAIMON_EXPORT ByteRange {
+    uint64_t offset;
+    uint64_t length;
+
+    ByteRange() = default;
+    ByteRange(uint64_t offset, uint64_t length) : offset(offset), 
length(length) {}
+
+    friend bool operator==(const ByteRange& left, const ByteRange& right) {
+        return (left.offset == right.offset && left.length == right.length);
+    }
+    friend bool operator!=(const ByteRange& left, const ByteRange& right) {
+        return !(left == right);
+    }
+
+    /// @param other The other byte range to check.
+    /// @return true if this range contains the other range
+    bool Contains(const ByteRange& other) const {
+        return (offset <= other.offset && offset + length >= other.offset + 
other.length);
+    }
+};
+
+/// A byte slice with buffer, offset and length.
+struct PAIMON_EXPORT ByteSlice {
+    std::shared_ptr<Bytes> buffer = nullptr;
+    uint64_t offset = 0;
+    uint64_t length = 0;
+};
+
+/// A read cache designed to hide IO latencies when reading.
+/// Prefetching strategy: When a range is read, the cache will prefetch up to
+/// `pre_buffer_range_count` additional adjacent ranges ahead of the requested 
offset. This helps
+/// hide I/O latency for sequential access. Example: If you read range [0, 
100), and
+/// pre_buffer_range_count=2, the next two configured ranges will also be 
prefetched.
+///
+/// Eviction policy: The cache uses a simple FIFO eviction policy based on 
total cached byte size.
+/// When adding new ranges would exceed `buffer_size_limit`, the oldest cached 
ranges are evicted
+/// first until there is enough space for the new data.
+class PAIMON_EXPORT ReadAheadCache {
+ public:
+    /// Construct a read cache with given options
+    ReadAheadCache(const std::shared_ptr<InputStream>& stream, const 
CacheConfig& config,
+                   const std::shared_ptr<MemoryPool>& memory_pool);
+    ~ReadAheadCache();
+
+    /// Initialize the cache with given byte ranges to be cached.
+    /// @param ranges The byte ranges to be cached.
+    /// @return Status of the operation.
+    /// @note This method must be called before any Read() calls. Ranges will 
be coalesced based
+    /// on the cache configuration.
+    Status Init(std::vector<ByteRange>&& ranges);
+
+    /// Read a range previously provided to Init().
+    /// @param range The byte range to read.
+    /// @return The byte slice containing the requested data. If the data is 
not yet cached
+    /// (cache miss), the returned `ByteSlice` will have a null buffer 
(`buffer == nullptr`)
+    Result<ByteSlice> Read(const ByteRange& range);
+
+    /// Reset the cache to its initial state, clearing all cached data and 
configuration.
+    ///
+    /// This method waits for all ongoing asynchronous read operations to 
complete,
+    /// clears all cached entries, and resets the internal state so that 
Init() can be called again.
+    /// After calling Reset, the cache can be safely re-initialized with new 
ranges.
+    void Reset();
+
+ private:
+    class Impl;
+    std::unique_ptr<Impl> impl_;
+};
+
+}  // namespace paimon
diff --git a/src/paimon/common/utils/object_utils.h 
b/src/paimon/common/utils/object_utils.h
new file mode 100644
index 0000000..66b6c54
--- /dev/null
+++ b/src/paimon/common/utils/object_utils.h
@@ -0,0 +1,153 @@
+/*
+ * 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 <functional>
+#include <map>
+#include <set>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+#include "paimon/result.h"
+#include "paimon/traits.h"
+namespace paimon {
+/// Utils for objects.
+class ObjectUtils {
+ public:
+    ObjectUtils() = delete;
+    ~ObjectUtils() = delete;
+
+    template <typename T, typename U>
+    static bool ContainsAll(const T& all, const U& contains) {
+        using V = typename T::value_type;
+        std::unordered_set<V> all_set(all.begin(), all.end());
+        for (const auto& element : contains) {
+            if (all_set.find(element) == all_set.end()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    template <typename T>
+    static bool Contains(const std::vector<T>& all, const T& target) {
+        for (const auto& v : all) {
+            if (v == target) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    template <typename T>
+    static std::unordered_set<T> DuplicateItems(const std::vector<T>& entries) 
{
+        std::unordered_map<T, int32_t> counts;
+        for (const auto& entry : entries) {
+            counts[entry]++;
+        }
+
+        std::unordered_set<T> duplicates;
+        for (const auto& entry : counts) {
+            if (entry.second > 1) {
+                duplicates.insert(entry.first);
+            }
+        }
+        return duplicates;
+    }
+
+    template <typename T>
+    static bool TEST_Equal(const std::vector<T>& lhs, const std::vector<T>& 
rhs) {
+        if (lhs.size() != rhs.size()) {
+            return false;
+        }
+        for (size_t i = 0; i < lhs.size(); i++) {
+            if constexpr (is_pointer<T>::value) {
+                if (!lhs[i]->TEST_Equal(*rhs[i])) {
+                    return false;
+                }
+            } else {
+                if (!lhs[i].TEST_Equal(rhs[i])) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    template <typename T>
+    static bool Equal(const std::vector<T>& lhs, const std::vector<T>& rhs) {
+        if (lhs.size() != rhs.size()) {
+            return false;
+        }
+        for (size_t i = 0; i < lhs.size(); i++) {
+            if constexpr (is_pointer<T>::value) {
+                if (!(*(lhs[i]) == *(rhs[i]))) {
+                    return false;
+                }
+            } else {
+                if (!(lhs[i] == rhs[i])) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    // used to create an identifier to index map, param func is used to 
convert T to identifier I
+    // e.g., T = DataField, I = std::string, return std::map<std::string, 
int32_t>
+    template <typename T, typename Func>
+    static auto CreateIdentifierToIndexMap(const std::vector<T>& vec, const 
Func& func)
+        -> std::map<decltype(func(std::declval<T>())), int32_t> {
+        using KeyType = decltype(func(std::declval<T>()));
+        std::map<KeyType, int32_t> result;
+        for (int32_t i = 0; i < static_cast<int32_t>(vec.size()); ++i) {
+            result[func(vec[i])] = i;
+        }
+        return result;
+    }
+
+    template <typename T>
+    static std::map<T, int32_t> CreateIdentifierToIndexMap(const 
std::vector<T>& vec) {
+        std::map<T, int32_t> index_map;
+        for (int32_t i = 0; i < static_cast<int32_t>(vec.size()); i++) {
+            index_map[vec[i]] = i;
+        }
+        return index_map;
+    }
+
+    /// Precondition: U and T must be pointer and U::value can move to T::value
+    template <typename T, typename U>
+    static std::vector<T> MoveVector(std::vector<U>&& input) {
+        static_assert(is_pointer<U>::value && is_pointer<T>::value &&
+                          std::is_convertible_v<value_type_traits_t<U>, 
value_type_traits_t<T>>,
+                      "U and T must be pointer and U::value can move to 
T::value");
+        std::vector<T> result;
+        result.reserve(input.size());
+        for (auto& item : input) {
+            result.push_back(std::move(item));
+        }
+        input.clear();
+        return result;
+    }
+};
+}  // namespace paimon
diff --git a/src/paimon/common/utils/object_utils_test.cpp 
b/src/paimon/common/utils/object_utils_test.cpp
new file mode 100644
index 0000000..6fdee78
--- /dev/null
+++ b/src/paimon/common/utils/object_utils_test.cpp
@@ -0,0 +1,116 @@
+/*
+ * 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/common/utils/object_utils.h"
+
+#include <string>
+
+#include "gtest/gtest.h"
+#include "paimon/common/types/data_field.h"
+
+namespace paimon::test {
+
+TEST(ObjectUtilsTest, TestContainsAll) {
+    std::vector<int32_t> all = {0, 1, 2, 3};
+    std::vector<int32_t> contains1 = {1, 2};
+    std::vector<int32_t> contains2 = {1, 10};
+    ASSERT_TRUE(ObjectUtils::ContainsAll(all, contains1));
+    ASSERT_FALSE(ObjectUtils::ContainsAll(all, contains2));
+}
+
+TEST(ObjectUtilsTest, TestContains) {
+    std::vector<int32_t> all = {0, 2, 4, 6};
+    ASSERT_TRUE(ObjectUtils::Contains(all, 4));
+    ASSERT_FALSE(ObjectUtils::Contains(all, 8));
+}
+
+TEST(ObjectUtilsTest, TestDuplicateItems) {
+    std::vector<std::string> entries = {"apple",  "banana", "cherry", "apple",
+                                        "banana", "date",   "fig",    
"cherry"};
+    std::unordered_set<std::string> expected = {"banana", "apple", "cherry"};
+    ASSERT_EQ(ObjectUtils::DuplicateItems(entries), expected);
+}
+
+TEST(ObjectUtilsTest, TestCreateIdentifierToIndexMap) {
+    std::vector<DataField> fields = {
+        DataField(0, arrow::field("f11", arrow::boolean())),
+        DataField(1, arrow::field("f10", arrow::int8())),
+        DataField(2, arrow::field("f9", arrow::int16())),
+        DataField(3, arrow::field("f8", arrow::int32())),
+        DataField(4, arrow::field("f7", arrow::int64())),
+        DataField(5, arrow::field("f6", arrow::float32())),
+        DataField(6, arrow::field("f5", arrow::float64())),
+        DataField(7, arrow::field("f4", arrow::utf8())),
+        DataField(8, arrow::field("f3", arrow::binary())),
+        DataField(9, arrow::field("f2", 
arrow::timestamp(arrow::TimeUnit::NANO))),
+        DataField(10, arrow::field("f1", arrow::date32())),
+        DataField(11, arrow::field("f0", arrow::decimal128(2, 2))),
+    };
+
+    {
+        auto result_map = ObjectUtils::CreateIdentifierToIndexMap(
+            fields, [](const DataField& field) { return field.Name(); });
+        ASSERT_EQ(result_map.size(), 12);
+        std::map<std::string, int32_t> expected_map = {
+            {"f0", 11}, {"f1", 10}, {"f2", 9}, {"f3", 8}, {"f4", 7},  {"f5", 
6},
+            {"f6", 5},  {"f7", 4},  {"f8", 3}, {"f9", 2}, {"f10", 1}, {"f11", 
0},
+        };
+        ASSERT_EQ(expected_map, result_map);
+    }
+    {
+        auto result_map = ObjectUtils::CreateIdentifierToIndexMap(
+            std::vector<DataField>({}), [](const DataField& field) { return 
field.Name(); });
+        ASSERT_TRUE(result_map.empty());
+    }
+    {
+        std::vector<std::string> vec = {"f0", "f1", "f2"};
+        std::map<std::string, int32_t> result_map = 
ObjectUtils::CreateIdentifierToIndexMap(vec);
+        std::map<std::string, int32_t> expected_map = {
+            {"f0", 0},
+            {"f1", 1},
+            {"f2", 2},
+        };
+        ASSERT_EQ(expected_map, result_map);
+    }
+}
+TEST(ObjectUtilsTest, TestMoveVector) {
+    struct Base {
+        virtual ~Base() = default;
+        virtual int32_t Value() const = 0;
+    };
+
+    struct Derived : Base {
+        explicit Derived(int32_t v) : val(v) {}
+        int32_t Value() const override {
+            return val;
+        }
+        int32_t val;
+    };
+    std::vector<std::unique_ptr<Derived>> derived_vec;
+    derived_vec.push_back(std::make_unique<Derived>(10));
+    derived_vec.push_back(std::make_unique<Derived>(20));
+    derived_vec.push_back(std::make_unique<Derived>(30));
+
+    auto base_vec = 
paimon::ObjectUtils::MoveVector<std::unique_ptr<Base>>(std::move(derived_vec));
+
+    ASSERT_EQ(base_vec[0]->Value(), 10);
+    ASSERT_EQ(base_vec[1]->Value(), 20);
+    ASSERT_EQ(base_vec[2]->Value(), 30);
+}
+}  // namespace paimon::test
diff --git a/src/paimon/common/utils/read_ahead_cache.cpp 
b/src/paimon/common/utils/read_ahead_cache.cpp
new file mode 100644
index 0000000..aa7e5cb
--- /dev/null
+++ b/src/paimon/common/utils/read_ahead_cache.cpp
@@ -0,0 +1,258 @@
+/*
+ * 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.
+ */
+
+// Adapted from Apache ORC
+// https://github.com/apache/orc/blob/main/c%2B%2B/src/io/Cache.cc
+
+#include "paimon/utils/read_ahead_cache.h"
+
+#include <algorithm>
+#include <cassert>
+#include <future>
+#include <shared_mutex>
+
+#include "paimon/common/utils/byte_range_combiner.h"
+
+namespace paimon {
+
+struct RangeCacheEntry {
+    ByteRange range;
+    std::shared_ptr<Bytes> buffer;
+    std::shared_future<Status> future;  // use shared_future in case of 
multiple get calls
+
+    RangeCacheEntry() = default;
+    RangeCacheEntry(const ByteRange& range, std::shared_ptr<Bytes> buffer,
+                    std::future<Status> future)
+        : range(range), buffer(std::move(buffer)), 
future(std::move(future).share()) {}
+
+    friend bool operator<(const RangeCacheEntry& left, const RangeCacheEntry& 
right) {
+        return left.range.offset < right.range.offset;
+    }
+};
+
+CacheConfig::CacheConfig(uint64_t buffer_size_limit, uint64_t range_size_limit,
+                         uint64_t hole_size_limit, uint64_t pre_buffer_limit)
+    : buffer_size_limit_(buffer_size_limit),
+      range_size_limit_(range_size_limit),
+      hole_size_limit_(hole_size_limit),
+      pre_buffer_limit_(pre_buffer_limit) {}
+
+CacheConfig::CacheConfig()
+    : CacheConfig(/*buffer_size_limit=*/512 * 1024 * 1024,
+                  /*range_size_limit=*/16 * 1024 * 1024,
+                  /*hole_size_limit=*/8 * 1024,
+                  /*pre_buffer_limit=*/128 * 1024 * 1024) {}
+
+class ReadAheadCache::Impl {
+ public:
+    Impl(const std::shared_ptr<InputStream>& stream, const CacheConfig& config,
+         const std::shared_ptr<MemoryPool>& memory_pool);
+    ~Impl();
+
+    Status Init(std::vector<ByteRange>&& ranges);
+    Result<ByteSlice> Read(const ByteRange& range);
+    void Reset();
+
+ private:
+    std::vector<RangeCacheEntry> MakeCacheEntries(const 
std::vector<ByteRange>& ranges) const;
+    void PreBuffer(uint64_t offset);
+
+    /// Cache the given ranges in the background.
+    ///
+    /// The caller must ensure that the ranges do not overlap with each other,
+    /// nor with previously cached ranges.  Otherwise, behaviour will be 
undefined.
+    void Cache(std::vector<ByteRange> ranges);
+
+    std::shared_ptr<InputStream> stream_;
+    CacheConfig config_;
+    // Ordered by offset (so as to find a matching region by binary search)
+    std::vector<RangeCacheEntry> entries_;
+    std::shared_ptr<MemoryPool> memory_pool_;
+    std::shared_mutex rw_mutex_;
+    std::vector<std::atomic<bool>> is_cached_;
+    std::vector<ByteRange> pending_ranges_;
+    bool is_initialized_ = false;
+};
+
+void ReadAheadCache::Impl::Cache(std::vector<ByteRange> ranges) {
+    std::sort(ranges.begin(), ranges.end(),
+              [](const ByteRange& a, const ByteRange& b) { return a.offset < 
b.offset; });
+    std::vector<RangeCacheEntry> new_entries = MakeCacheEntries(ranges);
+    // Add new entries, themselves ordered by offset
+    std::unique_lock<std::shared_mutex> lock(rw_mutex_);
+    if (entries_.size() > 0) {
+        size_t new_entries_size = 0;
+        for (const auto& e : new_entries) {
+            new_entries_size += e.range.length;
+        }
+
+        size_t total_size = 0;
+        for (const auto& e : entries_) {
+            total_size += e.range.length;
+        }
+        size_t limit = config_.GetBufferSizeLimit();
+        while (!entries_.empty() && total_size + new_entries_size > limit) {
+            auto iter = entries_.begin();
+            total_size -= entries_.front().range.length;
+            entries_.erase(iter);
+        }
+
+        std::vector<RangeCacheEntry> merged(entries_.size() + 
new_entries.size());
+        std::merge(entries_.begin(), entries_.end(), new_entries.begin(), 
new_entries.end(),
+                   merged.begin());
+        entries_ = std::move(merged);
+    } else {
+        entries_ = std::move(new_entries);
+    }
+}
+
+Status ReadAheadCache::Impl::Init(std::vector<ByteRange>&& ranges) {
+    if (is_initialized_) {
+        return Status::Invalid("Cache has already been initialized");
+    }
+    if (config_.GetRangeSizeLimit() > 
static_cast<uint64_t>(std::numeric_limits<uint32_t>::max())) {
+        return Status::Invalid("CacheConfig range_size_limit exceeds uint32_t 
max");
+    }
+
+    PAIMON_ASSIGN_OR_RAISE(
+        std::vector<ByteRange> pending_ranges,
+        ByteRangeCombiner::CoalesceByteRanges(std::move(ranges), 
config_.GetHoleSizeLimit(),
+                                              config_.GetRangeSizeLimit()));
+    for (const auto& pending_range : pending_ranges) {
+        if (pending_range.length > 
static_cast<uint64_t>(std::numeric_limits<uint32_t>::max())) {
+            return Status::Invalid("range length should not be larger than 
uint32_t max");
+        }
+    }
+    pending_ranges_ = pending_ranges;
+    is_cached_ = std::vector<std::atomic<bool>>(pending_ranges_.size());
+    for (auto& is_cached : is_cached_) {
+        is_cached.store(false);
+    }
+    is_initialized_ = true;
+    return Status::OK();
+}
+
+void ReadAheadCache::Impl::PreBuffer(uint64_t offset) {
+    auto it = std::lower_bound(pending_ranges_.begin(), pending_ranges_.end(), 
offset,
+                               [](const ByteRange& range, uint64_t offset) {
+                                   return range.offset + range.length <= 
offset;
+                               });
+    if (it == pending_ranges_.end() || it->offset > offset) {
+        return;
+    }
+
+    size_t start_idx = std::distance(pending_ranges_.begin(), it);
+    std::vector<ByteRange> ranges;
+    size_t total_bytes = 0;
+    for (size_t i = start_idx; i < pending_ranges_.size(); ++i) {
+        size_t range_size = pending_ranges_[i].length;
+        total_bytes += range_size;
+        if (total_bytes > config_.GetPreBufferLimit()) {
+            break;
+        }
+        if (is_cached_[i].exchange(true)) {
+            continue;
+        }
+        ranges.emplace_back(pending_ranges_[i]);
+    }
+
+    if (!ranges.empty()) {
+        Cache(std::move(ranges));
+    }
+}
+
+ReadAheadCache::Impl::Impl(const std::shared_ptr<InputStream>& stream, const 
CacheConfig& config,
+                           const std::shared_ptr<MemoryPool>& memory_pool)
+    : stream_(stream), config_(config), memory_pool_(memory_pool) {}
+
+ReadAheadCache::Impl::~Impl() {
+    std::unique_lock<std::shared_mutex> lock(rw_mutex_);
+    for (auto& entry : entries_) {
+        entry.future.wait();
+    }
+}
+
+void ReadAheadCache::Impl::Reset() {
+    std::unique_lock<std::shared_mutex> lock(rw_mutex_);
+    for (auto& entry : entries_) {
+        entry.future.wait();
+    }
+    entries_.clear();
+    is_cached_.clear();
+    pending_ranges_.clear();
+    is_initialized_ = false;
+}
+
+Result<ByteSlice> ReadAheadCache::Impl::Read(const ByteRange& range) {
+    if (range.length == 0) {
+        return ByteSlice{std::make_shared<Bytes>(0, memory_pool_.get()), 0, 0};
+    }
+    PreBuffer(range.offset);
+    ByteSlice result{};
+    {
+        std::shared_lock<std::shared_mutex> lock(rw_mutex_);
+        auto it = std::lower_bound(entries_.begin(), entries_.end(), 
range.offset,
+                                   [](const RangeCacheEntry& e, uint64_t 
offset) {
+                                       return e.range.offset + e.range.length 
<= offset;
+                                   });
+        if (it != entries_.end() && it->range.Contains(range)) {
+            PAIMON_RETURN_NOT_OK(it->future.get());
+            result = ByteSlice{it->buffer, range.offset - it->range.offset, 
range.length};
+            return result;
+        }
+    }
+    return result;
+}
+
+std::vector<RangeCacheEntry> ReadAheadCache::Impl::MakeCacheEntries(
+    const std::vector<ByteRange>& ranges) const {
+    std::vector<RangeCacheEntry> new_entries;
+    new_entries.reserve(ranges.size());
+    for (const auto& range : ranges) {
+        auto promise = std::make_shared<std::promise<Status>>();
+        auto future = promise->get_future();
+        auto buffer = std::make_shared<Bytes>(range.length, 
memory_pool_.get());
+        stream_->ReadAsync(
+            buffer->data(), static_cast<uint32_t>(buffer->size()), 
range.offset,
+            [promise, buffer](Status status) mutable { 
promise->set_value(status); });
+        new_entries.emplace_back(range, std::move(buffer), std::move(future));
+    }
+    return new_entries;
+}
+
+ReadAheadCache::ReadAheadCache(const std::shared_ptr<InputStream>& stream,
+                               const CacheConfig& config,
+                               const std::shared_ptr<MemoryPool>& memory_pool)
+    : impl_(std::make_unique<Impl>(stream, config, memory_pool)) {}
+
+ReadAheadCache::~ReadAheadCache() = default;
+
+Status ReadAheadCache::Init(std::vector<ByteRange>&& ranges) {
+    return impl_->Init(std::move(ranges));
+}
+
+Result<ByteSlice> ReadAheadCache::Read(const ByteRange& range) {
+    return impl_->Read(range);
+}
+
+void ReadAheadCache::Reset() {
+    return impl_->Reset();
+}
+
+}  // namespace paimon
diff --git a/src/paimon/common/utils/read_ahead_cache_test.cpp 
b/src/paimon/common/utils/read_ahead_cache_test.cpp
new file mode 100644
index 0000000..6762542
--- /dev/null
+++ b/src/paimon/common/utils/read_ahead_cache_test.cpp
@@ -0,0 +1,159 @@
+/*
+ * 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/utils/read_ahead_cache.h"
+
+#include <fstream>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/fs/file_system_factory.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+
+// Helper to create a test file, write content, and return a ready 
ReadAheadCache.
+struct TestCacheEnv {
+    std::string path;
+    std::shared_ptr<paimon::ReadAheadCache> cache;
+    std::shared_ptr<paimon::MemoryPool> pool;
+};
+
+TestCacheEnv CreateTestFileAndCache(const std::string& filename, const 
std::string& content,
+                                    const paimon::CacheConfig& config,
+                                    std::vector<paimon::ByteRange> ranges) {
+    auto dir = UniqueTestDirectory::Create();
+    EXPECT_TRUE(dir);
+    std::string path = dir->Str() + "/" + filename;
+    std::ofstream file(path, std::ios::binary);
+    EXPECT_TRUE(file.is_open());
+    file.write(content.data(), content.size());
+    EXPECT_FALSE(file.fail());
+    file.close();
+
+    auto fs_result = FileSystemFactory::Get("local", path, {});
+    EXPECT_TRUE(fs_result.ok());
+    auto fs = std::move(fs_result).value();
+    auto in_result = fs->Open(path);
+    EXPECT_TRUE(in_result.ok());
+    auto in = std::move(in_result).value();
+
+    auto pool = GetDefaultPool();
+    auto cache = std::make_shared<ReadAheadCache>(std::move(in), config, pool);
+    EXPECT_OK(cache->Init(std::move(ranges)));
+    return {path, cache, pool};
+}
+
+TEST(TestReadAheadCache, TestBasics) {
+    CacheConfig config(/*buffer_size_limit=*/256 * 1024 * 1024, 
/*range_size_limit=*/10,
+                       /*hole_size_limit=*/2, /*pre_buffer_limit=*/128 * 1024 
* 1024);
+    std::string content = "abcdefghijklmnopqrstuvwxyz";
+    auto env = CreateTestFileAndCache(
+        "data_file", content, config,
+        {{1, 2}, {3, 2}, {8, 2}, {10, 4}, {14, 0}, {15, 4}, {20, 2}, {25, 0}});
+    auto& cache = *env.cache;
+
+    auto assert_slice_equal = [](const ByteSlice& slice, const std::string& 
expected) {
+        ASSERT_TRUE(slice.buffer) << expected;
+        EXPECT_EQ(expected, std::string_view(slice.buffer->data() + 
slice.offset, slice.length));
+    };
+
+    ByteSlice slice;
+
+    ASSERT_OK_AND_ASSIGN(slice, cache.Read({20, 2}));
+    assert_slice_equal(slice, "uv");
+
+    ASSERT_OK_AND_ASSIGN(slice, cache.Read({1, 2}));
+    assert_slice_equal(slice, "bc");
+
+    ASSERT_OK_AND_ASSIGN(slice, cache.Read({3, 2}));
+    assert_slice_equal(slice, "de");
+
+    ASSERT_OK_AND_ASSIGN(slice, cache.Read({8, 2}));
+    assert_slice_equal(slice, "ij");
+
+    ASSERT_OK_AND_ASSIGN(slice, cache.Read({10, 4}));
+    assert_slice_equal(slice, "klmn");
+
+    ASSERT_OK_AND_ASSIGN(slice, cache.Read({15, 4}));
+    assert_slice_equal(slice, "pqrs");
+
+    ASSERT_OK_AND_ASSIGN(slice, cache.Read({19, 3}));
+    assert_slice_equal(slice, "tuv");
+
+    // Zero-sized
+    ASSERT_OK_AND_ASSIGN(slice, cache.Read({14, 0}));
+    assert_slice_equal(slice, "");
+    ASSERT_OK_AND_ASSIGN(slice, cache.Read({25, 0}));
+    assert_slice_equal(slice, "");
+
+    // Non-cached ranges
+
+    ASSERT_FALSE(cache.Read({20, 3}).value().buffer);
+    ASSERT_FALSE(cache.Read({0, 3}).value().buffer);
+    ASSERT_FALSE(cache.Read({25, 2}).value().buffer);
+}
+
+// Test repeated reads to the same range to ensure cache reuse.
+TEST(TestReadAheadCache, TestRepeatedReadCacheReuse) {
+    CacheConfig config(/*buffer_size_limit=*/64, /*range_size_limit=*/10,
+                       /*hole_size_limit=*/2, /*pre_buffer_limit=*/64);
+    std::string content = "abcdefghijklmnopqrstuvwxyz";
+    auto env = CreateTestFileAndCache("data_file", content, config, {{0, 5}, 
{7, 5}});
+    auto& cache = *env.cache;
+
+    ByteSlice slice;
+    ASSERT_OK_AND_ASSIGN(slice, cache.Read({0, 5}));
+    ASSERT_TRUE(slice.buffer);
+    std::string first_read(slice.buffer->data() + slice.offset, slice.length);
+    ASSERT_EQ(first_read, "abcde");
+
+    ASSERT_OK_AND_ASSIGN(slice, cache.Read({0, 5}));
+    ASSERT_TRUE(slice.buffer);
+    std::string second_read(slice.buffer->data() + slice.offset, slice.length);
+    ASSERT_EQ(second_read, "abcde");
+}
+
+// Test cache eviction when buffer size is limited.
+TEST(TestReadAheadCache, TestCacheEviction) {
+    CacheConfig config(/*buffer_size_limit=*/10, /*range_size_limit=*/5,
+                       /*hole_size_limit=*/2, /*pre_buffer_limit=*/10);
+    std::string content = "abcdefghijklmnopqrstuvwxyz";
+    auto env = CreateTestFileAndCache("data_file", content, config, {{0, 5}, 
{8, 5}, {16, 5}});
+    auto& cache = *env.cache;
+
+    ByteSlice slice;
+    ASSERT_OK_AND_ASSIGN(slice, cache.Read({0, 5}));
+    ASSERT_TRUE(slice.buffer);
+    std::string first_read(slice.buffer->data() + slice.offset, slice.length);
+    ASSERT_EQ(first_read, "abcde");
+
+    // Reading another range should evict the first one due to buffer size 
limit
+    ASSERT_OK_AND_ASSIGN(slice, cache.Read({8, 5}));
+    ASSERT_TRUE(slice.buffer);
+    std::string second_read(slice.buffer->data() + slice.offset, slice.length);
+    ASSERT_EQ(second_read, "ijklm");
+
+    // The first range should now be a cache miss (buffer is nullptr)
+    auto miss = cache.Read({0, 5});
+    ASSERT_FALSE(miss.value().buffer);
+}
+
+}  // namespace paimon::test
diff --git a/src/paimon/common/utils/serialization_utils.h 
b/src/paimon/common/utils/serialization_utils.h
new file mode 100644
index 0000000..4718ea9
--- /dev/null
+++ b/src/paimon/common/utils/serialization_utils.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 <cstdint>
+#include <cstring>
+#include <memory>
+#include <utility>
+
+#include "fmt/format.h"
+#include "paimon/common/data/binary_row.h"
+#include "paimon/common/io/memory_segment_output_stream.h"
+#include "paimon/common/memory/memory_segment.h"
+#include "paimon/common/memory/memory_segment_utils.h"
+#include "paimon/common/utils/math.h"
+#include "paimon/io/byte_order.h"
+#include "paimon/io/data_input_stream.h"
+#include "paimon/macros.h"
+#include "paimon/memory/bytes.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+#include "paimon/type_fwd.h"
+
+namespace paimon {
+
+/// Utils for serialization.
+class SerializationUtils {
+ public:
+    SerializationUtils() = delete;
+    ~SerializationUtils() = delete;
+
+    /// Serialize `BinaryRow`, the difference between this and 
`BinaryRowSerializer` is
+    /// that arity is also serialized here, so the deserialization is 
schemaless.
+    static std::shared_ptr<Bytes> SerializeBinaryRow(const BinaryRow& row, 
MemoryPool* pool) {
+        int32_t size_in_row = row.GetSizeInBytes();
+        auto bytes = Bytes::AllocateBytes(size_in_row + sizeof(int32_t), pool);
+        int32_t arity = row.GetFieldCount();
+        if (SystemByteOrder() == ByteOrder::PAIMON_LITTLE_ENDIAN) {
+            arity = EndianSwapValue(arity);
+        }
+        memcpy(bytes->data(), &arity, sizeof(int32_t));
+        MemorySegmentUtils::CopyToBytes({row.GetSegment()}, row.GetOffset(), 
bytes.get(),
+                                        sizeof(int32_t), size_in_row);
+        return bytes;
+    }
+
+    static Status SerializeBinaryRow(const BinaryRow& row, 
MemorySegmentOutputStream* out) {
+        if (row.GetSizeInBytes() < 0) {
+            return Status::Invalid(
+                fmt::format("bytes size {} is less than 0", 
row.GetSizeInBytes()));
+        }
+        out->WriteValue<int32_t>(4 + row.GetSizeInBytes());
+        out->WriteValue<int32_t>(row.GetFieldCount());
+        return MemorySegmentUtils::CopyToStream({row.GetSegment()}, 
row.GetOffset(),
+                                                row.GetSizeInBytes(), out);
+    }
+
+    /// Schemaless deserialization for `BinaryRow`.
+    static Result<BinaryRow> DeserializeBinaryRow(const 
std::shared_ptr<Bytes>& bytes) {
+        if (PAIMON_UNLIKELY(bytes->size() < 4)) {
+            return Status::Invalid(fmt::format("bytes size {} is less than 4", 
bytes->size()));
+        }
+        int32_t arity = *(reinterpret_cast<int32_t*>(bytes->data()));
+        if (SystemByteOrder() == ByteOrder::PAIMON_LITTLE_ENDIAN) {
+            arity = EndianSwapValue(arity);
+        }
+        if (PAIMON_UNLIKELY(arity < 0)) {
+            return Status::Invalid("arity is less than 0");
+        }
+        BinaryRow row(arity);
+        row.PointTo(MemorySegment::Wrap(bytes), 4, bytes->size() - 4);
+        return row;
+    }
+
+    /// Schemaless deserialization for `BinaryRow` from a `DataInputStream`.
+    static Result<BinaryRow> DeserializeBinaryRow(DataInputStream* input, 
MemoryPool* pool) {
+        int32_t read_length = -1;
+        PAIMON_ASSIGN_OR_RAISE(read_length, input->ReadValue<int32_t>());
+        std::shared_ptr<Bytes> bytes = Bytes::AllocateBytes(read_length, pool);
+        PAIMON_RETURN_NOT_OK(input->ReadBytes(bytes.get()));
+        return DeserializeBinaryRow(bytes);
+    }
+};
+
+}  // namespace paimon
diff --git a/src/paimon/common/utils/serialization_utils_test.cpp 
b/src/paimon/common/utils/serialization_utils_test.cpp
new file mode 100644
index 0000000..5e612ff
--- /dev/null
+++ b/src/paimon/common/utils/serialization_utils_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/common/utils/serialization_utils.h"
+
+#include "gtest/gtest.h"
+
+namespace paimon::test {
+
+class SerializationUtilsTest : public ::testing::Test {
+ public:
+    void SetUp() override {}
+    void TearDown() override {}
+};
+
+TEST_F(SerializationUtilsTest, TestSerializeBinaryRow) {
+    std::shared_ptr<MemoryPool> memory_pool = GetDefaultPool();
+    MemorySegmentOutputStream 
out(MemorySegmentOutputStream::DEFAULT_SEGMENT_SIZE, memory_pool);
+    BinaryRow row = BinaryRow::EmptyRow();
+    std::shared_ptr<Bytes> bytes = SerializationUtils::SerializeBinaryRow(row, 
memory_pool.get());
+    ASSERT_TRUE(bytes);
+}
+
+}  // namespace paimon::test
diff --git a/src/paimon/common/utils/stream_utils.h 
b/src/paimon/common/utils/stream_utils.h
new file mode 100644
index 0000000..5537c83
--- /dev/null
+++ b/src/paimon/common/utils/stream_utils.h
@@ -0,0 +1,102 @@
+/*
+ * 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 <future>
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "fmt/format.h"
+#include "paimon/common/executor/future.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/macros.h"
+#include "paimon/memory/bytes.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace paimon {
+
+class StreamUtils {
+ public:
+    StreamUtils() = default;
+    ~StreamUtils() = default;
+
+    static Result<PAIMON_UNIQUE_PTR<Bytes>> 
ReadFully(std::unique_ptr<InputStream> input_stream,
+                                                      const 
std::shared_ptr<MemoryPool>& pool) {
+        PAIMON_RETURN_NOT_OK(input_stream->Seek(0, FS_SEEK_SET));
+        PAIMON_ASSIGN_OR_RAISE(uint64_t file_length, input_stream->Length());
+        PAIMON_UNIQUE_PTR<Bytes> content = Bytes::AllocateBytes(file_length, 
pool.get());
+        PAIMON_ASSIGN_OR_RAISE(int32_t actual_read_len,
+                               input_stream->Read(content->data(), 
content->size()));
+        if (static_cast<uint32_t>(actual_read_len) != file_length) {
+            return Status::Invalid("actual read length {}, not match with 
expect length {}",
+                                   actual_read_len, file_length);
+        }
+        return content;
+    }
+
+    static Result<PAIMON_UNIQUE_PTR<Bytes>> ReadAsyncFully(
+        std::unique_ptr<InputStream> input_stream, const 
std::shared_ptr<MemoryPool>& pool) {
+        PAIMON_ASSIGN_OR_RAISE(uint64_t file_length, input_stream->Length());
+        PAIMON_UNIQUE_PTR<Bytes> content = Bytes::AllocateBytes(file_length, 
pool.get());
+        PAIMON_RETURN_NOT_OK(ReadAsyncFully(std::move(input_stream), 
content->data()));
+        return content;
+    }
+
+    static Status ReadAsyncFully(std::unique_ptr<InputStream> input_stream, 
char* content) {
+        PAIMON_RETURN_NOT_OK(input_stream->Seek(0, FS_SEEK_SET));
+        PAIMON_ASSIGN_OR_RAISE(uint64_t file_length, input_stream->Length());
+
+        uint64_t read_offset = 0;
+        uint32_t read_len = std::min(file_length, kDefaultReadChunkSize);
+        std::vector<std::future<Status>> futures;
+        futures.reserve(file_length / kDefaultReadChunkSize + 1);
+        while (read_len > 0) {
+            auto promise = std::make_shared<std::promise<Status>>();
+            futures.push_back(promise->get_future());
+            input_stream->ReadAsync(content, read_len, read_offset,
+                                    [promise](Status status) { 
promise->set_value(status); });
+            read_offset += read_len;
+            content += read_len;
+            read_len = std::min(file_length - read_offset, 
kDefaultReadChunkSize);
+        }
+        for (const auto& status : CollectAll(futures)) {
+            if (!status.ok()) {
+                return status;
+            }
+        }
+        if (PAIMON_UNLIKELY(read_offset != file_length)) {
+            return Status::IOError(
+                fmt::format("Total read length {} does not match expected 
length {}.", read_offset,
+                            file_length));
+        }
+        return Status::OK();
+    }
+
+ private:
+    static constexpr uint64_t kDefaultReadChunkSize = 1024 * 1024;
+};
+
+}  // namespace paimon
diff --git a/src/paimon/common/utils/stream_utils_test.cpp 
b/src/paimon/common/utils/stream_utils_test.cpp
new file mode 100644
index 0000000..9786a3e
--- /dev/null
+++ b/src/paimon/common/utils/stream_utils_test.cpp
@@ -0,0 +1,146 @@
+/*
+ * 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/common/utils/stream_utils.h"
+
+#include <cstring>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "paimon/fs/local/local_file_system.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon {
+
+class StreamUtilsTest : public ::testing::Test {
+ public:
+    void SetUp() override {
+        pool_ = GetDefaultPool();
+        dir_ = paimon::test::UniqueTestDirectory::Create();
+        ASSERT_TRUE(dir_);
+        file_system_ = std::make_shared<LocalFileSystem>();
+    }
+
+    void TearDown() override {
+        dir_.reset();
+        file_system_.reset();
+        pool_.reset();
+    }
+
+    std::string CreateTestFile(const std::string& content) {
+        std::string file_path = dir_->Str() + "/test_file.txt";
+        EXPECT_OK_AND_ASSIGN(auto output_stream, 
file_system_->Create(file_path, true));
+        EXPECT_OK_AND_ASSIGN(int32_t length, 
output_stream->Write(content.data(), content.size()));
+        EXPECT_EQ(length, static_cast<int32_t>(content.size()));
+        EXPECT_OK(output_stream->Close());
+        return file_path;
+    }
+
+ private:
+    std::shared_ptr<MemoryPool> pool_;
+    std::unique_ptr<paimon::test::UniqueTestDirectory> dir_;
+    std::shared_ptr<FileSystem> file_system_;
+};
+
+TEST_F(StreamUtilsTest, ReadBasicTest) {
+    std::string test_content = "Hello, World! This is a test file for 
StreamUtils.";
+    std::string file_path = CreateTestFile(test_content);
+
+    {
+        // ReadFully
+        ASSERT_OK_AND_ASSIGN(auto input_stream, file_system_->Open(file_path));
+        ASSERT_OK(input_stream->Seek(10, FS_SEEK_SET));  // ReadFully ignore 
any seek
+        ASSERT_OK_AND_ASSIGN(auto result, 
StreamUtils::ReadFully(std::move(input_stream), pool_));
+        ASSERT_EQ(result->size(), test_content.size());
+        ASSERT_EQ(std::string(result->data(), result->size()), test_content);
+    }
+    {
+        // ReadAsyncFully
+        ASSERT_OK_AND_ASSIGN(auto input_stream, file_system_->Open(file_path));
+        ASSERT_OK(input_stream->Seek(10, FS_SEEK_SET));  // ReadAsyncFully 
ignore any seek
+        ASSERT_OK_AND_ASSIGN(auto result,
+                             
StreamUtils::ReadAsyncFully(std::move(input_stream), pool_));
+        ASSERT_EQ(result->size(), test_content.size());
+        ASSERT_EQ(std::string(result->data(), result->size()), test_content);
+    }
+    {
+        // ReadAsyncFully with buffer
+        ASSERT_OK_AND_ASSIGN(auto input_stream, file_system_->Open(file_path));
+        ASSERT_OK(input_stream->Seek(10, FS_SEEK_SET));  // ReadAsyncFully 
ignore any seek
+        std::vector<char> buffer(test_content.size());
+        ASSERT_OK(StreamUtils::ReadAsyncFully(std::move(input_stream), 
buffer.data()));
+        ASSERT_EQ(std::string(buffer.data(), buffer.size()), test_content);
+    }
+}
+
+TEST_F(StreamUtilsTest, ReadEmptyFile) {
+    std::string file_path = CreateTestFile("");
+
+    {
+        // ReadFully
+        ASSERT_OK_AND_ASSIGN(auto input_stream, file_system_->Open(file_path));
+        ASSERT_OK_AND_ASSIGN(auto result, 
StreamUtils::ReadFully(std::move(input_stream), pool_));
+        ASSERT_EQ(result->size(), 0);
+    }
+    {
+        // ReadAsyncFully
+        ASSERT_OK_AND_ASSIGN(auto input_stream, file_system_->Open(file_path));
+        ASSERT_OK_AND_ASSIGN(auto result,
+                             
StreamUtils::ReadAsyncFully(std::move(input_stream), pool_));
+        ASSERT_EQ(result->size(), 0);
+    }
+}
+
+TEST_F(StreamUtilsTest, ReadLargeFile) {
+    std::string large_content;
+    const size_t file_size = 3 * 1024 * 1024 + 512;  // 3.5MB
+    large_content.reserve(file_size);
+    for (size_t i = 0; i < file_size; ++i) {
+        large_content += static_cast<char>('A' + (i % 26));
+    }
+    std::string file_path = CreateTestFile(large_content);
+
+    {
+        // ReadFully
+        ASSERT_OK_AND_ASSIGN(auto input_stream, file_system_->Open(file_path));
+        ASSERT_OK_AND_ASSIGN(auto result, 
StreamUtils::ReadFully(std::move(input_stream), pool_));
+        ASSERT_EQ(result->size(), large_content.size());
+        ASSERT_EQ(std::string(result->data(), result->size()), large_content);
+    }
+    {
+        // ReadAsyncFully
+        ASSERT_OK_AND_ASSIGN(auto input_stream, file_system_->Open(file_path));
+        ASSERT_OK_AND_ASSIGN(auto result,
+                             
StreamUtils::ReadAsyncFully(std::move(input_stream), pool_));
+        ASSERT_EQ(result->size(), large_content.size());
+        ASSERT_EQ(std::string(result->data(), result->size()), large_content);
+    }
+    {
+        // ReadAsyncFully with buffer
+        ASSERT_OK_AND_ASSIGN(auto input_stream, file_system_->Open(file_path));
+        std::vector<char> buffer(file_size);
+        ASSERT_OK(StreamUtils::ReadAsyncFully(std::move(input_stream), 
buffer.data()));
+        ASSERT_EQ(std::string(buffer.data(), buffer.size()), large_content);
+    }
+}
+
+}  // namespace paimon

Reply via email to