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 4d1c320 feat: Migrate sort merge reader (#85)
4d1c320 is described below
commit 4d1c320093ff32408269e4fda11c1b3edae1c64c
Author: lxy <[email protected]>
AuthorDate: Thu Jun 18 10:16:49 2026 +0800
feat: Migrate sort merge reader (#85)
---
src/paimon/core/mergetree/compact/loser_tree.cpp | 196 +++++
src/paimon/core/mergetree/compact/loser_tree.h | 171 ++++
src/paimon/core/mergetree/compact/merge_function.h | 30 +
.../mergetree/compact/merge_function_wrapper.h | 32 +
.../core/mergetree/compact/sort_merge_reader.h | 41 +
.../mergetree/compact/sort_merge_reader_test.cpp | 903 +++++++++++++++++++++
.../compact/sort_merge_reader_with_loser_tree.cpp | 88 ++
.../compact/sort_merge_reader_with_loser_tree.h | 97 +++
.../compact/sort_merge_reader_with_min_heap.cpp | 137 ++++
.../compact/sort_merge_reader_with_min_heap.h | 160 ++++
src/paimon/core/mergetree/drop_delete_reader.h | 85 ++
.../core/mergetree/drop_delete_reader_test.cpp | 111 +++
12 files changed, 2051 insertions(+)
diff --git a/src/paimon/core/mergetree/compact/loser_tree.cpp
b/src/paimon/core/mergetree/compact/loser_tree.cpp
new file mode 100644
index 0000000..e932119
--- /dev/null
+++ b/src/paimon/core/mergetree/compact/loser_tree.cpp
@@ -0,0 +1,196 @@
+/*
+ * 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/mergetree/compact/loser_tree.h"
+
+#include <algorithm>
+#include <cassert>
+
+namespace paimon {
+LoserTree::LoserTree(std::vector<std::unique_ptr<KeyValueRecordReader>>&&
readers,
+ const CompareFunc& first_comparator, const CompareFunc&
second_comparator)
+ : size_(readers.size()),
+ initialized_(false),
+ readers_holder_(std::move(readers)),
+ tree_(size_),
+ first_comparator_(first_comparator),
+ second_comparator_(second_comparator) {
+ leaves_.reserve(size_);
+ for (const auto& reader : readers_holder_) {
+ leaves_.emplace_back(reader.get());
+ }
+}
+
+Status LoserTree::InitializeIfNeeded() {
+ if (!initialized_) {
+ std::fill(tree_.begin(), tree_.end(), -1);
+ for (int32_t i = size_ - 1; i >= 0; i--) {
+ PAIMON_RETURN_NOT_OK(leaves_[i].AdvanceIfAvailable());
+ Adjust(i);
+ }
+ initialized_ = true;
+ }
+ return Status::OK();
+}
+
+Status LoserTree::AdjustForNextLoop() {
+ LeafIterator* winner = &leaves_[tree_[0]];
+ while (winner->state == State::WINNER_POPPED) {
+ PAIMON_RETURN_NOT_OK(winner->AdvanceIfAvailable());
+ Adjust(tree_[0]);
+ winner = &leaves_[tree_[0]];
+ }
+ return Status::OK();
+}
+
+std::optional<KeyValue> LoserTree::PopWinner() {
+ LeafIterator* winner = &leaves_[tree_[0]];
+ if (winner->state == State::WINNER_POPPED) {
+ // if the winner has already been popped, it means that all the same
key has been
+ // processed.
+ return std::nullopt;
+ }
+ std::optional<KeyValue> result = std::move(winner->Pop());
+ Adjust(tree_[0]);
+ return result;
+}
+
+const std::optional<KeyValue>& LoserTree::PeekWinner() const {
+ static const std::optional<KeyValue> empty_kv = std::nullopt;
+ return leaves_[tree_[0]].state != State::WINNER_POPPED ?
leaves_[tree_[0]].Peek() : empty_kv;
+}
+
+void LoserTree::Adjust(int32_t winner) {
+ for (int32_t parent = (winner + size_) / 2; parent > 0 && winner >= 0;
parent /= 2) {
+ LeafIterator* winner_node = &leaves_[winner];
+ LeafIterator* parent_node = nullptr;
+
+ if (tree_[parent] == -1) {
+ // initialize the tree.
+ winner_node->state = State::LOSER_WITH_NEW_KEY;
+ } else {
+ parent_node = &leaves_[tree_[parent]];
+ switch (winner_node->state) {
+ case State::WINNER_WITH_NEW_KEY: {
+ AdjustWithNewWinnerKey(parent, parent_node, winner_node);
+ break;
+ }
+ case State::WINNER_WITH_SAME_KEY: {
+ AdjustWithSameWinnerKey(parent, parent_node, winner_node);
+ break;
+ }
+ case State::WINNER_POPPED: {
+ if (winner_node->first_same_key_index < 0) {
+ // fast path, which means that the same key is not yet
processed in the
+ // current tree.
+ parent = -1;
+ } else {
+ // fast path. Directly exchange positions with the
same key that has not
+ // yet been processed, no need to compare level by
level.
+ parent = winner_node->first_same_key_index;
+ parent_node = &leaves_[tree_[parent]];
+ winner_node->state = State::LOSER_POPPED;
+ parent_node->state = State::WINNER_WITH_SAME_KEY;
+ }
+ break;
+ }
+ default:
+ assert(false);
+ }
+ }
+
+ // if the winner loses, exchange nodes.
+ if (!IsWinner(winner_node->state)) {
+ std::swap(winner, tree_[parent]);
+ }
+ }
+ tree_[0] = winner;
+}
+
+void LoserTree::AdjustWithSameWinnerKey(int32_t index, LeafIterator*
parent_node,
+ LeafIterator* winner_node) {
+ switch (parent_node->state) {
+ case State::LOSER_WITH_SAME_KEY: {
+ // the key of the previous loser is the same as the key of the
current winner,
+ // only the sequence needs to be compared.
+ const auto& parent_key = parent_node->Peek();
+ const auto& child_key = winner_node->Peek();
+ int32_t second_result = second_comparator_(parent_key, child_key);
+ if (second_result > 0) {
+ parent_node->state = State::WINNER_WITH_SAME_KEY;
+ winner_node->state = State::LOSER_WITH_SAME_KEY;
+ parent_node->SetFirstSameKeyIndex(index);
+ } else {
+ winner_node->SetFirstSameKeyIndex(index);
+ }
+ return;
+ }
+ case State::LOSER_WITH_NEW_KEY:
+ case State::LOSER_POPPED:
+ return;
+ default:
+ assert(false);
+ }
+}
+
+void LoserTree::AdjustWithNewWinnerKey(int32_t index, LeafIterator*
parent_node,
+ LeafIterator* winner_node) {
+ switch (parent_node->state) {
+ case State::LOSER_WITH_NEW_KEY: {
+ // when the new winner is also a new key, it needs to be compared.
+ const auto& parent_key = parent_node->Peek();
+ const auto& child_key = winner_node->Peek();
+ int32_t first_result = first_comparator_(parent_key, child_key);
+ if (first_result == 0) {
+ // if the compared keys are the same, we need to update the
state of the node
+ // and record the index of the same key for the winner.
+ int32_t second_result = second_comparator_(parent_key,
child_key);
+ if (second_result < 0) {
+ parent_node->state = State::LOSER_WITH_SAME_KEY;
+ winner_node->SetFirstSameKeyIndex(index);
+ } else {
+ winner_node->state = State::LOSER_WITH_SAME_KEY;
+ parent_node->state = State::WINNER_WITH_NEW_KEY;
+ parent_node->SetFirstSameKeyIndex(index);
+ }
+ } else if (first_result > 0) {
+ // the two keys are completely different and just need to
update the state.
+ parent_node->state = State::WINNER_WITH_NEW_KEY;
+ winner_node->state = State::LOSER_WITH_NEW_KEY;
+ }
+ return;
+ }
+ case State::LOSER_WITH_SAME_KEY: {
+ // A node in the WINNER_WITH_NEW_KEY state cannot encounter a node
in the
+ // LOSER_WITH_SAME_KEY state.
+ assert(false);
+ break;
+ }
+ case State::LOSER_POPPED: {
+ // this case will only happen during adjustForNextLoop.
+ parent_node->state = State::WINNER_POPPED;
+ parent_node->first_same_key_index = -1;
+ winner_node->state = State::LOSER_WITH_NEW_KEY;
+ return;
+ }
+ default:
+ assert(false);
+ }
+}
+
+} // namespace paimon
diff --git a/src/paimon/core/mergetree/compact/loser_tree.h
b/src/paimon/core/mergetree/compact/loser_tree.h
new file mode 100644
index 0000000..1417d8d
--- /dev/null
+++ b/src/paimon/core/mergetree/compact/loser_tree.h
@@ -0,0 +1,171 @@
+/*
+ * 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 <functional>
+#include <memory>
+#include <optional>
+#include <utility>
+#include <vector>
+
+#include "paimon/common/metrics/metrics_impl.h"
+#include "paimon/core/io/key_value_record_reader.h"
+#include "paimon/core/key_value.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace paimon {
+class Metrics;
+
+class LoserTree {
+ public:
+ using CompareFunc =
+ std::function<int32_t(const std::optional<KeyValue>&, const
std::optional<KeyValue>&)>;
+ LoserTree(std::vector<std::unique_ptr<KeyValueRecordReader>>&& readers,
+ const CompareFunc& first_comparator, const CompareFunc&
second_comparator);
+
+ /// Initialize the loser tree in the same way as the regular loser tree.
+ Status InitializeIfNeeded();
+
+ /// Adjust the Key that needs to be returned in the next round.
+ Status AdjustForNextLoop();
+
+ /// Pop the current winner and update its state to `State#WINNER_POPPED`.
+ std::optional<KeyValue> PopWinner();
+
+ /// Peek the current winner, mainly for key comparisons.
+ const std::optional<KeyValue>& PeekWinner() const;
+
+ std::shared_ptr<Metrics> GetReaderMetrics() const {
+ return MetricsImpl::CollectReadMetrics(readers_holder_);
+ }
+
+ void Close() {
+ for (const auto& reader : readers_holder_) {
+ reader->Close();
+ }
+ }
+
+ private:
+ struct LeafIterator;
+
+ /// Adjust the winner from bottom to top. Using different `State`, we can
quickly compare
+ /// whether all the current same keys have been processed.
+ void Adjust(int32_t winner);
+
+ /// The winner node has the same userKey as the global winner.
+ void AdjustWithSameWinnerKey(int32_t index, LeafIterator* parent_node,
+ LeafIterator* winner_node);
+
+ /// The userKey of the new local winner node is different from that of the
previous global
+ /// winner.
+ void AdjustWithNewWinnerKey(int32_t index, LeafIterator* parent_node,
+ LeafIterator* winner_node);
+
+ private:
+ enum class State {
+ LOSER_WITH_NEW_KEY = 1,
+ LOSER_WITH_SAME_KEY = 2,
+ LOSER_POPPED = 3,
+ WINNER_WITH_NEW_KEY = 4,
+ WINNER_WITH_SAME_KEY = 5,
+ WINNER_POPPED = 6
+ };
+
+ static bool IsWinner(State state) {
+ if (state == State::LOSER_WITH_NEW_KEY || state ==
State::LOSER_WITH_SAME_KEY ||
+ state == State::LOSER_POPPED) {
+ return false;
+ }
+ return true;
+ }
+
+ struct LeafIterator {
+ explicit LeafIterator(KeyValueRecordReader* reader) : reader(reader) {}
+
+ const std::optional<KeyValue>& Peek() const {
+ return kv;
+ }
+
+ std::optional<KeyValue>&& Pop() {
+ state = State::WINNER_POPPED;
+ return std::move(kv);
+ }
+
+ void SetFirstSameKeyIndex(int32_t index) {
+ if (first_same_key_index == -1) {
+ first_same_key_index = index;
+ }
+ }
+
+ /// Reads the next kv if any, otherwise returns null.
+ Status AdvanceIfAvailable() {
+ first_same_key_index = -1;
+ state = State::WINNER_WITH_NEW_KEY;
+ bool has_next = false;
+ if (iterator != nullptr) {
+ PAIMON_ASSIGN_OR_RAISE(has_next, iterator->HasNext());
+ }
+ if (iterator == nullptr || !has_next) {
+ while (!end_of_input) {
+ PAIMON_ASSIGN_OR_RAISE(iterator, reader->NextBatch());
+ if (!iterator) {
+ // read eof
+ reader->Close();
+ end_of_input = true;
+ kv = std::nullopt;
+ } else {
+ PAIMON_ASSIGN_OR_RAISE(has_next, iterator->HasNext());
+ if (!has_next) {
+ continue;
+ }
+ PAIMON_ASSIGN_OR_RAISE(kv, iterator->Next());
+ break;
+ }
+ }
+ } else {
+ PAIMON_ASSIGN_OR_RAISE(kv, iterator->Next());
+ }
+ return Status::OK();
+ }
+
+ bool end_of_input = false;
+ int32_t first_same_key_index = -1;
+ State state = State::WINNER_WITH_NEW_KEY;
+ KeyValueRecordReader* reader;
+ std::unique_ptr<KeyValueRecordReader::Iterator> iterator;
+ std::optional<KeyValue> kv;
+ };
+
+ private:
+ int32_t size_;
+ bool initialized_;
+ // must hold all readers, as data array is allocated by the pool of data
file
+ // reader
+ std::vector<std::unique_ptr<KeyValueRecordReader>> readers_holder_;
+
+ std::vector<int32_t> tree_;
+ std::vector<LeafIterator> leaves_;
+ /// if comparator.compare('a', 'b') > 0, then 'a' is the winner. In the
following
+ /// implementation, we always let 'a' represent the parent node.
+ CompareFunc first_comparator_;
+ /// same as first_comparator, but mainly used to compare sequenceNumber.
+ CompareFunc second_comparator_;
+};
+} // namespace paimon
diff --git a/src/paimon/core/mergetree/compact/merge_function.h
b/src/paimon/core/mergetree/compact/merge_function.h
new file mode 100644
index 0000000..851e4df
--- /dev/null
+++ b/src/paimon/core/mergetree/compact/merge_function.h
@@ -0,0 +1,30 @@
+/*
+ * 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 "paimon/core/key_value.h"
+namespace paimon {
+/// Merge function to merge multiple `KeyValue`s.
+class MergeFunction {
+ public:
+ virtual ~MergeFunction() = default;
+ virtual void Reset() = 0;
+ virtual Status Add(KeyValue&& kv) = 0;
+ virtual Result<std::optional<KeyValue>> GetResult() = 0;
+};
+} // namespace paimon
diff --git a/src/paimon/core/mergetree/compact/merge_function_wrapper.h
b/src/paimon/core/mergetree/compact/merge_function_wrapper.h
new file mode 100644
index 0000000..215e9f0
--- /dev/null
+++ b/src/paimon/core/mergetree/compact/merge_function_wrapper.h
@@ -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.
+ */
+
+#pragma once
+#include "paimon/core/key_value.h"
+
+namespace paimon {
+/// A wrapper for `MergeFunction`, which adds new functionalities or
optimizations.
+template <typename T>
+class MergeFunctionWrapper {
+ public:
+ virtual ~MergeFunctionWrapper() = default;
+ virtual void Reset() = 0;
+ virtual Status Add(KeyValue&& kv) = 0;
+ virtual Result<std::optional<T>> GetResult() = 0;
+};
+} // namespace paimon
diff --git a/src/paimon/core/mergetree/compact/sort_merge_reader.h
b/src/paimon/core/mergetree/compact/sort_merge_reader.h
new file mode 100644
index 0000000..c7754e3
--- /dev/null
+++ b/src/paimon/core/mergetree/compact/sort_merge_reader.h
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <memory>
+
+#include "paimon/core/key_value.h"
+#include "paimon/metrics.h"
+#include "paimon/result.h"
+
+namespace paimon {
+class SortMergeReader {
+ public:
+ virtual ~SortMergeReader() = default;
+ class Iterator {
+ public:
+ virtual ~Iterator() = default;
+ virtual Result<bool> HasNext() = 0;
+ virtual KeyValue&& Next() = 0;
+ };
+
+ virtual Result<std::unique_ptr<Iterator>> NextBatch() = 0;
+ virtual void Close() = 0;
+ virtual std::shared_ptr<Metrics> GetReaderMetrics() const = 0;
+};
+} // namespace paimon
diff --git a/src/paimon/core/mergetree/compact/sort_merge_reader_test.cpp
b/src/paimon/core/mergetree/compact/sort_merge_reader_test.cpp
new file mode 100644
index 0000000..3c217b5
--- /dev/null
+++ b/src/paimon/core/mergetree/compact/sort_merge_reader_test.cpp
@@ -0,0 +1,903 @@
+/*
+ * 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/mergetree/compact/sort_merge_reader.h"
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <utility>
+#include <variant>
+#include <vector>
+
+#include "arrow/api.h"
+#include "arrow/array/array_base.h"
+#include "arrow/array/array_nested.h"
+#include "arrow/ipc/json_simple.h"
+#include "gtest/gtest.h"
+#include "paimon/common/types/data_field.h"
+#include "paimon/common/utils/fields_comparator.h"
+#include "paimon/core/core_options.h"
+#include "paimon/core/io/concat_key_value_record_reader.h"
+#include "paimon/core/io/key_value_record_reader.h"
+#include "paimon/core/io/merged_key_value_record_reader.h"
+#include "paimon/core/key_value.h"
+#include "paimon/core/mergetree/compact/aggregate/aggregate_merge_function.h"
+#include "paimon/core/mergetree/compact/deduplicate_merge_function.h"
+#include "paimon/core/mergetree/compact/reducer_merge_function_wrapper.h"
+#include "paimon/core/mergetree/compact/sort_merge_reader_with_loser_tree.h"
+#include "paimon/core/mergetree/compact/sort_merge_reader_with_min_heap.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+#include "paimon/testing/mock/mock_file_batch_reader.h"
+#include "paimon/testing/mock/mock_key_value_data_file_record_reader.h"
+#include "paimon/testing/utils/key_value_checker.h"
+#include "paimon/testing/utils/read_result_collector.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon::test {
+class SortMergeReaderTest : public testing::Test {
+ public:
+ void SetUp() override {
+ pool_ = GetDefaultPool();
+ }
+
+ std::vector<DataField> CreateDataField(const arrow::FieldVector&
arrow_fields) {
+ // create DataField with fake field id
+ std::vector<DataField> data_fields;
+ data_fields.reserve(arrow_fields.size());
+ for (const auto& field : arrow_fields) {
+ data_fields.emplace_back(/*id=*/0, field);
+ }
+ return data_fields;
+ }
+
+ void CheckResult(const std::vector<std::shared_ptr<arrow::StructArray>>&
src_array_vec,
+ const std::shared_ptr<FieldsComparator>&
user_key_comparator,
+ const std::shared_ptr<FieldsComparator>&
user_defined_seq_comparator,
+ const std::shared_ptr<arrow::Schema>& key_schema,
+ const std::shared_ptr<arrow::Schema>& value_schema,
+ const std::vector<KeyValue>& expected, bool ignore_delete
= false) const {
+ CheckSortMergeResult<SortMergeReaderWithLoserTree>(src_array_vec,
user_key_comparator,
+
user_defined_seq_comparator, key_schema,
+ value_schema,
expected,
+
/*need_merge=*/true, ignore_delete);
+ CheckSortMergeResult<SortMergeReaderWithMinHeap>(src_array_vec,
user_key_comparator,
+
user_defined_seq_comparator, key_schema,
+ value_schema,
expected,
+ /*need_merge=*/true,
ignore_delete);
+ }
+
+ private:
+ template <typename SortMergeReaderType>
+ std::unique_ptr<SortMergeReader> CreateSortMergeReader(
+ const std::vector<std::shared_ptr<arrow::StructArray>>& src_array_vec,
+ const std::shared_ptr<FieldsComparator>& user_key_comparator,
+ const std::shared_ptr<FieldsComparator>& user_defined_seq_comparator,
+ const std::shared_ptr<arrow::Schema>& key_schema,
+ const std::shared_ptr<arrow::Schema>& value_schema, int32_t
batch_size, bool need_merge,
+ bool ignore_delete) const {
+ auto mfunc = std::make_unique<DeduplicateMergeFunction>(ignore_delete);
+ auto merge_function_wrapper =
+ std::make_shared<ReducerMergeFunctionWrapper>(std::move(mfunc));
+ if (!need_merge) {
+ if constexpr (std::is_same_v<SortMergeReaderType,
SortMergeReaderWithMinHeap>) {
+ merge_function_wrapper = nullptr;
+ } else {
+ ADD_FAILURE() << "Only SortMergeReaderWithMinHeap supports no
merge";
+ }
+ }
+ std::vector<std::unique_ptr<KeyValueRecordReader>> concat_readers;
+ for (const auto& src_array : src_array_vec) {
+ auto file_batch_reader = std::make_unique<MockFileBatchReader>(
+ src_array, src_array->type(), /*batch_size=*/batch_size);
+ auto record_reader =
std::make_unique<MockKeyValueDataFileRecordReader>(
+ std::move(file_batch_reader), key_schema, value_schema,
/*level=*/0, pool_);
+ std::vector<std::unique_ptr<KeyValueRecordReader>> readers;
+ readers.push_back(std::move(record_reader));
+ concat_readers.push_back(
+
std::make_unique<ConcatKeyValueRecordReader>(std::move(readers)));
+ }
+
+ return
std::make_unique<SortMergeReaderType>(std::move(concat_readers),
user_key_comparator,
+
user_defined_seq_comparator,
+ merge_function_wrapper);
+ }
+
+ template <typename SortMergeReaderType>
+ void CheckSortMergeResult(const
std::vector<std::shared_ptr<arrow::StructArray>>& src_array_vec,
+ const std::shared_ptr<FieldsComparator>&
user_key_comparator,
+ const std::shared_ptr<FieldsComparator>&
user_defined_seq_comparator,
+ const std::shared_ptr<arrow::Schema>& key_schema,
+ const std::shared_ptr<arrow::Schema>&
value_schema,
+ const std::vector<KeyValue>& expected, bool
need_merge,
+ bool ignore_delete = false) const {
+ for (auto batch_size : {1, 2, 3, 4, 100}) {
+ auto sort_merge_reader =
CreateSortMergeReader<SortMergeReaderType>(
+ src_array_vec, user_key_comparator,
user_defined_seq_comparator, key_schema,
+ value_schema, batch_size, need_merge, ignore_delete);
+ ASSERT_OK_AND_ASSIGN(
+ std::vector<KeyValue> results,
+ (ReadResultCollector::CollectKeyValueResult<
+ SortMergeReader,
SortMergeReader::Iterator>(sort_merge_reader.get())));
+ KeyValueChecker::CheckResult(expected, results,
key_schema->num_fields(),
+ value_schema->num_fields());
+ }
+ }
+
+ template <typename SortMergeReaderType>
+ void CheckSortMergeResultForAggregate(
+ const std::vector<std::shared_ptr<arrow::StructArray>>& src_array_vec,
+ const std::shared_ptr<FieldsComparator>& user_key_comparator,
+ const std::shared_ptr<FieldsComparator>& user_defined_seq_comparator,
+ const std::shared_ptr<arrow::Schema>& key_schema,
+ const std::shared_ptr<arrow::Schema>& value_schema,
+ const std::vector<std::string>& user_defined_sequence_fields,
+ const std::vector<std::string>& primary_keys, const CoreOptions&
core_options,
+ const std::vector<KeyValue>& expected) const {
+ for (auto batch_size : {1, 2, 3, 4, 100}) {
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<AggregateMergeFunction> mfunc,
+ AggregateMergeFunction::Create(value_schema, primary_keys,
core_options));
+ auto merge_function_wrapper =
+
std::make_shared<ReducerMergeFunctionWrapper>(std::move(mfunc));
+ std::vector<std::unique_ptr<KeyValueRecordReader>> merged_readers;
+
+ std::vector<std::unique_ptr<KeyValueRecordReader>> readers;
+ for (const auto& src_array : src_array_vec) {
+ auto file_batch_reader = std::make_unique<MockFileBatchReader>(
+ src_array, src_array->type(), /*batch_size=*/batch_size);
+ auto record_reader =
std::make_unique<MockKeyValueDataFileRecordReader>(
+ std::move(file_batch_reader), key_schema, value_schema,
/*level=*/0, pool_);
+
merged_readers.push_back(std::make_unique<MergedKeyValueRecordReader>(
+ std::move(record_reader), user_key_comparator,
merge_function_wrapper));
+ }
+
+ auto sort_merge_reader = std::make_unique<SortMergeReaderType>(
+ std::move(merged_readers), user_key_comparator,
user_defined_seq_comparator,
+ merge_function_wrapper);
+ ASSERT_OK_AND_ASSIGN(
+ std::vector<KeyValue> results,
+ (ReadResultCollector::CollectKeyValueResult<
+ SortMergeReader,
SortMergeReader::Iterator>(sort_merge_reader.get())));
+ KeyValueChecker::CheckResult(expected, results,
key_schema->num_fields(),
+ value_schema->num_fields());
+ }
+ }
+
+ private:
+ std::shared_ptr<MemoryPool> pool_;
+};
+
+TEST_F(SortMergeReaderTest, TestSimpleWithTwoSameKeys) {
+ arrow::FieldVector fields = {arrow::field("_SEQUENCE_NUMBER",
arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()),
+ arrow::field("k0", arrow::int32()),
+ arrow::field("v0", arrow::int32())};
+
+ auto data_fields = CreateDataField(fields);
+ std::shared_ptr<arrow::Schema> key_schema =
arrow::schema(arrow::FieldVector({fields[2]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [0, 0, 2, 10]
+ ])")
+ .ValueOrDie());
+
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 3, 30]
+ ])")
+ .ValueOrDie());
+
+ auto src_array3 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 5, 50]
+ ])")
+ .ValueOrDie());
+
+ auto src_array4 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 2, 30]
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2]},
std::vector<int32_t>({0}),
+
/*is_ascending_order=*/true));
+ std::vector<KeyValue> expected = KeyValueChecker::GenerateKeyValues(
+ {1, 1, 1}, {{2}, {3}, {5}}, {{2, 30}, {3, 30}, {5, 50}}, pool_);
+ CheckSortMergeResult<SortMergeReaderWithLoserTree>(
+ {src_array1, src_array2, src_array3, src_array4}, user_key_comparator,
nullptr, key_schema,
+ value_schema, expected, /*need_merge=*/true);
+}
+
+TEST_F(SortMergeReaderTest, TestSimpleWithThreeSameKeys) {
+ arrow::FieldVector fields = {arrow::field("_SEQUENCE_NUMBER",
arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()),
+ arrow::field("k0", arrow::int32()),
+ arrow::field("v0", arrow::int32())};
+
+ auto data_fields = CreateDataField(fields);
+ std::shared_ptr<arrow::Schema> key_schema =
arrow::schema(arrow::FieldVector({fields[2]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [0, 0, 2, 10]
+ ])")
+ .ValueOrDie());
+
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [2, 0, 2, 30]
+ ])")
+ .ValueOrDie());
+
+ auto src_array3 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 5, 50]
+ ])")
+ .ValueOrDie());
+
+ auto src_array4 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 2, 30]
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2]},
std::vector<int32_t>({0}),
+
/*is_ascending_order=*/true));
+ std::vector<KeyValue> expected =
+ KeyValueChecker::GenerateKeyValues({2, 1}, {{2}, {5}}, {{2, 30}, {5,
50}}, pool_);
+ CheckSortMergeResult<SortMergeReaderWithLoserTree>(
+ {src_array1, src_array2, src_array3, src_array4}, user_key_comparator,
nullptr, key_schema,
+ value_schema, expected, /*need_merge=*/true);
+}
+
+TEST_F(SortMergeReaderTest, TestSimpleWithThreeSameKeys2) {
+ arrow::FieldVector fields = {arrow::field("_SEQUENCE_NUMBER",
arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()),
+ arrow::field("k0", arrow::int32()),
+ arrow::field("v0", arrow::int32())};
+
+ auto data_fields = CreateDataField(fields);
+ std::shared_ptr<arrow::Schema> key_schema =
arrow::schema(arrow::FieldVector({fields[2]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [0, 0, 2, 10]
+ ])")
+ .ValueOrDie());
+
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 2, 30]
+ ])")
+ .ValueOrDie());
+
+ auto src_array3 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 5, 50]
+ ])")
+ .ValueOrDie());
+
+ auto src_array4 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [2, 0, 2, 30]
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2]},
std::vector<int32_t>({0}),
+
/*is_ascending_order=*/true));
+ std::vector<KeyValue> expected =
+ KeyValueChecker::GenerateKeyValues({2, 1}, {{2}, {5}}, {{2, 30}, {5,
50}}, pool_);
+ CheckSortMergeResult<SortMergeReaderWithLoserTree>(
+ {src_array1, src_array2, src_array3, src_array4}, user_key_comparator,
nullptr, key_schema,
+ value_schema, expected, /*need_merge=*/true);
+}
+
+TEST_F(SortMergeReaderTest, TestSortMergeIn2Ways) {
+ arrow::FieldVector fields = {arrow::field("_SEQUENCE_NUMBER",
arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()),
+ arrow::field("k0", arrow::int32()),
+ arrow::field("k1", arrow::int32()),
+ arrow::field("v0", arrow::int32()),
+ arrow::field("v1", arrow::int32()),
+ arrow::field("v2", arrow::int32())};
+
+ auto data_fields = CreateDataField(fields);
+ std::shared_ptr<arrow::Schema> key_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3], fields[4],
fields[5], fields[6]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [0, 0, 1, 1, 10, 20, 30],
+ [0, 0, 1, 3, 11, 21, 31],
+ [1, 0, 2, 2, 12, 22, 32],
+ [2, 0, 2, 3, 13, 23, 33]
+ ])")
+ .ValueOrDie());
+
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 1, 1, 14, 24, 34],
+ [0, 0, 1, 2, 15, 25, 35],
+ [2, 0, 2, 2, 16, 26, 36],
+ [2, 0, 2, 5, 17, 27, 37]
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2], data_fields[3]},
std::vector<int32_t>({0, 1}),
+ /*is_ascending_order=*/true));
+ std::vector<KeyValue> expected = KeyValueChecker::GenerateKeyValues(
+ {1, 0, 0, 2, 2, 2}, {{1, 1}, {1, 2}, {1, 3}, {2, 2}, {2, 3}, {2, 5}},
+ {{1, 1, 14, 24, 34},
+ {1, 2, 15, 25, 35},
+ {1, 3, 11, 21, 31},
+ {2, 2, 16, 26, 36},
+ {2, 3, 13, 23, 33},
+ {2, 5, 17, 27, 37}},
+ pool_);
+ CheckResult({src_array1, src_array2}, user_key_comparator,
+ /*user_defined_seq_comparator=*/nullptr, key_schema,
value_schema, expected);
+}
+
+TEST_F(SortMergeReaderTest, TestSortMergeIn3Ways) {
+ arrow::FieldVector fields = {arrow::field("_SEQUENCE_NUMBER",
arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()),
+ arrow::field("k0", arrow::int32()),
+ arrow::field("v0", arrow::int32())};
+ auto data_fields = CreateDataField(fields);
+ std::shared_ptr<arrow::Schema> key_schema =
arrow::schema(arrow::FieldVector({fields[2]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [0, 0, 1, 10],
+ [0, 0, 2, 11],
+ [1, 0, 3, 12],
+ [2, 0, 4, 13]
+ ])")
+ .ValueOrDie());
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 2, 14],
+ [2, 0, 3, 15],
+ [3, 0, 4, 16],
+ [2, 0, 5, 17]
+ ])")
+ .ValueOrDie());
+ auto src_array3 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 1, 17],
+ [2, 0, 2, 18],
+ [4, 0, 4, 19],
+ [4, 0, 5, 20]
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2]},
std::vector<int32_t>({0}),
+
/*is_ascending_order=*/true));
+ std::vector<KeyValue> expected =
+ KeyValueChecker::GenerateKeyValues({1, 2, 2, 4, 4}, {{1}, {2}, {3},
{4}, {5}},
+ {{1, 17}, {2, 18}, {3, 15}, {4,
19}, {5, 20}}, pool_);
+
+ CheckResult({src_array1, src_array2, src_array3}, user_key_comparator,
+ /*user_defined_seq_comparator=*/nullptr, key_schema,
value_schema, expected);
+}
+
+TEST_F(SortMergeReaderTest, TestSortMergeWithDeleteMessages) {
+ arrow::FieldVector fields = {arrow::field("_SEQUENCE_NUMBER",
arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()),
+ arrow::field("k0", arrow::int32()),
+ arrow::field("v0", arrow::int32())};
+ auto data_fields = CreateDataField(fields);
+ std::shared_ptr<arrow::Schema> key_schema =
arrow::schema(arrow::FieldVector({fields[2]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [0, 3, 1, 10],
+ [2, 3, 2, 200],
+ [1, 0, 3, 300]
+ ])")
+ .ValueOrDie());
+
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [3, 3, 1, 11],
+ [4, 3, 2, 240],
+ [5, 3, 3, 30]
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2]},
std::vector<int32_t>({0}),
+
/*is_ascending_order=*/true));
+
+ std::vector<RowKind*> delete_row_kinds =
{const_cast<RowKind*>(RowKind::Delete()),
+
const_cast<RowKind*>(RowKind::Delete()),
+
const_cast<RowKind*>(RowKind::Delete())};
+ std::vector<KeyValue> expected_delete = KeyValueChecker::GenerateKeyValues(
+ delete_row_kinds, /*seq_vec=*/{3, 4, 5}, /*level_vec=*/{0, 0, 0},
+ /*key_vec=*/{{1}, {2}, {3}}, /*value_vec=*/{{1, 11}, {2, 240}, {3,
30}}, pool_);
+ CheckResult({src_array1, src_array2}, user_key_comparator,
+ /*user_defined_seq_comparator=*/nullptr, key_schema,
value_schema, expected_delete,
+ /*ignore_delete=*/false);
+
+ std::vector<KeyValue> expected_ignore_delete =
KeyValueChecker::GenerateKeyValues(
+ /*seq_vec=*/{1}, /*key_vec=*/{{3}}, /*value_vec=*/{{3, 300}}, pool_);
+ CheckResult({src_array1, src_array2}, user_key_comparator,
+ /*user_defined_seq_comparator=*/nullptr, key_schema,
value_schema,
+ expected_ignore_delete, /*ignore_delete=*/true);
+}
+
+TEST_F(SortMergeReaderTest, TestSortMergeIn2WaysWithEmptyArray) {
+ arrow::FieldVector fields = {arrow::field("_SEQUENCE_NUMBER",
arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()),
+ arrow::field("k0", arrow::int32()),
+ arrow::field("v0", arrow::int32())};
+ auto data_fields = CreateDataField(fields);
+
+ std::shared_ptr<arrow::Schema> key_schema =
arrow::schema(arrow::FieldVector({fields[2]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [0, 0, 1, 10],
+ [0, 0, 2, 11],
+ [1, 0, 3, 12],
+ [2, 0, 4, 13]
+ ])")
+ .ValueOrDie());
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2]},
std::vector<int32_t>({0}),
+
/*is_ascending_order=*/true));
+ std::vector<KeyValue> expected = KeyValueChecker::GenerateKeyValues(
+ {0, 0, 1, 2}, {{1}, {2}, {3}, {4}}, {{1, 10}, {2, 11}, {3, 12}, {4,
13}}, pool_);
+
+ CheckResult({src_array1, src_array2}, user_key_comparator,
+ /*user_defined_seq_comparator=*/nullptr, key_schema,
value_schema, expected);
+}
+
+TEST_F(SortMergeReaderTest, TestSortMergeIn2WaysWithNoOverlap) {
+ arrow::FieldVector fields = {arrow::field("_SEQUENCE_NUMBER",
arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()),
+ arrow::field("k0", arrow::int32()),
+ arrow::field("v0", arrow::int32())};
+
+ auto data_fields = CreateDataField(fields);
+ std::shared_ptr<arrow::Schema> key_schema =
arrow::schema(arrow::FieldVector({fields[2]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [0, 0, 1, 10],
+ [0, 0, 2, 11],
+ [1, 0, 3, 12],
+ [2, 0, 4, 13]
+ ])")
+ .ValueOrDie());
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 102, 14],
+ [2, 0, 103, 15],
+ [3, 0, 104, 16],
+ [2, 0, 105, 17]
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2]},
std::vector<int32_t>({0}),
+
/*is_ascending_order=*/true));
+ std::vector<KeyValue> expected = KeyValueChecker::GenerateKeyValues(
+ {0, 0, 1, 2, 1, 2, 3, 2}, {{1}, {2}, {3}, {4}, {102}, {103}, {104},
{105}},
+ {{1, 10}, {2, 11}, {3, 12}, {4, 13}, {102, 14}, {103, 15}, {104, 16},
{105, 17}}, pool_);
+
+ CheckResult({src_array1, src_array2}, user_key_comparator,
+ /*user_defined_seq_comparator=*/nullptr, key_schema,
value_schema, expected);
+}
+
+TEST_F(SortMergeReaderTest, TestSortMergeIn2WaysWithFullOverlap) {
+ arrow::FieldVector fields = {arrow::field("_SEQUENCE_NUMBER",
arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()),
+ arrow::field("k0", arrow::int32()),
+ arrow::field("v0", arrow::int32())};
+
+ auto data_fields = CreateDataField(fields);
+ std::shared_ptr<arrow::Schema> key_schema =
arrow::schema(arrow::FieldVector({fields[2]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [0, 0, 1, 10],
+ [0, 0, 2, 11],
+ [1, 0, 3, 12],
+ [2, 0, 4, 13]
+ ])")
+ .ValueOrDie());
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 1, 14],
+ [2, 0, 2, 15],
+ [3, 0, 3, 16],
+ [3, 0, 4, 17]
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2]},
std::vector<int32_t>({0}),
+
/*is_ascending_order=*/true));
+ std::vector<KeyValue> expected = KeyValueChecker::GenerateKeyValues(
+ {1, 2, 3, 3}, {{1}, {2}, {3}, {4}}, {{1, 14}, {2, 15}, {3, 16}, {4,
17}}, pool_);
+
+ CheckResult({src_array1, src_array2}, user_key_comparator,
+ /*user_defined_seq_comparator=*/nullptr, key_schema,
value_schema, expected);
+}
+
+TEST_F(SortMergeReaderTest, TestSortMergeIn2WaysWithPartialOverlap) {
+ arrow::FieldVector fields = {arrow::field("_SEQUENCE_NUMBER",
arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()),
+ arrow::field("k0", arrow::int32()),
+ arrow::field("v0", arrow::int32())};
+
+ auto data_fields = CreateDataField(fields);
+ std::shared_ptr<arrow::Schema> key_schema =
arrow::schema(arrow::FieldVector({fields[2]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [0, 0, 1, 10],
+ [0, 0, 2, 11],
+ [1, 0, 3, 12],
+ [2, 0, 4, 13]
+ ])")
+ .ValueOrDie());
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 1, 14],
+ [2, 0, 2, 15],
+ [3, 0, 3, 16],
+ [3, 0, 4, 17],
+ [0, 0, 5, 18],
+ [0, 0, 6, 19]
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2]},
std::vector<int32_t>({0}),
+
/*is_ascending_order=*/true));
+ std::vector<KeyValue> expected = KeyValueChecker::GenerateKeyValues(
+ {1, 2, 3, 3, 0, 0}, {{1}, {2}, {3}, {4}, {5}, {6}},
+ {{1, 14}, {2, 15}, {3, 16}, {4, 17}, {5, 18}, {6, 19}}, pool_);
+
+ CheckResult({src_array1, src_array2}, user_key_comparator,
+ /*user_defined_seq_comparator=*/nullptr, key_schema,
value_schema, expected);
+}
+
+TEST_F(SortMergeReaderTest, TestSortMergeIn3WaysWithUserDefinedSeq) {
+ // key: k0, k1
+ // user defined sequence field: v0, v1
+ arrow::FieldVector fields = {arrow::field("_SEQUENCE_NUMBER",
arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()),
+ arrow::field("k0", arrow::int32()),
+ arrow::field("k1", arrow::int32()),
+ arrow::field("v0", arrow::int32()),
+ arrow::field("v1", arrow::int32()),
+ arrow::field("v2", arrow::int32())};
+
+ auto data_fields = CreateDataField(fields);
+ std::shared_ptr<arrow::Schema> key_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3], fields[4],
fields[5], fields[6]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [3, 0, 1, 1, 10, 20, 30],
+ [0, 0, 1, 3, 11, 21, 31],
+ [3, 0, 2, 2, 12, 22, 32],
+ [2, 0, 2, 3, 13, 23, 33]
+ ])")
+ .ValueOrDie());
+
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 1, 1, 14, 24, 34],
+ [1, 0, 1, 2, 15, 25, 35],
+ [2, 0, 2, 2, 18, 28, 38],
+ [2, 0, 2, 5, 17, 27, 37]
+ ])")
+ .ValueOrDie());
+
+ auto src_array3 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [2, 0, 1, 1, 14, 24, 34],
+ [0, 0, 1, 2, 15, 25, 35],
+ [5, 0, 2, 2, 16, 26, 36],
+ [3, 0, 2, 5, 17, 28, 37]
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2], data_fields[3]},
std::vector<int32_t>({0, 1}),
+ /*is_ascending_order=*/true));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator>
user_defined_seq_comparator,
+ FieldsComparator::Create({data_fields[2],
data_fields[3], data_fields[4],
+ data_fields[5],
data_fields[6]},
+ std::vector<int32_t>({2, 3}),
+
/*is_ascending_order=*/true));
+ std::vector<KeyValue> expected = KeyValueChecker::GenerateKeyValues(
+ {2, 1, 0, 2, 2, 3}, {{1, 1}, {1, 2}, {1, 3}, {2, 2}, {2, 3}, {2, 5}},
+ {{1, 1, 14, 24, 34},
+ {1, 2, 15, 25, 35},
+ {1, 3, 11, 21, 31},
+ {2, 2, 18, 28, 38},
+ {2, 3, 13, 23, 33},
+ {2, 5, 17, 28, 37}},
+ pool_);
+ CheckResult({src_array1, src_array2, src_array3}, user_key_comparator,
+ user_defined_seq_comparator, key_schema, value_schema,
expected);
+}
+
+TEST_F(SortMergeReaderTest, TestSortMergeWithAggMergeFunction) {
+ // key: k0, user defined sequence field: ts, value: v0
+ // Format: [_SEQUENCE_NUMBER, _VALUE_KIND, k0, ts, v0]
+ // Using sum aggregation: k0 uses primary-key agg, ts use last_value agg
and v0 use sum agg.
+ //
+ // Reader1 (SEQUENCE_NUMBER 0..5):
+ // [key=1,ts=1,v=1], [key=1,ts=2,v=2], [key=1,ts=3,v=3]
+ // [key=1,ts=4,v=4], [key=2,ts=4,v=40], [key=2,ts=5,v=50]
+ // Reader2 (SEQUENCE_NUMBER 6..11):
+ // [key=1,ts=5,v=5], [key=1,ts=6,v=6], [key=2,ts=1,v=10]
+ // [key=2,ts=2,v=20], [key=2,ts=3,v=30], [key=2,ts=6,v=60]
+ //
+ // With user_defined_seq_comparator on ts field, sort by key asc, then ts
asc within same key:
+ // key=1: ts=1(v=1), ts=2(v=2), ts=3(v=3), ts=4(v=4), ts=5(v=5), ts=6(v=6)
+ // key=2: ts=1(v=10), ts=2(v=20), ts=3(v=30), ts=4(v=40), ts=5(v=50),
ts=6(v=60)
+ //
+ // After sum aggregation:
+ // key=1: k0=1, ts=last_value(1,2,3,4,5,6)=6, v0=sum(1,2,3,4,5,6)=21, seq=7
+ // key=2: k0=2, ts=last_value(1,2,3,4,5,6)=6,
v0=sum(10,20,30,40,50,60)=210, seq=11
+
+ arrow::FieldVector fields = {
+ arrow::field("_SEQUENCE_NUMBER", arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()), arrow::field("k0",
arrow::int32()),
+ arrow::field("ts", arrow::int32()), arrow::field("v0",
arrow::int32())};
+
+ auto data_fields = CreateDataField(fields);
+ std::shared_ptr<arrow::Schema> key_schema =
arrow::schema(arrow::FieldVector({fields[2]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3], fields[4]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [0, 0, 1, 1, 1],
+ [1, 0, 1, 2, 2],
+ [2, 0, 1, 3, 3],
+ [3, 0, 1, 4, 4],
+ [4, 0, 2, 4, 40],
+ [5, 0, 2, 5, 50]
+ ])")
+ .ValueOrDie());
+
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [6, 0, 1, 5, 5],
+ [7, 0, 1, 6, 6],
+ [8, 0, 2, 1, 10],
+ [9, 0, 2, 2, 20],
+ [10, 0, 2, 3, 30],
+ [11, 0, 2, 6, 60]
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2]},
std::vector<int32_t>({0}),
+
/*is_ascending_order=*/true));
+ // user_defined_seq_comparator based on ts field (index 1 in value schema
{k0, ts, v0})
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator>
user_defined_seq_comparator,
+ FieldsComparator::Create({data_fields[2],
data_fields[3], data_fields[4]},
+ std::vector<int32_t>({1}),
+
/*is_ascending_order=*/true));
+ // Configure sum aggregation for all non-primary-key fields
+ std::string user_defined_sequence_field = "ts";
+ ASSERT_OK_AND_ASSIGN(
+ CoreOptions core_options,
+ CoreOptions::FromMap({{Options::FIELDS_DEFAULT_AGG_FUNC, "sum"},
+ {Options::SEQUENCE_FIELD,
user_defined_sequence_field}}));
+
+ // After sum aggregation, same-key rows are merged:
+ // key=1: seq=7, k0=1, ts=6, v0=21
+ // key=2: seq=11, k0=2, ts=6, v0=210
+ std::vector<KeyValue> expected =
+ KeyValueChecker::GenerateKeyValues({7, 11}, {{1}, {2}}, {{1, 6, 21},
{2, 6, 210}}, pool_);
+ for (auto& kv : expected) {
+ kv.level = KeyValue::UNKNOWN_LEVEL;
+ }
+ CheckSortMergeResultForAggregate<SortMergeReaderWithLoserTree>(
+ {src_array1, src_array2}, user_key_comparator,
user_defined_seq_comparator, key_schema,
+ value_schema, {user_defined_sequence_field}, {"k0"}, core_options,
expected);
+ CheckSortMergeResultForAggregate<SortMergeReaderWithMinHeap>(
+ {src_array1, src_array2}, user_key_comparator,
user_defined_seq_comparator, key_schema,
+ value_schema, {user_defined_sequence_field}, {"k0"}, core_options,
expected);
+}
+
+TEST_F(SortMergeReaderTest, TestRawSortNoMergeKeepsDuplicateKeys) {
+ arrow::FieldVector fields = {arrow::field("_SEQUENCE_NUMBER",
arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()),
+ arrow::field("k0", arrow::int32()),
+ arrow::field("v0", arrow::int32())};
+
+ auto data_fields = CreateDataField(fields);
+ std::shared_ptr<arrow::Schema> key_schema =
arrow::schema(arrow::FieldVector({fields[2]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [0, 0, 2, 10]
+ ])")
+ .ValueOrDie());
+
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [2, 0, 2, 30]
+ ])")
+ .ValueOrDie());
+
+ auto src_array3 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 5, 50]
+ ])")
+ .ValueOrDie());
+
+ auto src_array4 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [1, 0, 2, 30]
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2]},
std::vector<int32_t>({0}),
+
/*is_ascending_order=*/true));
+ std::vector<KeyValue> expected = KeyValueChecker::GenerateKeyValues(
+ {0, 1, 2, 1}, {{2}, {2}, {2}, {5}}, {{2, 10}, {2, 30}, {2, 30}, {5,
50}}, pool_);
+ CheckSortMergeResult<SortMergeReaderWithMinHeap>(
+ {src_array1, src_array2, src_array3, src_array4}, user_key_comparator,
+ /*user_defined_seq_comparator=*/nullptr, key_schema, value_schema,
expected,
+ /*need_merge=*/false);
+}
+
+TEST_F(SortMergeReaderTest, TestRawSortNoMergeWithMinHeap) {
+ // key: k0, user defined sequence field: ts, value: v0
+ // Format: [_SEQUENCE_NUMBER, _VALUE_KIND, k0, ts, v0]
+ // Reader1 (SEQUENCE_NUMBER 0..5):
+ // [key=1,ts=1,v=1], [key=1,ts=2,v=2], [key=1,ts=3,v=3]
+ // [key=1,ts=4,v=4], [key=2,ts=4,v=40], [key=2,ts=5,v=50]
+ // Reader2 (SEQUENCE_NUMBER 6..11):
+ // [key=1,ts=5,v=5], [key=1,ts=6,v=6], [key=2,ts=1,v=10]
+ // [key=2,ts=2,v=20], [key=2,ts=3,v=30], [key=2,ts=6,v=60]
+ //
+ // After sort:
+ // With user_defined_seq_comparator on ts field, sort by key asc, then ts
asc within same key
+ // key=1: ts=1(seq0,v=1), ts=2(seq1,v=2), ts=3(seq2,v=3), ts=4(seq3,v=4),
+ // ts=5(seq6,v=5), ts=6(seq7,v=6)
+ // key=2: ts=1(seq8,v=10), ts=2(seq9,v=20), ts=3(seq10,v=30),
ts=4(seq4,v=40),
+ // ts=5(seq5,v=50), ts=6(seq11,v=60)
+
+ arrow::FieldVector fields = {
+ arrow::field("_SEQUENCE_NUMBER", arrow::int64()),
+ arrow::field("_VALUE_KIND", arrow::int8()), arrow::field("k0",
arrow::int32()),
+ arrow::field("ts", arrow::int32()), arrow::field("v0",
arrow::int32())};
+
+ auto data_fields = CreateDataField(fields);
+ std::shared_ptr<arrow::Schema> key_schema =
arrow::schema(arrow::FieldVector({fields[2]}));
+ std::shared_ptr<arrow::Schema> value_schema =
+ arrow::schema(arrow::FieldVector({fields[2], fields[3], fields[4]}));
+ std::shared_ptr<arrow::DataType> src_type = arrow::struct_(fields);
+
+ auto src_array1 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [0, 0, 1, 1, 1],
+ [1, 0, 1, 2, 2],
+ [2, 0, 1, 3, 3],
+ [3, 0, 1, 4, 4],
+ [4, 0, 2, 4, 40],
+ [5, 0, 2, 5, 50]
+ ])")
+ .ValueOrDie());
+
+ auto src_array2 = std::dynamic_pointer_cast<arrow::StructArray>(
+ arrow::ipc::internal::json::ArrayFromJSON(src_type, R"([
+ [6, 0, 1, 5, 5],
+ [7, 0, 1, 6, 6],
+ [8, 0, 2, 1, 10],
+ [9, 0, 2, 2, 20],
+ [10, 0, 2, 3, 30],
+ [11, 0, 2, 6, 60]
+ ])")
+ .ValueOrDie());
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator> user_key_comparator,
+ FieldsComparator::Create({data_fields[2]},
std::vector<int32_t>({0}),
+
/*is_ascending_order=*/true));
+ // user_defined_seq_comparator based on ts field (index 1 in value schema
{k0, ts, v0})
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator>
user_defined_seq_comparator,
+ FieldsComparator::Create({data_fields[2],
data_fields[3], data_fields[4]},
+ std::vector<int32_t>({1}),
+
/*is_ascending_order=*/true));
+
+ std::vector<KeyValue> expected = KeyValueChecker::GenerateKeyValues(
+ {0, 1, 2, 3, 6, 7, 8, 9, 10, 4, 5, 11},
+ {{1}, {1}, {1}, {1}, {1}, {1}, {2}, {2}, {2}, {2}, {2}, {2}},
+ {{1, 1, 1},
+ {1, 2, 2},
+ {1, 3, 3},
+ {1, 4, 4},
+ {1, 5, 5},
+ {1, 6, 6},
+ {2, 1, 10},
+ {2, 2, 20},
+ {2, 3, 30},
+ {2, 4, 40},
+ {2, 5, 50},
+ {2, 6, 60}},
+ pool_);
+ CheckSortMergeResult<SortMergeReaderWithMinHeap>({src_array1, src_array2},
user_key_comparator,
+
user_defined_seq_comparator, key_schema,
+ value_schema, expected,
/*need_merge=*/false);
+}
+
+} // namespace paimon::test
diff --git
a/src/paimon/core/mergetree/compact/sort_merge_reader_with_loser_tree.cpp
b/src/paimon/core/mergetree/compact/sort_merge_reader_with_loser_tree.cpp
new file mode 100644
index 0000000..17b0009
--- /dev/null
+++ b/src/paimon/core/mergetree/compact/sort_merge_reader_with_loser_tree.cpp
@@ -0,0 +1,88 @@
+/*
+ * 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/mergetree/compact/sort_merge_reader_with_loser_tree.h"
+
+#include <cassert>
+#include <cstdint>
+
+#include "paimon/common/utils/fields_comparator.h"
+#include "paimon/core/io/key_value_record_reader.h"
+
+namespace paimon {
+SortMergeReaderWithLoserTree::SortMergeReaderWithLoserTree(
+ std::vector<std::unique_ptr<KeyValueRecordReader>>&& readers,
+ const std::shared_ptr<FieldsComparator>& user_key_comparator,
+ const std::shared_ptr<FieldsComparator>& user_defined_seq_comparator,
+ const std::shared_ptr<MergeFunctionWrapper<KeyValue>>&
merge_function_wrapper)
+ : merge_function_wrapper_(merge_function_wrapper) {
+ // if lhs and rhs are both null, it doesn't matter who becomes the new
winner. But if
+ // first_comparator returns 0, it means that second_comparator must be
used to compare
+ // again.
+ // lhs and rhs are swapped when compare to generate loser tree pop
smallest first
+ auto first_comparator = [user_key_comparator](const
std::optional<KeyValue>& lhs,
+ const
std::optional<KeyValue>& rhs) -> int32_t {
+ if (lhs == std::nullopt) {
+ return -1;
+ }
+ if (rhs == std::nullopt) {
+ return 1;
+ }
+ return user_key_comparator->CompareTo(*(rhs.value().key),
*(lhs.value().key));
+ };
+ auto second_comparator = [user_defined_seq_comparator](
+ const std::optional<KeyValue>& lhs,
+ const std::optional<KeyValue>& rhs) ->
int32_t {
+ if (lhs == std::nullopt) {
+ return -1;
+ }
+ if (rhs == std::nullopt) {
+ return 1;
+ }
+ if (user_defined_seq_comparator != nullptr) {
+ int32_t result =
+ user_defined_seq_comparator->CompareTo(*(rhs.value().value),
*(lhs.value().value));
+ if (result != 0) {
+ return result;
+ }
+ }
+ assert(lhs.value().sequence_number != rhs.value().sequence_number);
+ return rhs.value().sequence_number < lhs.value().sequence_number ? -1
: 1;
+ };
+ loser_tree_ =
+ std::make_unique<LoserTree>(std::move(readers), first_comparator,
second_comparator);
+}
+
+Result<bool> SortMergeReaderWithLoserTree::Iterator::HasNext() {
+ while (true) {
+ PAIMON_RETURN_NOT_OK(reader_->loser_tree_->AdjustForNextLoop());
+ std::optional<KeyValue> winner = reader_->loser_tree_->PopWinner();
+ if (winner == std::nullopt) {
+ return false;
+ }
+ reader_->merge_function_wrapper_->Reset();
+
PAIMON_RETURN_NOT_OK(reader_->merge_function_wrapper_->Add(std::move(winner.value())));
+ PAIMON_RETURN_NOT_OK(Merge());
+ if (result_ != std::nullopt) {
+ return true;
+ }
+ }
+ return false;
+}
+
+} // namespace paimon
diff --git
a/src/paimon/core/mergetree/compact/sort_merge_reader_with_loser_tree.h
b/src/paimon/core/mergetree/compact/sort_merge_reader_with_loser_tree.h
new file mode 100644
index 0000000..debeee2
--- /dev/null
+++ b/src/paimon/core/mergetree/compact/sort_merge_reader_with_loser_tree.h
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <memory>
+#include <optional>
+#include <utility>
+#include <vector>
+
+#include "paimon/common/utils/fields_comparator.h"
+#include "paimon/core/io/concat_key_value_record_reader.h"
+#include "paimon/core/key_value.h"
+#include "paimon/core/mergetree/compact/loser_tree.h"
+#include "paimon/core/mergetree/compact/merge_function_wrapper.h"
+#include "paimon/core/mergetree/compact/sort_merge_reader.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace paimon {
+class FieldsComparator;
+class KeyValueRecordReader;
+class Metrics;
+
+/// `SortMergeReader` implemented with loser tree. Merge the KeyValue parsed by
+/// `KeyValueRecordReader` and return the iterator of KeyValue
+class SortMergeReaderWithLoserTree : public SortMergeReader {
+ public:
+ SortMergeReaderWithLoserTree(
+ std::vector<std::unique_ptr<KeyValueRecordReader>>&& readers,
+ const std::shared_ptr<FieldsComparator>& user_key_comparator,
+ const std::shared_ptr<FieldsComparator>& user_defined_seq_comparator,
+ const std::shared_ptr<MergeFunctionWrapper<KeyValue>>&
merge_function_wrapper);
+
+ std::shared_ptr<Metrics> GetReaderMetrics() const override {
+ return loser_tree_->GetReaderMetrics();
+ }
+
+ void Close() override {
+ loser_tree_->Close();
+ if (merge_function_wrapper_) {
+ merge_function_wrapper_->Reset();
+ }
+ }
+
+ class Iterator : public SortMergeReader::Iterator {
+ public:
+ explicit Iterator(SortMergeReaderWithLoserTree* reader) :
reader_(reader) {}
+
+ Result<bool> HasNext() override;
+
+ KeyValue&& Next() override {
+ return std::move(result_).value();
+ }
+
+ private:
+ Status Merge() {
+ while (reader_->loser_tree_->PeekWinner() != std::nullopt) {
+ PAIMON_RETURN_NOT_OK(reader_->merge_function_wrapper_->Add(
+ std::move(reader_->loser_tree_->PopWinner().value())));
+ }
+ PAIMON_ASSIGN_OR_RAISE(result_,
reader_->merge_function_wrapper_->GetResult());
+ return Status::OK();
+ }
+
+ private:
+ SortMergeReaderWithLoserTree* reader_;
+ std::optional<KeyValue> result_;
+ };
+
+ Result<std::unique_ptr<SortMergeReader::Iterator>> NextBatch() override {
+ PAIMON_RETURN_NOT_OK(loser_tree_->InitializeIfNeeded());
+ return loser_tree_->PeekWinner() == std::nullopt
+ ? std::unique_ptr<SortMergeReader::Iterator>()
+ : std::make_unique<Iterator>(this);
+ }
+
+ private:
+ std::unique_ptr<LoserTree> loser_tree_;
+ std::shared_ptr<MergeFunctionWrapper<KeyValue>> merge_function_wrapper_;
+};
+} // namespace paimon
diff --git
a/src/paimon/core/mergetree/compact/sort_merge_reader_with_min_heap.cpp
b/src/paimon/core/mergetree/compact/sort_merge_reader_with_min_heap.cpp
new file mode 100644
index 0000000..08629e6
--- /dev/null
+++ b/src/paimon/core/mergetree/compact/sort_merge_reader_with_min_heap.cpp
@@ -0,0 +1,137 @@
+/*
+ * 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/mergetree/compact/sort_merge_reader_with_min_heap.h"
+
+#include "paimon/core/mergetree/compact/merge_function_wrapper.h"
+#include "paimon/status.h"
+
+namespace paimon {
+class InternalRow;
+
+SortMergeReaderWithMinHeap::SortMergeReaderWithMinHeap(
+ std::vector<std::unique_ptr<KeyValueRecordReader>>&& readers,
+ const std::shared_ptr<FieldsComparator>& user_key_comparator,
+ const std::shared_ptr<FieldsComparator>& user_defined_seq_comparator,
+ const std::shared_ptr<MergeFunctionWrapper<KeyValue>>&
merge_function_wrapper)
+ : need_merge_(merge_function_wrapper != nullptr),
+ readers_holder_(std::move(readers)),
+ user_key_comparator_(user_key_comparator),
+ merge_function_wrapper_(merge_function_wrapper),
+ min_heap_(HeapSorter(user_key_comparator, user_defined_seq_comparator)) {
+ next_batch_readers_.reserve(readers_holder_.size());
+ for (auto& reader : readers_holder_) {
+ next_batch_readers_.push_back(reader.get());
+ }
+}
+
+Result<std::unique_ptr<SortMergeReader::Iterator>>
SortMergeReaderWithMinHeap::NextBatch() {
+ for (auto* reader : next_batch_readers_) {
+ while (true) {
+
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<KeyValueRecordReader::Iterator> iterator,
+ reader->NextBatch());
+ if (!iterator) {
+ // no more batches, permanently remove this reader
+ reader->Close();
+ break;
+ }
+ PAIMON_ASSIGN_OR_RAISE(bool has_next, iterator->HasNext());
+ if (has_next) {
+ PAIMON_ASSIGN_OR_RAISE(KeyValue kv, iterator->Next());
+ min_heap_.emplace(std::move(kv), std::move(iterator), reader);
+ break;
+ }
+ }
+ }
+ next_batch_readers_.clear();
+ if (min_heap_.empty()) {
+ return std::unique_ptr<SortMergeReader::Iterator>();
+ }
+ return std::make_unique<SortMergeReaderWithMinHeap::Iterator>(this);
+}
+
+Result<bool> SortMergeReaderWithMinHeap::Iterator::HasNext() {
+ while (true) {
+ PAIMON_ASSIGN_OR_RAISE(bool has_more, NextImpl());
+ if (!reader_->need_merge_) {
+ // no merge: just return every kv in sorted order, possibly with
duplicate keys
+ return has_more;
+ }
+ if (!has_more) {
+ return false;
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::optional<KeyValue> result,
+ reader_->merge_function_wrapper_->GetResult());
+ if (result) {
+ result_ = std::move(result);
+ return true;
+ }
+ }
+}
+
+Result<bool> SortMergeReaderWithMinHeap::Iterator::NextImpl() {
+ assert(reader_->next_batch_readers_.empty());
+ // add previously polled elements back to priority queue
+ for (auto& element : reader_->polled_) {
+ PAIMON_ASSIGN_OR_RAISE(bool updated, element.Update());
+ if (updated) {
+ // still kvs left, add back to priority queue
+ reader_->min_heap_.push(std::move(element));
+ } else {
+ // reach end of batch, clean up
+ reader_->next_batch_readers_.push_back(std::move(element.reader));
+ }
+ }
+ reader_->polled_.clear();
+ if (!reader_->next_batch_readers_.empty()) {
+ return false;
+ }
+
+ if (reader_->min_heap_.empty()) {
+ return Status::Invalid("Min heap is empty. This is a bug.");
+ }
+
+ if (!reader_->need_merge_) {
+ // no merge: only poll the top element, set it as result directly
+ auto& element = const_cast<Element&>(reader_->min_heap_.top());
+ result_ = std::move(element.kv);
+ reader_->polled_.push_back(std::move(element));
+ reader_->min_heap_.pop();
+ return true;
+ }
+
+ reader_->merge_function_wrapper_->Reset();
+ std::shared_ptr<InternalRow> key = reader_->min_heap_.top().kv.key;
+ bool is_first = true;
+
+ // fetch all elements with the same key
+ // note that the same iterator should not produce the same keys, so this
code is correct
+ while (!reader_->min_heap_.empty()) {
+ auto& element = const_cast<Element&>(reader_->min_heap_.top());
+ if (!is_first && reader_->user_key_comparator_->CompareTo(*key,
*(element.kv.key)) != 0) {
+ break;
+ }
+
PAIMON_RETURN_NOT_OK(reader_->merge_function_wrapper_->Add(std::move(element.kv)));
+ reader_->polled_.push_back(std::move(element));
+ reader_->min_heap_.pop();
+ is_first = false;
+ }
+ return true;
+}
+
+} // namespace paimon
diff --git
a/src/paimon/core/mergetree/compact/sort_merge_reader_with_min_heap.h
b/src/paimon/core/mergetree/compact/sort_merge_reader_with_min_heap.h
new file mode 100644
index 0000000..3d99100
--- /dev/null
+++ b/src/paimon/core/mergetree/compact/sort_merge_reader_with_min_heap.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 <cassert>
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <queue>
+#include <utility>
+#include <vector>
+
+#include "paimon/common/metrics/metrics_impl.h"
+#include "paimon/common/utils/fields_comparator.h"
+#include "paimon/core/io/concat_key_value_record_reader.h"
+#include "paimon/core/io/key_value_record_reader.h"
+#include "paimon/core/key_value.h"
+#include "paimon/core/mergetree/compact/merge_function_wrapper.h"
+#include "paimon/core/mergetree/compact/sort_merge_reader.h"
+#include "paimon/result.h"
+
+namespace paimon {
+class Metrics;
+template <typename T>
+class MergeFunctionWrapper;
+
+/// `SortMergeReader` implemented with min-heap. Merge the KeyValue or only
sort the KeyValue parsed
+/// by `KeyValueRecordReader` and return the iterator of KeyValue
+class SortMergeReaderWithMinHeap : public SortMergeReader {
+ public:
+ SortMergeReaderWithMinHeap(
+ std::vector<std::unique_ptr<KeyValueRecordReader>>&& readers,
+ const std::shared_ptr<FieldsComparator>& user_key_comparator,
+ const std::shared_ptr<FieldsComparator>& user_defined_seq_comparator,
+ const std::shared_ptr<MergeFunctionWrapper<KeyValue>>&
merge_function_wrapper);
+
+ class Iterator : public SortMergeReader::Iterator {
+ public:
+ explicit Iterator(SortMergeReaderWithMinHeap* reader) :
reader_(reader) {}
+ Result<bool> HasNext() override;
+ KeyValue&& Next() override {
+ return std::move(result_).value();
+ }
+
+ private:
+ Result<bool> NextImpl();
+
+ private:
+ SortMergeReaderWithMinHeap* reader_;
+ std::optional<KeyValue> result_;
+ };
+
+ Result<std::unique_ptr<SortMergeReader::Iterator>> NextBatch() override;
+
+ void Close() override {
+ for (const auto& reader : readers_holder_) {
+ reader->Close();
+ }
+ if (merge_function_wrapper_) {
+ merge_function_wrapper_->Reset();
+ }
+ }
+
+ std::shared_ptr<Metrics> GetReaderMetrics() const override {
+ return MetricsImpl::CollectReadMetrics(readers_holder_);
+ }
+
+ private:
+ struct Element {
+ Element(KeyValue&& _kv,
std::unique_ptr<KeyValueRecordReader::Iterator>&& _iterator,
+ KeyValueRecordReader* _reader)
+ : kv(std::move(_kv)), reader(_reader),
iterator(std::move(_iterator)) {
+ assert(iterator);
+ assert(reader);
+ }
+
+ Element(Element&& other) noexcept : kv(std::move(other.kv)) {
+ iterator = std::move(other.iterator);
+ reader = other.reader;
+ }
+
+ Element& operator=(Element&& other) noexcept {
+ if (&other == this) {
+ return *this;
+ }
+ kv = std::move(other.kv);
+ iterator = std::move(other.iterator);
+ reader = other.reader;
+ return *this;
+ }
+
+ Result<bool> Update() {
+ PAIMON_ASSIGN_OR_RAISE(bool has_next, iterator->HasNext());
+ if (!has_next) {
+ return false;
+ }
+ PAIMON_ASSIGN_OR_RAISE(KeyValue tmp_kv, iterator->Next());
+ kv = std::move(tmp_kv);
+ return true;
+ }
+
+ public:
+ KeyValue kv;
+ KeyValueRecordReader* reader;
+ std::unique_ptr<KeyValueRecordReader::Iterator> iterator;
+ };
+
+ class HeapSorter {
+ public:
+ HeapSorter(const std::shared_ptr<FieldsComparator>& key_comparator,
+ const std::shared_ptr<FieldsComparator>& seq_comparator)
+ : key_comparator_(key_comparator), seq_comparator_(seq_comparator)
{
+ assert(key_comparator_);
+ }
+ bool operator()(const Element& lhs, const Element& rhs) const {
+ int32_t result = key_comparator_->CompareTo(*(lhs.kv.key),
*(rhs.kv.key));
+ if (result != 0) {
+ return result > 0;
+ }
+ if (seq_comparator_ != nullptr) {
+ int32_t seq_result =
seq_comparator_->CompareTo(*(lhs.kv.value), *(rhs.kv.value));
+ if (seq_result != 0) {
+ return seq_result > 0;
+ }
+ }
+ return lhs.kv.sequence_number > rhs.kv.sequence_number;
+ }
+
+ private:
+ std::shared_ptr<FieldsComparator> key_comparator_;
+ std::shared_ptr<FieldsComparator> seq_comparator_;
+ };
+
+ private:
+ const bool need_merge_;
+ // must hold all readers, as data array is allocated by the pool of data
file reader
+ std::vector<std::unique_ptr<KeyValueRecordReader>> readers_holder_;
+ std::vector<KeyValueRecordReader*> next_batch_readers_;
+ std::shared_ptr<FieldsComparator> user_key_comparator_;
+ std::shared_ptr<MergeFunctionWrapper<KeyValue>> merge_function_wrapper_;
+ std::priority_queue<Element, std::vector<Element>, HeapSorter> min_heap_;
+ std::vector<Element> polled_;
+};
+} // namespace paimon
diff --git a/src/paimon/core/mergetree/drop_delete_reader.h
b/src/paimon/core/mergetree/drop_delete_reader.h
new file mode 100644
index 0000000..0d67caf
--- /dev/null
+++ b/src/paimon/core/mergetree/drop_delete_reader.h
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+#include <memory>
+#include <optional>
+#include <utility>
+
+#include "paimon/common/types/row_kind.h"
+#include "paimon/core/key_value.h"
+#include "paimon/core/mergetree/compact/sort_merge_reader.h"
+#include "paimon/result.h"
+
+namespace paimon {
+class Metrics;
+
+/// A `RecordReader` which drops `KeyValue` that does not meet `RowKind#isAdd`
from
+/// the wrapped reader.
+class DropDeleteReader : public SortMergeReader {
+ public:
+ explicit DropDeleteReader(std::unique_ptr<SortMergeReader>&& reader)
+ : reader_(std::move(reader)) {}
+
+ class Iterator : public SortMergeReader::Iterator {
+ public:
+ explicit Iterator(std::unique_ptr<SortMergeReader::Iterator>&&
iterator)
+ : iterator_(std::move(iterator)) {}
+ Result<bool> HasNext() override {
+ while (true) {
+ PAIMON_ASSIGN_OR_RAISE(bool has_next, iterator_->HasNext());
+ if (!has_next) {
+ return false;
+ }
+ result_ = std::move(iterator_->Next());
+ if (result_.value().value_kind->IsAdd()) {
+ break;
+ }
+ }
+ return true;
+ }
+ KeyValue&& Next() override {
+ return std::move(result_).value();
+ }
+
+ private:
+ std::optional<KeyValue> result_;
+ std::unique_ptr<SortMergeReader::Iterator> iterator_;
+ };
+
+ Result<std::unique_ptr<SortMergeReader::Iterator>> NextBatch() override {
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<SortMergeReader::Iterator> iter,
+ reader_->NextBatch());
+ if (iter == nullptr) {
+ return iter;
+ }
+ return std::make_unique<Iterator>(std::move(iter));
+ }
+
+ std::shared_ptr<Metrics> GetReaderMetrics() const override {
+ return reader_->GetReaderMetrics();
+ }
+
+ void Close() override {
+ reader_->Close();
+ }
+
+ private:
+ std::unique_ptr<SortMergeReader> reader_;
+};
+} // namespace paimon
diff --git a/src/paimon/core/mergetree/drop_delete_reader_test.cpp
b/src/paimon/core/mergetree/drop_delete_reader_test.cpp
new file mode 100644
index 0000000..af09286
--- /dev/null
+++ b/src/paimon/core/mergetree/drop_delete_reader_test.cpp
@@ -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.
+ */
+
+#include "paimon/core/mergetree/drop_delete_reader.h"
+
+#include <cstddef>
+#include <variant>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "paimon/common/data/internal_row.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/status.h"
+#include "paimon/testing/utils/binary_row_generator.h"
+#include "paimon/testing/utils/read_result_collector.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon {
+class Metrics;
+} // namespace paimon
+
+namespace paimon::test {
+class DropDeleteReaderTest : public testing::Test {
+ public:
+ class FakeSortMergeReader : public SortMergeReader {
+ public:
+ explicit FakeSortMergeReader(std::vector<KeyValue>&& data) :
data_(std::move(data)) {}
+
+ class Iterator : public SortMergeReader::Iterator {
+ public:
+ explicit Iterator(FakeSortMergeReader* reader) : reader_(reader) {}
+ Result<bool> HasNext() override {
+ return reader_->iter_ < reader_->data_.size();
+ }
+ KeyValue&& Next() override {
+ return std::move(reader_->data_[reader_->iter_++]);
+ }
+
+ private:
+ FakeSortMergeReader* reader_;
+ };
+
+ Result<std::unique_ptr<SortMergeReader::Iterator>> NextBatch()
override {
+ if (iter_ < data_.size()) {
+ return std::make_unique<Iterator>(this);
+ }
+ return std::unique_ptr<SortMergeReader::Iterator>();
+ }
+
+ std::shared_ptr<Metrics> GetReaderMetrics() const override {
+ return nullptr;
+ }
+
+ void Close() override {}
+
+ private:
+ std::vector<KeyValue> data_;
+ size_t iter_ = 0;
+ };
+};
+
+TEST_F(DropDeleteReaderTest, TestSimple) {
+ auto pool = GetDefaultPool();
+ KeyValue kv1(RowKind::UpdateAfter(), /*sequence_number=*/1, /*level=*/0,
/*key=*/
+ BinaryRowGenerator::GenerateRowPtr({10}, pool.get()),
+ /*value=*/BinaryRowGenerator::GenerateRowPtr({10, 100},
pool.get()));
+ KeyValue kv2(RowKind::Delete(), /*sequence_number=*/2, /*level=*/2,
+ /*key=*/BinaryRowGenerator::GenerateRowPtr({20}, pool.get()),
+ /*value=*/BinaryRowGenerator::GenerateRowPtr({20, 200},
pool.get()));
+ KeyValue kv3(RowKind::Insert(), /*sequence_number=*/3, /*level=*/1,
/*key=*/
+ BinaryRowGenerator::GenerateRowPtr({30}, pool.get()),
+ /*value=*/BinaryRowGenerator::GenerateRowPtr({30, 300},
pool.get()));
+ KeyValue kv4(RowKind::UpdateBefore(), /*sequence_number=*/1, /*level=*/0,
/*key=*/
+ BinaryRowGenerator::GenerateRowPtr({40}, pool.get()),
+ /*value=*/BinaryRowGenerator::GenerateRowPtr({40, 100},
pool.get()));
+ std::vector<KeyValue> kvs;
+ kvs.reserve(4);
+ kvs.push_back(std::move(kv1));
+ kvs.push_back(std::move(kv2));
+ kvs.push_back(std::move(kv3));
+ kvs.push_back(std::move(kv4));
+
+ auto sort_merge_reader =
std::make_unique<FakeSortMergeReader>(std::move(kvs));
+ auto drop_delete_reader =
std::make_unique<DropDeleteReader>(std::move(sort_merge_reader));
+
+ ASSERT_OK_AND_ASSIGN(
+ std::vector<KeyValue> results,
+ (ReadResultCollector::CollectKeyValueResult<SortMergeReader,
SortMergeReader::Iterator>(
+ drop_delete_reader.get())));
+ ASSERT_EQ(results.size(), 2);
+ ASSERT_EQ(results[0].value->GetInt(0), 10);
+ ASSERT_EQ(results[0].value->GetInt(1), 100);
+ ASSERT_EQ(results[1].value->GetInt(0), 30);
+ ASSERT_EQ(results[1].value->GetInt(1), 300);
+}
+} // namespace paimon::test