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 8f83fe1 feat: Migrate merge split read modules (#77)
8f83fe1 is described below
commit 8f83fe1b2867609a31a5791faa26236285343426
Author: lxy <[email protected]>
AuthorDate: Tue Jun 16 09:11:35 2026 +0800
feat: Migrate merge split read modules (#77)
---
.../core/operation/merge_file_split_read.cpp | 537 ++++++++
src/paimon/core/operation/merge_file_split_read.h | 191 +++
.../core/operation/merge_file_split_read_test.cpp | 1296 ++++++++++++++++++++
3 files changed, 2024 insertions(+)
diff --git a/src/paimon/core/operation/merge_file_split_read.cpp
b/src/paimon/core/operation/merge_file_split_read.cpp
new file mode 100644
index 0000000..ca6c1d5
--- /dev/null
+++ b/src/paimon/core/operation/merge_file_split_read.cpp
@@ -0,0 +1,537 @@
+/*
+ * 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/operation/merge_file_split_read.h"
+
+#include <algorithm>
+#include <cassert>
+#include <cstddef>
+#include <map>
+#include <optional>
+#include <set>
+#include <utility>
+
+#include "arrow/c/abi.h"
+#include "arrow/c/bridge.h"
+#include "arrow/type.h"
+#include "fmt/format.h"
+#include "paimon/common/reader/complete_row_kind_batch_reader.h"
+#include "paimon/common/reader/concat_batch_reader.h"
+#include "paimon/common/table/special_fields.h"
+#include "paimon/common/types/data_field.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/object_utils.h"
+#include "paimon/core/core_options.h"
+#include "paimon/core/deletionvectors/apply_deletion_vector_batch_reader.h"
+#include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
+#include "paimon/core/deletionvectors/deletion_vector.h"
+#include "paimon/core/io/async_key_value_projection_reader.h"
+#include "paimon/core/io/concat_key_value_record_reader.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/io/key_value_data_file_record_reader.h"
+#include "paimon/core/io/key_value_projection_consumer.h"
+#include "paimon/core/io/key_value_projection_reader.h"
+#include "paimon/core/mergetree/compact/interval_partition.h"
+#include "paimon/core/mergetree/compact/lookup_merge_function.h"
+#include "paimon/core/mergetree/compact/merge_function.h"
+#include "paimon/core/mergetree/compact/partial_update_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/core/mergetree/drop_delete_reader.h"
+#include "paimon/core/mergetree/sorted_run.h"
+#include "paimon/core/operation/internal_read_context.h"
+#include "paimon/core/options/merge_engine.h"
+#include "paimon/core/options/sort_engine.h"
+#include "paimon/core/schema/schema_manager.h"
+#include "paimon/core/schema/table_schema.h"
+#include "paimon/core/table/bucket_mode.h"
+#include "paimon/core/table/source/data_split_impl.h"
+#include "paimon/core/utils/file_store_path_factory.h"
+#include "paimon/core/utils/primary_key_table_utils.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/predicate/predicate_utils.h"
+#include "paimon/reader/file_batch_reader.h"
+#include "paimon/table/source/data_split.h"
+#include "paimon/utils/roaring_bitmap32.h"
+
+namespace paimon {
+class BinaryRow;
+class DataFilePathFactory;
+class Executor;
+struct KeyValue;
+template <typename T>
+class MergeFunctionWrapper;
+
+Result<std::unique_ptr<MergeFileSplitRead>> MergeFileSplitRead::Create(
+ const std::shared_ptr<FileStorePathFactory>& path_factory,
+ const std::shared_ptr<InternalReadContext>& context,
+ const std::shared_ptr<MemoryPool>& memory_pool, const
std::shared_ptr<Executor>& executor) {
+ const auto& core_options = context->GetCoreOptions();
+ const auto& table_schema = context->GetTableSchema();
+ assert(table_schema);
+ // value_schema is the schema of member value in KeyValue Object
+ std::shared_ptr<arrow::Schema> value_schema;
+ // read_schema is the read schema for format file reader (e.g., includes
_SEQUENCE_NUMBER)
+ std::shared_ptr<arrow::Schema> read_schema;
+ // comparator of member key in KeyValue object
+ std::shared_ptr<FieldsComparator> key_comparator;
+ // comparator of user-defined sequence fields in member value of KeyValue
object
+ std::shared_ptr<FieldsComparator> user_defined_seq_comparator;
+
+ PAIMON_RETURN_NOT_OK(GenerateKeyValueReadSchema(
+ *table_schema, core_options, context->GetReadSchema(), &value_schema,
&read_schema,
+ &key_comparator, &user_defined_seq_comparator));
+
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Predicate> predicate_for_keys,
+ GenerateKeyPredicates(context->GetPredicate(),
*table_schema));
+
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema> key_schema,
+ table_schema->TrimmedPrimaryKeySchema());
+
+ // projection is the mapping from value_schema in KeyValue object to
raw_read_schema
+ std::vector<int32_t> projection;
+ projection.reserve(context->GetReadSchema()->num_fields());
+ bool project_sequence_number =
+ core_options.RowTrackingEnabled() ||
core_options.KeyValueSequenceNumberEnabled();
+ for (const auto& field : context->GetReadSchema()->fields()) {
+ if (field->name() == SpecialFields::SequenceNumber().Name() &&
project_sequence_number) {
+
projection.push_back(KeyValueProjectionConsumer::kSequenceNumberProjection);
+ continue;
+ }
+ if (field->name() == SpecialFields::ValueKind().Name()) {
+
projection.push_back(KeyValueProjectionConsumer::kValueKindProjection);
+ continue;
+ }
+ auto src_field_idx = value_schema->GetFieldIndex(field->name());
+ if (src_field_idx < 0) {
+ return Status::Invalid(
+ fmt::format("Field '{}' not found or duplicate in value
schema", field->name()));
+ }
+ projection.push_back(src_field_idx);
+ }
+
+ return std::unique_ptr<MergeFileSplitRead>(new MergeFileSplitRead(
+ path_factory, context,
+ std::make_unique<SchemaManager>(core_options.GetFileSystem(),
context->GetPath(),
+ context->GetCoreOptions().GetBranch()),
+ key_schema, value_schema, read_schema, projection, key_comparator,
+ user_defined_seq_comparator, predicate_for_keys, memory_pool,
executor));
+}
+
+Result<std::unique_ptr<BatchReader>> MergeFileSplitRead::CreateReader(
+ const std::shared_ptr<Split>& split) {
+ auto data_split = std::dynamic_pointer_cast<DataSplitImpl>(split);
+ if (!data_split) {
+ return Status::Invalid("cannot cast split to data_split in
MergeFileSplitRead");
+ }
+ if (!data_split->BeforeFiles().empty()) {
+ return Status::Invalid("this read cannot accept split with before
files.");
+ }
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<DataFilePathFactory> data_file_path_factory,
+ path_factory_->CreateDataFilePathFactory(data_split->Partition(),
data_split->Bucket()));
+ std::unique_ptr<BatchReader> batch_reader;
+ if (data_split->IsStreaming() || data_split->Bucket() ==
BucketModeDefine::POSTPONE_BUCKET) {
+ PAIMON_ASSIGN_OR_RAISE(
+ batch_reader,
+ CreateNoMergeReader(data_split,
/*only_filter_key=*/data_split->IsStreaming(),
+ data_file_path_factory));
+ } else {
+ PAIMON_ASSIGN_OR_RAISE(batch_reader, CreateMergeReader(data_split,
data_file_path_factory));
+ }
+ return
std::make_unique<CompleteRowKindBatchReader>(std::move(batch_reader), pool_);
+}
+
+void MergeFileSplitRead::SetMergeFunctionWrapper(
+ const std::shared_ptr<MergeFunctionWrapper<KeyValue>>&
merge_function_wrapper) {
+ merge_function_wrapper_ = merge_function_wrapper;
+}
+
+Result<std::shared_ptr<MergeFunctionWrapper<KeyValue>>>
+MergeFileSplitRead::GetMergeFunctionWrapper() {
+ if (!merge_function_wrapper_) {
+ // In deletion vector mode, streaming data split or postpone bucket
mode, we don't need
+ // to use merge function. Even if the merge function in CoreOptions is
not supported, it
+ // should not affect data reading. So we create
merge_function_wrapper_ lazily, to avoid
+ // raise errors when creating MergeFileSplitRead at the beginning.
+ PAIMON_ASSIGN_OR_RAISE(
+ merge_function_wrapper_,
+ CreateMergeFunctionWrapper(options_, context_->GetTableSchema(),
value_schema_));
+ }
+ return merge_function_wrapper_;
+}
+
+Result<std::shared_ptr<MergeFunctionWrapper<KeyValue>>>
+MergeFileSplitRead::CreateMergeFunctionWrapper(const CoreOptions& core_options,
+ const
std::shared_ptr<TableSchema>& table_schema,
+ const
std::shared_ptr<arrow::Schema>& value_schema) {
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<MergeFunction> merge_function,
+ PrimaryKeyTableUtils::CreateMergeFunction(
+ value_schema, table_schema->PrimaryKeys(),
core_options));
+ if (core_options.NeedLookup() && core_options.GetMergeEngine() !=
MergeEngine::FIRST_ROW) {
+ // don't wrap first row, it is already OK
+ merge_function =
std::make_unique<LookupMergeFunction>(std::move(merge_function));
+ }
+ return
std::make_shared<ReducerMergeFunctionWrapper>(std::move(merge_function));
+}
+
+Result<std::unique_ptr<FileBatchReader>>
MergeFileSplitRead::ApplyIndexAndDvReaderIfNeeded(
+ std::unique_ptr<FileBatchReader>&& file_reader, const
std::shared_ptr<DataFileMeta>& file,
+ const std::shared_ptr<arrow::Schema>& data_schema,
+ const std::shared_ptr<arrow::Schema>& read_schema, const
std::shared_ptr<Predicate>& predicate,
+ DeletionVector::Factory dv_factory, const
std::optional<std::vector<Range>>& ranges,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) const {
+ // merge read does not use index
+ std::shared_ptr<DeletionVector> deletion_vector;
+ if (dv_factory) {
+ PAIMON_ASSIGN_OR_RAISE(deletion_vector, dv_factory(file->file_name));
+ }
+
+ const RoaringBitmap32* deletion = nullptr;
+ if (auto* bitmap_dv =
dynamic_cast<BitmapDeletionVector*>(deletion_vector.get())) {
+ deletion = bitmap_dv->GetBitmap();
+ }
+
+ std::optional<RoaringBitmap32> actual_selection;
+ if (deletion) {
+ actual_selection = *deletion;
+ PAIMON_ASSIGN_OR_RAISE(uint64_t num_rows,
file_reader->GetNumberOfRows());
+ actual_selection.value().Flip(0, num_rows);
+ }
+
+ ::ArrowSchema c_read_schema;
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*read_schema,
&c_read_schema));
+
+ PAIMON_RETURN_NOT_OK(file_reader->SetReadSchema(&c_read_schema, predicate,
actual_selection));
+
+ if (!file_reader->SupportPreciseBitmapSelection() && actual_selection) {
+ return
std::make_unique<ApplyDeletionVectorBatchReader>(std::move(file_reader),
+
deletion_vector);
+ }
+ if (deletion_vector && !deletion && !deletion_vector->IsEmpty()) {
+ // TODO(xinyu.lxy): if deletion vector is bitmap64, use
ApplyBitmapIndexBatchReader to
+ // filter result
+ return Status::NotImplemented("Only support BitmapDeletionVector");
+ }
+ return std::move(file_reader);
+}
+
+Result<std::unique_ptr<BatchReader>> MergeFileSplitRead::CreateMergeReader(
+ const std::shared_ptr<DataSplitImpl>& data_split,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) {
+ auto dv_factory = DeletionVector::CreateFactory(options_.GetFileSystem(),
+
CreateDeletionFileMap(*data_split), pool_);
+
+ std::vector<std::vector<SortedRun>> sections =
+ IntervalPartition(data_split->DataFiles(),
key_comparator_).Partition();
+ std::vector<std::unique_ptr<BatchReader>> batch_readers;
+ batch_readers.reserve(sections.size());
+ // no overlap through multiple sections
+ for (const auto& section : sections) {
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<BatchReader> projection_reader,
+ CreateReaderForSection(section,
data_split->Partition(), dv_factory,
+ data_file_path_factory));
+ batch_readers.push_back(std::move(projection_reader));
+ }
+ auto concat_batch_reader =
std::make_unique<ConcatBatchReader>(std::move(batch_readers), pool_);
+ return
AbstractSplitRead::ApplyPredicateFilterIfNeeded(std::move(concat_batch_reader),
+
context_->GetPredicate());
+}
+
+Result<std::unique_ptr<BatchReader>> MergeFileSplitRead::CreateNoMergeReader(
+ const std::shared_ptr<DataSplitImpl>& data_split, bool only_filter_key,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) const {
+ auto dv_factory = DeletionVector::CreateFactory(options_.GetFileSystem(),
+
CreateDeletionFileMap(*data_split), pool_);
+
+ // create read schema without extra fields (e.g., completed key, sequence
fields)
+ auto row_kind_field =
DataField::ConvertDataFieldToArrowField(SpecialFields::ValueKind());
+
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Schema>
read_schema,
+ raw_read_schema_->AddField(0,
row_kind_field));
+ PAIMON_ASSIGN_OR_RAISE(
+ std::vector<std::unique_ptr<FileBatchReader>> raw_file_readers,
+ CreateRawFileReaders(data_split->Partition(), data_split->DataFiles(),
read_schema,
+ only_filter_key ? predicate_for_keys_ :
context_->GetPredicate(),
+ dv_factory, /*row_ranges=*/{},
data_file_path_factory));
+
+ auto raw_readers =
+
ObjectUtils::MoveVector<std::unique_ptr<BatchReader>>(std::move(raw_file_readers));
+ auto concat_batch_reader =
std::make_unique<ConcatBatchReader>(std::move(raw_readers), pool_);
+ return
AbstractSplitRead::ApplyPredicateFilterIfNeeded(std::move(concat_batch_reader),
+
context_->GetPredicate());
+}
+
+MergeFileSplitRead::MergeFileSplitRead(
+ const std::shared_ptr<FileStorePathFactory>& path_factory,
+ const std::shared_ptr<InternalReadContext>& context,
+ std::unique_ptr<SchemaManager>&& schema_manager,
+ const std::shared_ptr<arrow::Schema>& key_schema,
+ const std::shared_ptr<arrow::Schema>& value_schema,
+ const std::shared_ptr<arrow::Schema>& read_schema, const
std::vector<int32_t>& projection,
+ const std::shared_ptr<FieldsComparator>& key_comparator,
+ const std::shared_ptr<FieldsComparator>& user_defined_seq_comparator,
+ const std::shared_ptr<Predicate>& predicate_for_keys,
+ const std::shared_ptr<MemoryPool>& memory_pool, const
std::shared_ptr<Executor>& executor)
+ : AbstractSplitRead(path_factory, context, std::move(schema_manager),
memory_pool, executor),
+ key_schema_(key_schema),
+ value_schema_(value_schema),
+ read_schema_(read_schema),
+ projection_(projection),
+ key_comparator_(key_comparator),
+ user_defined_seq_comparator_(user_defined_seq_comparator),
+ predicate_for_keys_(predicate_for_keys) {}
+
+Status MergeFileSplitRead::GenerateKeyValueReadSchema(
+ const TableSchema& table_schema, const CoreOptions& options,
+ const std::shared_ptr<arrow::Schema>& raw_read_schema,
+ std::shared_ptr<arrow::Schema>* value_schema,
std::shared_ptr<arrow::Schema>* read_schema,
+ std::shared_ptr<FieldsComparator>* key_comparator,
+ std::shared_ptr<FieldsComparator>* sequence_fields_comparator) {
+ PAIMON_ASSIGN_OR_RAISE(std::vector<DataField> trimmed_key_fields,
+ table_schema.TrimmedPrimaryKeyFields());
+ PAIMON_ASSIGN_OR_RAISE(*key_comparator,
FieldsComparator::Create(trimmed_key_fields,
+
/*is_ascending_order=*/true));
+ const auto& table_fields = table_schema.Fields();
+ auto table_fields_schema =
DataField::ConvertDataFieldsToArrowSchema(table_fields);
+ if (table_fields_schema->Equals(raw_read_schema)) {
+ // Short-circuit: if raw_read_schema is the same as the table schema,
+ // use the table schema field order directly (for compact process).
+ *value_schema = table_fields_schema;
+ // sequence_fields_comparator
+ PAIMON_ASSIGN_OR_RAISE(
+ *sequence_fields_comparator,
+ PrimaryKeyTableUtils::CreateSequenceFieldsComparator(table_fields,
options));
+ *read_schema =
SpecialFields::CompleteSequenceAndValueKindField(*value_schema);
+ return Status::OK();
+ }
+
+ // 1. add user raw read schema to need_fields
+ PAIMON_ASSIGN_OR_RAISE(std::vector<DataField> need_fields,
+
DataField::ConvertArrowSchemaToDataFields(raw_read_schema));
+ if (options.RowTrackingEnabled() ||
options.KeyValueSequenceNumberEnabled()) {
+ // _SEQUENCE_NUMBER is carried by KeyValue metadata, not by
KeyValue.value. Remove it before
+ // splitting key/value fields so the value projection can inject it
from KeyValue directly.
+ need_fields.erase(std::remove_if(need_fields.begin(),
need_fields.end(),
+ [](const DataField& field) {
+ return field.Name() ==
+
SpecialFields::SequenceNumber().Name();
+ }),
+ need_fields.end());
+ }
+ // _VALUE_KIND is also carried by KeyValue metadata. Keep it out of
KeyValue.value so the
+ // projection can inject the actual row kind instead of resolving it as a
table field.
+ need_fields.erase(std::remove_if(need_fields.begin(), need_fields.end(),
+ [](const DataField& field) {
+ return field.Name() ==
SpecialFields::ValueKind().Name();
+ }),
+ need_fields.end());
+ // 2. add user defined sequence field to need_fields
+ PAIMON_RETURN_NOT_OK(CompleteSequenceField(table_schema, options,
&need_fields));
+ if (options.GetMergeEngine() == MergeEngine::PARTIAL_UPDATE) {
+ // add sequence group fields for partial update
+ std::map<std::string, std::vector<std::string>>
value_field_to_seq_group_field;
+ std::set<std::string> seq_group_key_set;
+
PAIMON_RETURN_NOT_OK(PartialUpdateMergeFunction::ParseSequenceGroupFields(
+ options, &value_field_to_seq_group_field, &seq_group_key_set));
+
PAIMON_RETURN_NOT_OK(PartialUpdateMergeFunction::CompleteSequenceGroupFields(
+ table_schema, value_field_to_seq_group_field, &need_fields));
+ }
+ // 3. split need_fields to key and non-key fields
+ std::vector<DataField> key_fields;
+ std::vector<DataField> non_key_fields;
+ PAIMON_ASSIGN_OR_RAISE(std::vector<std::string> trimmed_key_names,
+ table_schema.TrimmedPrimaryKeys());
+ PAIMON_RETURN_NOT_OK(
+ SplitKeyAndNonKeyField(trimmed_key_names, need_fields, &key_fields,
&non_key_fields));
+
+ // 4. construct value fields: key fields are put before non-key fields
+ std::vector<DataField> value_fields;
+ value_fields.insert(value_fields.end(), key_fields.begin(),
key_fields.end());
+ value_fields.insert(value_fields.end(), non_key_fields.begin(),
non_key_fields.end());
+ *value_schema = DataField::ConvertDataFieldsToArrowSchema(value_fields);
+ // 5. create sequence field comparator
+ PAIMON_ASSIGN_OR_RAISE(
+ *sequence_fields_comparator,
+ PrimaryKeyTableUtils::CreateSequenceFieldsComparator(value_fields,
options));
+ // 6. construct actual read fields: special + key + non-key value
+ std::vector<DataField> read_fields = {SpecialFields::SequenceNumber(),
+ SpecialFields::ValueKind()};
+ read_fields.insert(read_fields.end(), trimmed_key_fields.begin(),
trimmed_key_fields.end());
+ read_fields.insert(read_fields.end(), non_key_fields.begin(),
non_key_fields.end());
+ *read_schema = DataField::ConvertDataFieldsToArrowSchema(read_fields);
+ return Status::OK();
+}
+
+Status MergeFileSplitRead::SplitKeyAndNonKeyField(
+ const std::vector<std::string>& trimmed_key_fields, const
std::vector<DataField>& read_fields,
+ std::vector<DataField>* key_fields, std::vector<DataField>*
non_key_fields) {
+ for (const auto& field : read_fields) {
+ auto iter = std::find(trimmed_key_fields.begin(),
trimmed_key_fields.end(), field.Name());
+ if (iter == trimmed_key_fields.end()) {
+ non_key_fields->push_back(field);
+ } else {
+ key_fields->push_back(field);
+ }
+ }
+ return Status::OK();
+}
+
+Status MergeFileSplitRead::CompleteSequenceField(const TableSchema&
table_schema,
+ const CoreOptions& options,
+ std::vector<DataField>*
non_key_fields) {
+ auto sequence_field_names = options.GetSequenceField();
+ if (sequence_field_names.empty()) {
+ return Status::OK();
+ }
+
+ std::set<std::string> non_key_field_names;
+ for (const auto& field : *non_key_fields) {
+ non_key_field_names.insert(field.Name());
+ }
+
+ for (const auto& seq_field_name : sequence_field_names) {
+ auto iter = non_key_field_names.find(seq_field_name);
+ if (iter == non_key_field_names.end()) {
+ // force add sequence fields
+ PAIMON_ASSIGN_OR_RAISE(DataField seq_field,
table_schema.GetField(seq_field_name));
+ non_key_fields->push_back(seq_field);
+ }
+ }
+ return Status::OK();
+}
+
+Result<std::shared_ptr<Predicate>> MergeFileSplitRead::GenerateKeyPredicates(
+ const std::shared_ptr<Predicate>& predicate, const TableSchema&
table_schema) {
+ // extract predicates only contain trimmed key fields
+ if (!predicate) {
+ return std::shared_ptr<Predicate>();
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::vector<std::string> trimmed_key_fields,
+ table_schema.TrimmedPrimaryKeys());
+ std::set<std::string> non_primary_keys;
+ for (const auto& field_name : table_schema.FieldNames()) {
+ auto iter = std::find(trimmed_key_fields.begin(),
trimmed_key_fields.end(), field_name);
+ if (iter == trimmed_key_fields.end()) {
+ non_primary_keys.insert(field_name);
+ }
+ }
+ return PredicateUtils::ExcludePredicateWithFields(predicate,
non_primary_keys);
+}
+
+Result<std::unique_ptr<BatchReader>>
MergeFileSplitRead::CreateReaderForSection(
+ const std::vector<SortedRun>& section, const BinaryRow& partition,
+ DeletionVector::Factory dv_factory,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) {
+ // with overlap in one section
+ std::shared_ptr<Predicate> predicate;
+ if (section.size() > 1) {
+ predicate = predicate_for_keys_;
+ } else {
+ predicate = context_->GetPredicate();
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<SortMergeReader> sort_merge_reader,
+ CreateSortMergeReaderForSection(section, partition,
dv_factory,
+ predicate,
data_file_path_factory,
+
/*drop_delete=*/!force_keep_delete_));
+ // KeyValueProjectionReader converts KeyValue objects to arrow array
according to projection
+ if (!context_->EnableMultiThreadRowToBatch()) {
+ return KeyValueProjectionReader::Create(std::move(sort_merge_reader),
raw_read_schema_,
+ projection_,
options_.GetReadBatchSize(), pool_);
+ }
+ int32_t thread_number = context_->GetRowToBatchThreadNumber();
+ assert(thread_number > 0);
+ return std::make_unique<AsyncKeyValueProjectionReader>(
+ std::move(sort_merge_reader), raw_read_schema_, projection_,
options_.GetReadBatchSize(),
+ thread_number, pool_);
+}
+
+Result<std::unique_ptr<SortMergeReader>>
MergeFileSplitRead::CreateSortMergeReaderForSection(
+ const std::vector<SortedRun>& section, const BinaryRow& partition,
+ DeletionVector::Factory dv_factory, const std::shared_ptr<Predicate>&
predicate,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory, bool
drop_delete) {
+ // with overlap in one section
+ std::vector<std::unique_ptr<KeyValueRecordReader>> record_readers;
+ record_readers.reserve(section.size());
+ for (const auto& run : section) {
+ // no overlap in a run
+ PAIMON_ASSIGN_OR_RAISE(
+ std::unique_ptr<KeyValueRecordReader> run_reader,
+ CreateReaderForRun(partition, run, dv_factory, predicate,
data_file_path_factory));
+ record_readers.emplace_back(std::move(run_reader));
+ }
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<SortMergeReader> sort_merge_reader,
+ CreateSortMergeReader(std::move(record_readers)));
+ if (drop_delete) {
+ sort_merge_reader =
std::make_unique<DropDeleteReader>(std::move(sort_merge_reader));
+ }
+ return sort_merge_reader;
+}
+
+Result<std::unique_ptr<KeyValueRecordReader>>
MergeFileSplitRead::CreateReaderForRun(
+ const BinaryRow& partition, const SortedRun& sorted_run,
DeletionVector::Factory dv_factory,
+ const std::shared_ptr<Predicate>& predicate,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory) const {
+ // no overlap in a run
+ const auto& data_files = sorted_run.Files();
+ PAIMON_ASSIGN_OR_RAISE(
+ std::vector<std::unique_ptr<FileBatchReader>> raw_file_readers,
+ CreateRawFileReaders(partition, data_files, read_schema_, predicate,
dv_factory,
+ /*row_ranges=*/{}, data_file_path_factory));
+
+ assert(data_files.size() == raw_file_readers.size());
+ // KeyValueDataFileRecordReader converts arrow array from format reader to
KeyValue objects
+ std::vector<std::unique_ptr<KeyValueRecordReader>> file_record_readers;
+ file_record_readers.reserve(data_files.size());
+ for (size_t i = 0; i < data_files.size(); i++) {
+
file_record_readers.push_back(std::make_unique<KeyValueDataFileRecordReader>(
+ std::move(raw_file_readers[i]), key_schema_, value_schema_,
data_files[i]->level,
+ pool_));
+ }
+ return
std::make_unique<ConcatKeyValueRecordReader>(std::move(file_record_readers));
+}
+
+Result<std::unique_ptr<SortMergeReader>>
MergeFileSplitRead::CreateSortMergeReader(
+ std::vector<std::unique_ptr<KeyValueRecordReader>>&& record_readers) {
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<MergeFunctionWrapper<KeyValue>>
merge_function_wrapper,
+ GetMergeFunctionWrapper());
+ auto sort_engine = options_.GetSortEngine();
+ if (sort_engine == SortEngine::MIN_HEAP) {
+ return std::make_unique<SortMergeReaderWithMinHeap>(
+ std::move(record_readers), key_comparator_,
user_defined_seq_comparator_,
+ merge_function_wrapper);
+ } else if (sort_engine == SortEngine::LOSER_TREE) {
+ return std::make_unique<SortMergeReaderWithLoserTree>(
+ std::move(record_readers), key_comparator_,
user_defined_seq_comparator_,
+ merge_function_wrapper);
+ }
+ return Status::Invalid("only support loser-tree or min-heap sort engine");
+}
+
+Result<bool> MergeFileSplitRead::Match(const std::shared_ptr<Split>& split,
+ bool force_keep_delete) const {
+ // TODO(yonghao.fyh): just pass split impl
+ auto split_impl = dynamic_cast<DataSplitImpl*>(split.get());
+ if (split_impl == nullptr) {
+ return Status::Invalid("unexpected error, split cast to impl failed");
+ }
+ return split_impl->BeforeFiles().empty();
+}
+
+} // namespace paimon
diff --git a/src/paimon/core/operation/merge_file_split_read.h
b/src/paimon/core/operation/merge_file_split_read.h
new file mode 100644
index 0000000..f00883b
--- /dev/null
+++ b/src/paimon/core/operation/merge_file_split_read.h
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "paimon/common/utils/fields_comparator.h"
+#include "paimon/core/io/concat_key_value_record_reader.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/io/key_value_record_reader.h"
+#include "paimon/core/mergetree/compact/interval_partition.h"
+#include "paimon/core/mergetree/compact/merge_function_wrapper.h"
+#include "paimon/core/mergetree/compact/sort_merge_reader.h"
+#include "paimon/core/mergetree/sorted_run.h"
+#include "paimon/core/operation/abstract_split_read.h"
+#include "paimon/core/table/source/deletion_file.h"
+#include "paimon/predicate/predicate.h"
+#include "paimon/reader/batch_reader.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace arrow {
+class Schema;
+} // namespace arrow
+
+namespace paimon {
+class BinaryRow;
+class CoreOptions;
+class DataField;
+class DataFilePathFactory;
+class DataSplit;
+class DataSplitImpl;
+class Executor;
+class FieldsComparator;
+class FileBatchReader;
+class FileStorePathFactory;
+class InternalReadContext;
+class MemoryPool;
+class SchemaManager;
+class SortedRun;
+class TableSchema;
+struct DataFileMeta;
+struct DeletionFile;
+struct KeyValue;
+template <typename T>
+class MergeFunctionWrapper;
+
+/// If the class name below is enclosed in parentheses, it might be present in
the read path;
+/// otherwise, it must be present in the read path.
+///
+/// Readers Overview: (ConcatBatchReader across
+/// splits)->CompleteRowKindBatchReader->(PredicateBatchReader)
+/// ->ConcatBatchReader across no overlapped
+/// files->KeyValueProjectionReader/AsyncKeyValueProjectionReader
+///
->DropDeleteReader->SortMergeReader->ConcatKeyValueRecordReader->KeyValueDataFileRecordReader
+///
->FieldMappingReader->(ApplyDeletionVectorBatchReader)->(DelegatingPrefetchReader)
+/// ->(PrefetchFileBatchReader)->FormatReader
+class MergeFileSplitRead : public AbstractSplitRead {
+ public:
+ static Result<std::unique_ptr<MergeFileSplitRead>> Create(
+ const std::shared_ptr<FileStorePathFactory>& path_factory,
+ const std::shared_ptr<InternalReadContext>& context,
+ const std::shared_ptr<MemoryPool>& memory_pool, const
std::shared_ptr<Executor>& executor);
+
+ Result<std::unique_ptr<BatchReader>> CreateReader(const
std::shared_ptr<Split>& split) override;
+
+ Result<bool> Match(const std::shared_ptr<Split>& split, bool
force_keep_delete) const override;
+
+ void ForceKeepDelete(bool force_keep_delete) {
+ force_keep_delete_ = force_keep_delete;
+ }
+
+ Result<std::unique_ptr<FileBatchReader>> ApplyIndexAndDvReaderIfNeeded(
+ std::unique_ptr<FileBatchReader>&& file_reader, const
std::shared_ptr<DataFileMeta>& file,
+ const std::shared_ptr<arrow::Schema>& data_schema,
+ const std::shared_ptr<arrow::Schema>& read_schema,
+ const std::shared_ptr<Predicate>& predicate, DeletionVector::Factory
dv_factory,
+ const std::optional<std::vector<Range>>& ranges,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory)
const override;
+
+ Result<std::unique_ptr<SortMergeReader>> CreateSortMergeReaderForSection(
+ const std::vector<SortedRun>& section, const BinaryRow& partition,
+ DeletionVector::Factory dv_factory, const std::shared_ptr<Predicate>&
predicate,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory,
bool drop_delete);
+
+ std::shared_ptr<FileStorePathFactory> GetPathFactory() const {
+ return path_factory_;
+ }
+
+ std::shared_ptr<arrow::Schema> GetValueSchema() const {
+ return value_schema_;
+ }
+
+ void SetMergeFunctionWrapper(
+ const std::shared_ptr<MergeFunctionWrapper<KeyValue>>&
merge_function_wrapper);
+
+ private:
+ Result<std::unique_ptr<BatchReader>> CreateMergeReader(
+ const std::shared_ptr<DataSplitImpl>& data_split,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory);
+
+ Result<std::unique_ptr<BatchReader>> CreateNoMergeReader(
+ const std::shared_ptr<DataSplitImpl>& data_split, bool only_filter_key,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory)
const;
+
+ Result<std::unique_ptr<BatchReader>> CreateReaderForSection(
+ const std::vector<SortedRun>& section, const BinaryRow& partition,
+ DeletionVector::Factory dv_factory,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory);
+
+ Result<std::unique_ptr<KeyValueRecordReader>> CreateReaderForRun(
+ const BinaryRow& partition, const SortedRun& sorted_run,
DeletionVector::Factory dv_factory,
+ const std::shared_ptr<Predicate>& predicate,
+ const std::shared_ptr<DataFilePathFactory>& data_file_path_factory)
const;
+
+ Result<std::unique_ptr<SortMergeReader>> CreateSortMergeReader(
+ std::vector<std::unique_ptr<KeyValueRecordReader>>&& record_readers);
+
+ Result<std::shared_ptr<MergeFunctionWrapper<KeyValue>>>
GetMergeFunctionWrapper();
+
+ MergeFileSplitRead(const std::shared_ptr<FileStorePathFactory>&
path_factory,
+ const std::shared_ptr<InternalReadContext>& context,
+ std::unique_ptr<SchemaManager>&& schema_manager,
+ const std::shared_ptr<arrow::Schema>& key_schema,
+ const std::shared_ptr<arrow::Schema>& value_schema,
+ const std::shared_ptr<arrow::Schema>& read_schema,
+ const std::vector<int32_t>& projection,
+ const std::shared_ptr<FieldsComparator>& key_comparator,
+ const std::shared_ptr<FieldsComparator>&
user_defined_seq_comparator,
+ const std::shared_ptr<Predicate>& predicate_for_keys,
+ const std::shared_ptr<MemoryPool>& memory_pool,
+ const std::shared_ptr<Executor>& executor);
+
+ static Result<std::shared_ptr<MergeFunctionWrapper<KeyValue>>>
CreateMergeFunctionWrapper(
+ const CoreOptions& core_options, const std::shared_ptr<TableSchema>&
table_schema,
+ const std::shared_ptr<arrow::Schema>& value_schema);
+
+ static Status GenerateKeyValueReadSchema(
+ const TableSchema& table_schema, const CoreOptions& options,
+ const std::shared_ptr<arrow::Schema>& raw_read_schema,
+ std::shared_ptr<arrow::Schema>* value_schema,
std::shared_ptr<arrow::Schema>* read_schema,
+ std::shared_ptr<FieldsComparator>* key_comparator,
+ std::shared_ptr<FieldsComparator>* sequence_fields_comparator);
+
+ static Status SplitKeyAndNonKeyField(const std::vector<std::string>&
trimmed_key_fields,
+ const std::vector<DataField>&
read_fields,
+ std::vector<DataField>* key_fields,
+ std::vector<DataField>*
non_key_fields);
+
+ static Status CompleteSequenceField(const TableSchema& table_schema, const
CoreOptions& options,
+ std::vector<DataField>*
non_key_fields);
+
+ static Result<std::shared_ptr<Predicate>> GenerateKeyPredicates(
+ const std::shared_ptr<Predicate>& predicate, const TableSchema&
table_schema);
+
+ private:
+ // schema of key member in KeyValue object (trimmed pk)
+ std::shared_ptr<arrow::Schema> key_schema_;
+ // schema of value member in KeyValue object
+ std::shared_ptr<arrow::Schema> value_schema_;
+ // actual read schema, e.g., complete all key fields, user defined
sequence fields
+ std::shared_ptr<arrow::Schema> read_schema_;
+ std::vector<int32_t> projection_;
+ // merge_function_wrapper is lazy created, use through
GetMergeFunctionWrapper()
+ std::shared_ptr<MergeFunctionWrapper<KeyValue>> merge_function_wrapper_;
+ std::shared_ptr<FieldsComparator> key_comparator_;
+ std::shared_ptr<FieldsComparator> user_defined_seq_comparator_;
+ std::shared_ptr<Predicate> predicate_for_keys_;
+ bool force_keep_delete_ = false;
+};
+} // namespace paimon
diff --git a/src/paimon/core/operation/merge_file_split_read_test.cpp
b/src/paimon/core/operation/merge_file_split_read_test.cpp
new file mode 100644
index 0000000..70383b2
--- /dev/null
+++ b/src/paimon/core/operation/merge_file_split_read_test.cpp
@@ -0,0 +1,1296 @@
+/*
+ * 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/operation/merge_file_split_read.h"
+
+#include <cstddef>
+#include <map>
+#include <optional>
+#include <ostream>
+#include <tuple>
+#include <utility>
+#include <variant>
+
+#include "arrow/api.h"
+#include "arrow/array/array_base.h"
+#include "arrow/c/abi.h"
+#include "arrow/ipc/json_simple.h"
+#include "gtest/gtest.h"
+#include "paimon/common/data/binary_row.h"
+#include "paimon/common/factories/io_hook.h"
+#include "paimon/common/reader/concat_batch_reader.h"
+#include "paimon/common/table/special_fields.h"
+#include "paimon/common/types/data_field.h"
+#include "paimon/common/utils/fields_comparator.h"
+#include "paimon/common/utils/scope_guard.h"
+#include "paimon/core/core_options.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/manifest/file_source.h"
+#include "paimon/core/operation/internal_read_context.h"
+#include "paimon/core/schema/schema_manager.h"
+#include "paimon/core/schema/table_schema.h"
+#include "paimon/core/table/source/data_split_impl.h"
+#include "paimon/core/utils/file_store_path_factory.h"
+#include "paimon/data/timestamp.h"
+#include "paimon/defs.h"
+#include "paimon/executor.h"
+#include "paimon/fs/local/local_file_system.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/metrics.h"
+#include "paimon/predicate/literal.h"
+#include "paimon/predicate/predicate_builder.h"
+#include "paimon/read_context.h"
+#include "paimon/table/source/data_split.h"
+#include "paimon/testing/utils/binary_row_generator.h"
+#include "paimon/testing/utils/io_exception_helper.h"
+#include "paimon/testing/utils/read_result_collector.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon {
+class FileSystem;
+} // namespace paimon
+
+namespace paimon::test {
+// Parameter: min_heap/loser_tree; enable/disable IO prefetch; enable/disable
multi thread row to
+// batch
+class MergeFileSplitReadTest : public ::testing::Test,
+ public
::testing::WithParamInterface<std::tuple<bool, bool, bool>> {
+ void SetUp() override {}
+ void TearDown() override {}
+
+ void CheckResult(const std::shared_ptr<arrow::ChunkedArray>& result,
+ const std::shared_ptr<arrow::ChunkedArray>& expected,
+ const std::shared_ptr<arrow::Schema>& schema) const {
+ if (!std::get<2>(GetParam())) {
+ ASSERT_TRUE(result->ApproxEquals(*expected)) << result->ToString();
+ return;
+ }
+ ASSERT_OK_AND_ASSIGN(auto sorted_result,
ReadResultCollector::SortArray(result, schema));
+ ASSERT_OK_AND_ASSIGN(auto sorted_expected,
+ ReadResultCollector::SortArray(expected, schema));
+ ASSERT_TRUE(sorted_result->ApproxEquals(*sorted_expected))
+ << sorted_result->ToString() << std::endl
+ << sorted_expected->ToString();
+ }
+
+ std::shared_ptr<InternalReadContext> CreateInternalReadContext(
+ const std::shared_ptr<ReadContext>& read_context, int32_t schema_id =
0) {
+ SchemaManager schema_manager(fs_, read_context->GetPath());
+ EXPECT_OK_AND_ASSIGN(auto table_schema,
schema_manager.ReadSchema(schema_id));
+ EXPECT_OK_AND_ASSIGN(auto context,
InternalReadContext::Create(read_context, table_schema,
+
read_context->GetOptions()));
+ return context;
+ }
+
+ void AddOptions(ReadContextBuilder* context_builder) const {
+ auto [use_min_heap, enable_io_prefetch,
enable_multi_thread_row_to_batch] = GetParam();
+ if (use_min_heap) {
+ context_builder->AddOption(Options::SORT_ENGINE, "min-heap");
+ } else {
+ context_builder->AddOption(Options::SORT_ENGINE, "loser-tree");
+ }
+ if (enable_io_prefetch) {
+
context_builder->AddOption("test.enable-adaptive-prefetch-strategy", "false");
+ context_builder->EnablePrefetch(true);
+ context_builder->SetPrefetchBatchCount(/*batch_count=*/3);
+ } else {
+ context_builder->EnablePrefetch(false);
+ }
+ if (enable_multi_thread_row_to_batch) {
+ context_builder->EnableMultiThreadRowToBatch(true);
+ context_builder->SetRowToBatchThreadNumber(4);
+ } else {
+ context_builder->EnableMultiThreadRowToBatch(false);
+ }
+ }
+
+ // for table pk_table_with_mor
+ std::vector<std::shared_ptr<DataSplit>> PrepareDataSplit() const {
+ auto meta1_1 = std::make_shared<DataFileMeta>(
+ "data-c80ccf0f-6387-4cbc-8889-ade8cef54c43-0.parquet",
/*file_size=*/3346,
+ /*row_count=*/4,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({0, 0}, pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({1, 1}, pool_.get()),
+ /*key_stats=*/BinaryRowGenerator::GenerateStats({0, 0}, {1, 1},
{0, 0}, pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats(
+ {0, 0, 0, 0, "apple", "!", static_cast<double>(10), false},
+ {1, 1, 0, 0, "driver", "you", 13.3, true}, {0, 0, 0, 0, 0, 0,
0, 0}, pool_.get()),
+ /*min_sequence_number=*/0, /*max_sequence_number=*/3,
/*schema_id=*/0,
+ /*level=*/0,
/*extra_files=*/std::vector<std::optional<std::string>>(),
+ /*creation_time=*/Timestamp(1735149279565ll, 0),
+ /*delete_row_count=*/0, /*embedded_index=*/nullptr,
FileSource::Append(),
+ /*value_stats_cols=*/std::nullopt, /*external_path=*/std::nullopt,
+ /*first_row_id=*/std::nullopt,
+ /*write_cols=*/std::nullopt);
+ auto meta1_2 = std::make_shared<DataFileMeta>(
+ "data-c80ccf0f-6387-4cbc-8889-ade8cef54c43-1.parquet",
/*file_size=*/3370,
+ /*row_count=*/4,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({0, 0}, pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({1, 2}, pool_.get()),
+ /*key_stats=*/BinaryRowGenerator::GenerateStats({0, 0}, {1, 2},
{0, 0}, pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats(
+ {0, 0, 0, 0, "abandon", "!", static_cast<double>(110), false},
+ {1, 2, 0, 0, "driver", "see", 112.2, true}, {0, 0, 0, 0, 0, 0,
0, 0}, pool_.get()),
+ /*min_sequence_number=*/4, /*max_sequence_number=*/7,
/*schema_id=*/0,
+ /*level=*/0,
/*extra_files=*/std::vector<std::optional<std::string>>(),
+ /*creation_time=*/Timestamp(1735149279917ll, 0),
+ /*delete_row_count=*/0, /*embedded_index=*/nullptr,
FileSource::Append(),
+ /*value_stats_cols=*/std::nullopt, /*external_path=*/std::nullopt,
+ /*first_row_id=*/std::nullopt,
+ /*write_cols=*/std::nullopt);
+ auto meta1_3 = std::make_shared<DataFileMeta>(
+ "data-c80ccf0f-6387-4cbc-8889-ade8cef54c43-2.parquet",
/*file_size=*/3252,
+ /*row_count=*/1,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({100, 200},
pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({100, 200},
pool_.get()),
+ /*key_stats=*/
+ BinaryRowGenerator::GenerateStats({100, 200}, {100, 200}, {0, 0},
pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats(
+ {100, 200, 0, 0, std::string("max"), std::string("number"),
140.4, false},
+ {100, 200, 0, 0, std::string("max"), std::string("number"),
140.4, false},
+ {0, 0, 0, 0, 0, 0, 0, 0}, pool_.get()),
+ /*min_sequence_number=*/8, /*max_sequence_number=*/8,
/*schema_id=*/0,
+ /*level=*/0,
/*extra_files=*/std::vector<std::optional<std::string>>(),
+ /*creation_time=*/Timestamp(1735230606999ll, 0),
+ /*delete_row_count=*/0, /*embedded_index=*/nullptr,
FileSource::Append(),
+ /*value_stats_cols=*/std::nullopt, /*external_path=*/std::nullopt,
+ /*first_row_id=*/std::nullopt,
+ /*write_cols=*/std::nullopt);
+ DataSplitImpl::Builder builder1(BinaryRowGenerator::GenerateRow({0,
0}, pool_.get()),
+ /*bucket=*/0, /*bucket_path=*/
+ paimon::test::GetDataDir() +
+ "/parquet/pk_table_with_mor.db/"
+
"pk_table_with_mor/p0=0/p1=0/bucket-0",
+ {meta1_1, meta1_2, meta1_3});
+ EXPECT_OK_AND_ASSIGN(
+ auto data_split1,
+
builder1.WithSnapshot(3).IsStreaming(false).RawConvertible(false).Build());
+ auto meta2_1 = std::make_shared<DataFileMeta>(
+ "data-24f8588c-d950-4e44-9d99-a023ea65a136-0.parquet",
/*file_size=*/3245,
+ /*row_count=*/1,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({0, 1}, pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({0, 1}, pool_.get()),
+ /*key_stats=*/BinaryRowGenerator::GenerateStats({0, 1}, {0, 1},
{0, 0}, pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats(
+ {0, 1, 0, 1, "mouse", "you", static_cast<double>(30), false},
+ {0, 1, 0, 1, "mouse", "you", static_cast<double>(30), false},
+ {0, 0, 0, 0, 0, 0, 0, 0}, pool_.get()),
+ /*min_sequence_number=*/0, /*max_sequence_number=*/0,
/*schema_id=*/0,
+ /*level=*/0,
/*extra_files=*/std::vector<std::optional<std::string>>(),
+ /*creation_time=*/Timestamp(1735149279951ll, 0),
+ /*delete_row_count=*/0, /*embedded_index=*/nullptr,
FileSource::Append(),
+ /*value_stats_cols=*/std::nullopt, /*external_path=*/std::nullopt,
+ /*first_row_id=*/std::nullopt,
+ /*write_cols=*/std::nullopt);
+ auto meta2_2 = std::make_shared<DataFileMeta>(
+ "data-24f8588c-d950-4e44-9d99-a023ea65a136-1.parquet",
/*file_size=*/3229,
+ /*row_count=*/1,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({0, 1}, pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({0, 1}, pool_.get()),
+ /*key_stats=*/BinaryRowGenerator::GenerateStats({0, 1}, {0, 1},
{0, 0}, pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats(
+ {0, 1, 0, 1, "zoo", "you", static_cast<double>(130), false},
+ {0, 1, 0, 1, "zoo", "you", static_cast<double>(130), false},
+ {0, 0, 0, 0, 0, 0, 0, 0}, pool_.get()),
+ /*min_sequence_number=*/1, /*max_sequence_number=*/1,
/*schema_id=*/0,
+ /*level=*/0,
/*extra_files=*/std::vector<std::optional<std::string>>(),
+ /*creation_time=*/Timestamp(1735149279612ll, 0),
+ /*delete_row_count=*/0, /*embedded_index=*/nullptr,
FileSource::Append(),
+ /*value_stats_cols=*/std::nullopt, /*external_path=*/std::nullopt,
+ /*first_row_id=*/std::nullopt,
+ /*write_cols=*/std::nullopt);
+ DataSplitImpl::Builder builder2(BinaryRowGenerator::GenerateRow({0,
1}, pool_.get()),
+ /*bucket=*/0, /*bucket_path=*/
+ paimon::test::GetDataDir() +
+ "/parquet/pk_table_with_mor.db/"
+
"pk_table_with_mor/p0=0/p1=1/bucket-0",
+ {meta2_1, meta2_2});
+ EXPECT_OK_AND_ASSIGN(
+ auto data_split2,
+
builder2.WithSnapshot(3).IsStreaming(false).RawConvertible(false).Build());
+
+ auto meta3_1 = std::make_shared<DataFileMeta>(
+ "data-184f2304-49fd-4916-ba07-037757e904eb-0.parquet",
/*file_size=*/3259,
+ /*row_count=*/1,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({0, 0}, pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({0, 0}, pool_.get()),
+ /*key_stats=*/BinaryRowGenerator::GenerateStats({0, 0}, {0, 0},
{0, 0}, pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats(
+ {0, 0, 1, 0, "elephant", "hi", static_cast<double>(120),
false},
+ {0, 0, 1, 0, "elephant", "hi", static_cast<double>(120),
false},
+ {0, 0, 0, 0, 0, 0, 0, 0}, pool_.get()),
+ /*min_sequence_number=*/0, /*max_sequence_number=*/0,
/*schema_id=*/0,
+ /*level=*/0,
/*extra_files=*/std::vector<std::optional<std::string>>(),
+ /*creation_time=*/Timestamp(1735149271981ll, 0),
+ /*delete_row_count=*/0, /*embedded_index=*/nullptr,
FileSource::Append(),
+ /*value_stats_cols=*/std::nullopt, /*external_path=*/std::nullopt,
+ /*first_row_id=*/std::nullopt,
+ /*write_cols=*/std::nullopt);
+ auto meta3_2 = std::make_shared<DataFileMeta>(
+ "data-184f2304-49fd-4916-ba07-037757e904eb-1.parquet",
/*file_size=*/3259,
+ /*row_count=*/1,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({0, 0}, pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({0, 0}, pool_.get()),
+ /*key_stats=*/BinaryRowGenerator::GenerateStats({0, 0}, {0, 0},
{0, 0}, pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats(
+ {0, 0, 1, 0, "elephant", "hi", static_cast<double>(20), true},
+ {0, 0, 1, 0, "elephant", "hi", static_cast<double>(20), true},
+ {0, 0, 0, 0, 0, 0, 0, 0}, pool_.get()),
+ /*min_sequence_number=*/1, /*max_sequence_number=*/1,
/*schema_id=*/0,
+ /*level=*/0,
/*extra_files=*/std::vector<std::optional<std::string>>(),
+ /*creation_time=*/Timestamp(1735149279651ll, 0),
+ /*delete_row_count=*/0, /*embedded_index=*/nullptr,
FileSource::Append(),
+ /*value_stats_cols=*/std::nullopt, /*external_path=*/std::nullopt,
+ /*first_row_id=*/std::nullopt,
+ /*write_cols=*/std::nullopt);
+ DataSplitImpl::Builder builder3(BinaryRowGenerator::GenerateRow({1,
0}, pool_.get()),
+ /*bucket=*/0, /*bucket_path=*/
+ paimon::test::GetDataDir() +
+ "/parquet/pk_table_with_mor.db/"
+
"pk_table_with_mor/p0=1/p1=0/bucket-0",
+ {meta3_1, meta3_2});
+ EXPECT_OK_AND_ASSIGN(
+ auto data_split3,
+
builder3.WithSnapshot(3).IsStreaming(false).RawConvertible(false).Build());
+ return {data_split1, data_split2, data_split3};
+ }
+
+ // for table pk_table_partial_update
+ std::vector<std::shared_ptr<DataSplit>> PrepareDataSplit2() const {
+ auto meta1_1 = std::make_shared<DataFileMeta>(
+ "data-d03e13e5-5e2e-463a-b53a-8d44e4dc9141-0.parquet",
+ /*file_size=*/2554, /*row_count=*/
+ 3,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({0, 1}, pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({1, 1}, pool_.get()),
+ /*key_stats=*/BinaryRowGenerator::GenerateStats({0, 0}, {1, 1},
{0, 0}, pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats({0, 0, 2.0, false,
std::string("apple")},
+ {1, 1, 2.0, true,
std::string("banana")},
+ {0, 0, 2, 0, 1}, pool_.get()),
+ /*min_sequence_number=*/0, /*max_sequence_number=*/2,
/*schema_id=*/0,
+ /*level=*/0,
/*extra_files=*/std::vector<std::optional<std::string>>(),
+ /*creation_time=*/Timestamp(1736793059256ll, 0),
+ /*delete_row_count=*/0, /*embedded_index=*/nullptr,
FileSource::Append(),
+ /*value_stats_cols=*/std::nullopt, /*external_path=*/std::nullopt,
+ /*first_row_id=*/std::nullopt,
+ /*write_cols=*/std::nullopt);
+ auto meta1_2 = std::make_shared<DataFileMeta>(
+ "data-d03e13e5-5e2e-463a-b53a-8d44e4dc9141-1.parquet",
+ /*file_size=*/2623, /*row_count=*/
+ 5,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({0, 1}, pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({2, 2}, pool_.get()),
+ /*key_stats=*/BinaryRowGenerator::GenerateStats({0, 0}, {2, 2},
{0, 0}, pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats({0, 0, 100.0, false,
std::string("new_apple")},
+ {2, 2, 144.4, true,
std::string("orange")},
+ {0, 0, 0, 0, 3}, pool_.get()),
+ /*min_sequence_number=*/3, /*max_sequence_number=*/7,
/*schema_id=*/0,
+ /*level=*/0,
/*extra_files=*/std::vector<std::optional<std::string>>(),
+ /*creation_time=*/Timestamp(1736793059526ll, 0),
+ /*delete_row_count=*/2, /*embedded_index=*/nullptr,
FileSource::Append(),
+ /*value_stats_cols=*/std::nullopt, /*external_path=*/std::nullopt,
+ /*first_row_id=*/std::nullopt,
+ /*write_cols=*/std::nullopt);
+ DataSplitImpl::Builder builder1(
+ /*partition=*/BinaryRow::EmptyRow(),
+ /*bucket=*/0, /*bucket_path=*/
+ paimon::test::GetDataDir() +
+
"/parquet/pk_table_partial_update.db/pk_table_partial_update/bucket-0",
+ {meta1_1, meta1_2});
+ EXPECT_OK_AND_ASSIGN(
+ auto data_split1,
+
builder1.WithSnapshot(2).IsStreaming(false).RawConvertible(false).Build());
+
+ return {data_split1};
+ }
+
+ Result<std::unique_ptr<BatchReader>> CreateReader(
+ const std::shared_ptr<InternalReadContext>& internal_context,
+ const std::vector<std::shared_ptr<DataSplit>>& data_splits) {
+ const auto& core_options = internal_context->GetCoreOptions();
+ const auto& table_schema = internal_context->GetTableSchema();
+ auto arrow_schema =
DataField::ConvertDataFieldsToArrowSchema(table_schema->Fields());
+ EXPECT_OK_AND_ASSIGN(std::vector<std::string> external_paths,
+ core_options.CreateExternalPaths());
+ EXPECT_OK_AND_ASSIGN(std::optional<std::string>
global_index_external_path,
+ core_options.CreateGlobalIndexExternalPath());
+
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<FileStorePathFactory> path_factory,
+ FileStorePathFactory::Create(
+ internal_context->GetPath(), arrow_schema,
table_schema->PartitionKeys(),
+ core_options.GetPartitionDefaultName(),
core_options.GetFileFormat()->Identifier(),
+ core_options.DataFilePrefix(),
core_options.LegacyPartitionNameEnabled(),
+ external_paths, global_index_external_path,
core_options.IndexFileInDataFileDir(),
+ pool_));
+ PAIMON_ASSIGN_OR_RAISE(auto split_read,
+ MergeFileSplitRead::Create(path_factory,
std::move(internal_context),
+ pool_, executor_));
+ std::vector<std::unique_ptr<BatchReader>> batch_readers;
+ batch_readers.reserve(data_splits.size());
+ for (const auto& split : data_splits) {
+ PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<BatchReader> reader,
+ split_read->CreateReader(split));
+ batch_readers.emplace_back(std::move(reader));
+ }
+ return std::make_unique<ConcatBatchReader>(std::move(batch_readers),
pool_);
+ }
+
+ private:
+ std::shared_ptr<MemoryPool> pool_ = GetDefaultPool();
+ std::shared_ptr<FileSystem> fs_ = std::make_shared<LocalFileSystem>();
+ std::shared_ptr<Executor> executor_ =
CreateDefaultExecutor(/*thread_count=*/4);
+};
+
+// test GenerateKeyValueReadSchema with user define fields
+TEST_F(MergeFileSplitReadTest, TestGenerateKeyValueReadSchema) {
+ std::string table_path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ auto schema_manager = std::make_unique<SchemaManager>(fs_, table_path);
+ ASSERT_OK_AND_ASSIGN(auto table_schema,
schema_manager->ReadSchema(/*schema_id=*/0));
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::SEQUENCE_FIELD,
"s0,s1"},
+ {Options::MERGE_ENGINE,
"deduplicate"},
+ {Options::SORT_ENGINE,
"min-heap"},
+ {Options::IGNORE_DELETE,
"true"}}));
+ std::vector<DataField> raw_read_fields = {
+ DataField(1, arrow::field("k1", arrow::int32(), /*nullable=*/false)),
+ DataField(3, arrow::field("p1", arrow::int32(), /*nullable=*/false)),
+ DataField(5, arrow::field("s1", arrow::utf8())),
+ DataField(6, arrow::field("v0", arrow::float64())),
+ DataField(7, arrow::field("v1", arrow::boolean()))};
+ auto raw_read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(raw_read_schema);
+
+ std::shared_ptr<arrow::Schema> value_schema;
+ std::shared_ptr<arrow::Schema> read_schema;
+ std::shared_ptr<FieldsComparator> key_comparator;
+ std::shared_ptr<FieldsComparator> sequence_fields_comparator;
+ ASSERT_OK(MergeFileSplitRead::GenerateKeyValueReadSchema(
+ *table_schema, options, raw_read_schema, &value_schema, &read_schema,
&key_comparator,
+ &sequence_fields_comparator));
+
+ // check result
+ std::vector<DataField> expected_value_fields = {
+ DataField(1, arrow::field("k1", arrow::int32(), /*nullable=*/false)),
+ DataField(3, arrow::field("p1", arrow::int32(), /*nullable=*/false)),
+ DataField(5, arrow::field("s1", arrow::utf8())),
+ DataField(6, arrow::field("v0", arrow::float64())),
+ DataField(7, arrow::field("v1", arrow::boolean())),
+ DataField(4, arrow::field("s0", arrow::utf8()))};
+ auto expected_value_schema =
DataField::ConvertDataFieldsToArrowSchema(expected_value_fields);
+
+ ASSERT_OK_AND_ASSIGN(auto result_fields,
+
DataField::ConvertArrowSchemaToDataFields(value_schema));
+ ASSERT_OK_AND_ASSIGN(auto expected_fields,
+
DataField::ConvertArrowSchemaToDataFields(expected_value_schema));
+ for (size_t i = 0; i < expected_fields.size(); i++) {
+ EXPECT_EQ(result_fields[i], expected_fields[i]);
+ EXPECT_OK_AND_ASSIGN(std::string result_str,
result_fields[i].ToJsonString());
+ EXPECT_OK_AND_ASSIGN(std::string expected_str,
expected_fields[i].ToJsonString());
+ EXPECT_EQ(result_str, expected_str);
+ }
+ ASSERT_TRUE(value_schema->Equals(*expected_value_schema));
+
+ std::vector<DataField> expected_read_data_fields = {
+ SpecialFields::SequenceNumber(),
+ SpecialFields::ValueKind(),
+ DataField(0, arrow::field("k0", arrow::int32(), /*nullable=*/false)),
+ DataField(1, arrow::field("k1", arrow::int32(), /*nullable=*/false)),
+ DataField(3, arrow::field("p1", arrow::int32(), /*nullable=*/false)),
+ DataField(5, arrow::field("s1", arrow::utf8())),
+ DataField(6, arrow::field("v0", arrow::float64())),
+ DataField(7, arrow::field("v1", arrow::boolean())),
+ DataField(4, arrow::field("s0", arrow::utf8()))};
+ auto expected_read_schema =
+ DataField::ConvertDataFieldsToArrowSchema(expected_read_data_fields);
+ ASSERT_TRUE(read_schema->Equals(*expected_read_schema));
+
+ std::vector<int32_t> expected_sort_key_fields = {0, 1};
+ ASSERT_EQ(key_comparator->sort_fields_, expected_sort_key_fields);
+ ASSERT_EQ(key_comparator->is_ascending_order_, true);
+
+ std::vector<int32_t> expected_sort_seq_fields = {5, 2};
+ ASSERT_EQ(sequence_fields_comparator->sort_fields_,
expected_sort_seq_fields);
+ ASSERT_EQ(sequence_fields_comparator->is_ascending_order_, true);
+}
+
+// test GenerateKeyValueReadSchema without user define fields
+TEST_F(MergeFileSplitReadTest, TestGenerateKeyValueReadSchema1) {
+ std::string table_path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ auto schema_manager = std::make_unique<SchemaManager>(fs_, table_path);
+ ASSERT_OK_AND_ASSIGN(auto table_schema,
schema_manager->ReadSchema(/*schema_id=*/0));
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::MERGE_ENGINE,
"deduplicate"},
+ {Options::SORT_ENGINE,
"min-heap"},
+ {Options::IGNORE_DELETE,
"true"}}));
+ std::vector<DataField> raw_read_fields = {
+ DataField(1, arrow::field("k1", arrow::int32(), /*nullable=*/false)),
+ DataField(3, arrow::field("p1", arrow::int32(), /*nullable=*/false)),
+ DataField(5, arrow::field("s1", arrow::utf8())),
+ DataField(6, arrow::field("v0", arrow::float64())),
+ DataField(7, arrow::field("v1", arrow::boolean()))};
+ auto raw_read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(raw_read_schema);
+
+ std::shared_ptr<arrow::Schema> value_schema;
+ std::shared_ptr<arrow::Schema> read_schema;
+ std::shared_ptr<FieldsComparator> key_comparator;
+ std::shared_ptr<FieldsComparator> sequence_fields_comparator;
+ ASSERT_OK(MergeFileSplitRead::GenerateKeyValueReadSchema(
+ *table_schema, options, raw_read_schema, &value_schema, &read_schema,
&key_comparator,
+ &sequence_fields_comparator));
+
+ // check result
+ std::vector<DataField> expected_value_fields = {
+ DataField(1, arrow::field("k1", arrow::int32(), /*nullable=*/false)),
+ DataField(3, arrow::field("p1", arrow::int32(), /*nullable=*/false)),
+ DataField(5, arrow::field("s1", arrow::utf8())),
+ DataField(6, arrow::field("v0", arrow::float64())),
+ DataField(7, arrow::field("v1", arrow::boolean()))};
+ auto expected_value_schema =
DataField::ConvertDataFieldsToArrowSchema(expected_value_fields);
+ ASSERT_TRUE(value_schema->Equals(*expected_value_schema));
+
+ std::vector<DataField> expected_read_data_fields = {
+ SpecialFields::SequenceNumber(),
+ SpecialFields::ValueKind(),
+ DataField(0, arrow::field("k0", arrow::int32(), /*nullable=*/false)),
+ DataField(1, arrow::field("k1", arrow::int32(), /*nullable=*/false)),
+ DataField(3, arrow::field("p1", arrow::int32(), /*nullable=*/false)),
+ DataField(5, arrow::field("s1", arrow::utf8())),
+ DataField(6, arrow::field("v0", arrow::float64())),
+ DataField(7, arrow::field("v1", arrow::boolean()))};
+ auto expected_read_schema =
+ DataField::ConvertDataFieldsToArrowSchema(expected_read_data_fields);
+ ASSERT_TRUE(read_schema->Equals(*expected_read_schema));
+
+ std::vector<int32_t> expected_sort_key_fields = {0, 1};
+ ASSERT_EQ(key_comparator->sort_fields_, expected_sort_key_fields);
+ ASSERT_EQ(key_comparator->is_ascending_order_, true);
+
+ ASSERT_FALSE(sequence_fields_comparator);
+}
+
+// test GenerateKeyValueReadSchema with compaction mode that raw_read_schema
equals table_schema
+TEST_F(MergeFileSplitReadTest, TestGenerateKeyValueReadSchema2) {
+ std::string table_path =
+ paimon::test::GetDataDir() +
"parquet/pk_table_with_mor.db/pk_table_with_mor";
+ auto schema_manager = std::make_unique<SchemaManager>(fs_, table_path);
+ ASSERT_OK_AND_ASSIGN(auto table_schema,
schema_manager->ReadSchema(/*schema_id=*/0));
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::SEQUENCE_FIELD,
"s0,s1"},
+ {Options::MERGE_ENGINE,
"deduplicate"},
+ {Options::SORT_ENGINE,
"min-heap"},
+ {Options::IGNORE_DELETE,
"true"}}));
+ auto raw_read_schema =
DataField::ConvertDataFieldsToArrowSchema(table_schema->Fields());
+ ASSERT_TRUE(raw_read_schema);
+
+ std::shared_ptr<arrow::Schema> value_schema;
+ std::shared_ptr<arrow::Schema> read_schema;
+ std::shared_ptr<FieldsComparator> key_comparator;
+ std::shared_ptr<FieldsComparator> sequence_fields_comparator;
+ ASSERT_OK(MergeFileSplitRead::GenerateKeyValueReadSchema(
+ *table_schema, options, raw_read_schema, &value_schema, &read_schema,
&key_comparator,
+ &sequence_fields_comparator));
+
+ // check result
+ auto expected_value_schema =
DataField::ConvertDataFieldsToArrowSchema(table_schema->Fields());
+ ASSERT_OK_AND_ASSIGN(auto result_fields,
+
DataField::ConvertArrowSchemaToDataFields(value_schema));
+ ASSERT_OK_AND_ASSIGN(auto expected_fields,
+
DataField::ConvertArrowSchemaToDataFields(expected_value_schema));
+ for (size_t i = 0; i < expected_fields.size(); i++) {
+ EXPECT_EQ(result_fields[i], expected_fields[i]);
+ EXPECT_OK_AND_ASSIGN(std::string result_str,
result_fields[i].ToJsonString());
+ EXPECT_OK_AND_ASSIGN(std::string expected_str,
expected_fields[i].ToJsonString());
+ EXPECT_EQ(result_str, expected_str);
+ }
+ ASSERT_TRUE(value_schema->Equals(*expected_value_schema));
+
+ auto expected_read_schema =
+
SpecialFields::CompleteSequenceAndValueKindField(expected_value_schema);
+ ASSERT_TRUE(read_schema->Equals(*expected_read_schema));
+
+ std::vector<int32_t> expected_sort_key_fields = {0, 1};
+ ASSERT_EQ(key_comparator->sort_fields_, expected_sort_key_fields);
+ ASSERT_EQ(key_comparator->is_ascending_order_, true);
+
+ std::vector<int32_t> expected_sort_seq_fields = {4, 5};
+ ASSERT_EQ(sequence_fields_comparator->sort_fields_,
expected_sort_seq_fields);
+ ASSERT_EQ(sequence_fields_comparator->is_ascending_order_, true);
+}
+
+// test no predicate
+TEST_F(MergeFileSplitReadTest, TestGenerateKeyPredicates) {
+ std::string table_path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ auto schema_manager = std::make_unique<SchemaManager>(fs_, table_path);
+ ASSERT_OK_AND_ASSIGN(auto table_schema,
schema_manager->ReadSchema(/*schema_id=*/0));
+ ASSERT_OK_AND_ASSIGN(
+ std::shared_ptr<Predicate> predicate_result,
+ MergeFileSplitRead::GenerateKeyPredicates(/*predicate=*/nullptr,
*table_schema));
+ ASSERT_FALSE(predicate_result);
+}
+
+// test exist primary predicate
+TEST_F(MergeFileSplitReadTest, TestGenerateKeyPredicates1) {
+ std::string table_path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ auto schema_manager = std::make_unique<SchemaManager>(fs_, table_path);
+ ASSERT_OK_AND_ASSIGN(auto table_schema,
schema_manager->ReadSchema(/*schema_id=*/0));
+
+ ASSERT_OK_AND_ASSIGN(
+ auto predicate_result,
+ PredicateBuilder::And({PredicateBuilder::Equal(/*field_index=*/0,
/*field_name=*/"k0",
+ FieldType::INT,
Literal(3)),
+ PredicateBuilder::Equal(/*field_index=*/2,
/*field_name=*/"p0",
+ FieldType::INT,
Literal(5))}));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> result,
+ MergeFileSplitRead::GenerateKeyPredicates(
+ /*predicate=*/predicate_result, *table_schema));
+ ASSERT_OK_AND_ASSIGN(auto expected_key_predicate,
+ PredicateBuilder::And({PredicateBuilder::Equal(
+ /*field_index=*/0, /*field_name=*/"k0",
FieldType::INT, Literal(3))}));
+ ASSERT_EQ(*result, *expected_key_predicate);
+}
+
+// test non-primary predicate
+TEST_F(MergeFileSplitReadTest, TestGenerateKeyPredicates2) {
+ std::string table_path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ auto schema_manager = std::make_unique<SchemaManager>(fs_, table_path);
+ ASSERT_OK_AND_ASSIGN(auto table_schema,
schema_manager->ReadSchema(/*schema_id=*/0));
+
+ ASSERT_OK_AND_ASSIGN(
+ auto predicate,
+ PredicateBuilder::And({PredicateBuilder::Equal(/*field_index=*/3,
/*field_name=*/"p1",
+ FieldType::INT,
Literal(3)),
+ PredicateBuilder::Equal(/*field_index=*/2,
/*field_name=*/"p0",
+ FieldType::INT,
Literal(5))}));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> result,
+ MergeFileSplitRead::GenerateKeyPredicates(
+ /*predicate=*/predicate, *table_schema));
+ ASSERT_FALSE(result);
+}
+
+TEST_P(MergeFileSplitReadTest, TestSimple) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ ReadContextBuilder context_builder(path);
+
+ std::vector<DataField> raw_read_fields = {DataField(1, arrow::field("k1",
arrow::int32())),
+ DataField(3, arrow::field("p1",
arrow::int32())),
+ DataField(5, arrow::field("s1",
arrow::utf8())),
+ DataField(6, arrow::field("v0",
arrow::float64())),
+ DataField(7, arrow::field("v1",
arrow::boolean()))};
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+
+ context_builder.SetReadSchema({"k1", "p1", "s1", "v0", "v1"});
+ context_builder.SetOptions({{Options::SEQUENCE_FIELD, "s0,s1"},
+ {Options::MERGE_ENGINE, "deduplicate"},
+ {Options::IGNORE_DELETE, "true"}});
+ AddOptions(&context_builder);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+
+ auto internal_context = CreateInternalReadContext(read_context);
+
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<MergeFileSplitRead> split_read,
+ MergeFileSplitRead::Create(/*path_factory=*/nullptr, internal_context,
pool_, executor_));
+ auto data_splits = PrepareDataSplit();
+
+ // test split read match
+ {
+ ASSERT_OK_AND_ASSIGN(bool matched,
+ split_read->Match(data_splits[0],
/*force_keep_delete=*/false));
+ ASSERT_TRUE(matched);
+ }
+ {
+ auto fake_data_split = PrepareDataSplit()[0];
+ auto split_impl = dynamic_cast<DataSplitImpl*>(fake_data_split.get());
+ split_impl->before_files_ = split_impl->data_files_;
+ ASSERT_OK_AND_ASSIGN(bool matched,
+ split_read->Match(fake_data_split,
/*force_keep_delete=*/false));
+ ASSERT_FALSE(matched);
+ }
+
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
data_splits));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ auto fields_with_row_kind = read_schema->fields();
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
+
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow::struct_(fields_with_row_kind),
{R"([
+ [0, 0, 0, "see", 110.0, false],
+ [0, 1, 0, "you", 11.1, false],
+ [0, 0, 0, "later", 12.2, true],
+ [0, 1, 0, "!", 13.3, false],
+ [0, 2, 0, "!", 13.3, false],
+ [0, 200, 0, "number",140.4, false],
+ [0, 1, 1, "you", 130.0, false],
+ [0, 0, 0, "hi", 120.0, false]
+ ])"},
+ &expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckResult(result_array, expected_array, read_schema);
+}
+
+TEST_P(MergeFileSplitReadTest, TestLookUp) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ ReadContextBuilder context_builder(path);
+
+ std::vector<DataField> raw_read_fields = {DataField(1, arrow::field("k1",
arrow::int32())),
+ DataField(3, arrow::field("p1",
arrow::int32())),
+ DataField(5, arrow::field("s1",
arrow::utf8())),
+ DataField(6, arrow::field("v0",
arrow::float64())),
+ DataField(7, arrow::field("v1",
arrow::boolean()))};
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+
+ context_builder.SetReadSchema({"k1", "p1", "s1", "v0", "v1"});
+ context_builder.SetOptions({{Options::SEQUENCE_FIELD, "s0,s1"},
+ {Options::MERGE_ENGINE, "deduplicate"},
+ {Options::IGNORE_DELETE, "true"},
+ {Options::FORCE_LOOKUP, "true"}});
+ AddOptions(&context_builder);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+
+ auto internal_context = CreateInternalReadContext(read_context);
+
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
PrepareDataSplit()));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ auto fields_with_row_kind = read_schema->fields();
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
+
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow::struct_(fields_with_row_kind),
{R"([
+ [0, 0, 0, "see", 110.0, false],
+ [0, 1, 0, "you", 11.1, false],
+ [0, 0, 0, "later", 12.2, true],
+ [0, 1, 0, "!", 13.3, false],
+ [0, 2, 0, "!", 13.3, false],
+ [0, 200, 0, "number",140.4, false],
+ [0, 1, 1, "you", 130.0, false],
+ [0, 0, 0, "hi", 120.0, false]
+ ])"},
+ &expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckResult(result_array, expected_array, read_schema);
+}
+
+TEST_P(MergeFileSplitReadTest, TestReadWithLimits) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ ReadContextBuilder context_builder(path);
+ context_builder.SetOptions({{Options::SEQUENCE_FIELD, "s0,s1"},
+ {Options::MERGE_ENGINE, "deduplicate"},
+ {Options::IGNORE_DELETE, "true"},
+ {Options::READ_BATCH_SIZE, "1"}});
+ AddOptions(&context_builder);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+
+ auto internal_context = CreateInternalReadContext(read_context);
+
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
PrepareDataSplit()));
+
+ // simulate read limits, only read 4 batches
+ for (int32_t i = 0; i < 4; i++) {
+ ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatch batch,
batch_reader->NextBatch());
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> array,
+ ReadResultCollector::GetArray(std::move(batch)));
+ ASSERT_TRUE(array);
+ ASSERT_EQ(array->length(), 1);
+ }
+ batch_reader->Close();
+}
+
+TEST_P(MergeFileSplitReadTest, TestDeduplicateMergeEngineWithDeleteMsg) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_partial_update.db/pk_table_partial_update";
+ ReadContextBuilder context_builder(path);
+
+ std::vector<DataField> raw_read_fields = {DataField(0, arrow::field("k0",
arrow::int32())),
+ DataField(1, arrow::field("k1",
arrow::int32())),
+ DataField(2, arrow::field("v0",
arrow::float64())),
+ DataField(3, arrow::field("v1",
arrow::boolean())),
+ DataField(4, arrow::field("v2",
arrow::utf8()))};
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+
+ context_builder.SetReadSchema({"k0", "k1", "v0", "v1", "v2"});
+ context_builder.SetOptions({{Options::MERGE_ENGINE, "deduplicate"}});
+ AddOptions(&context_builder);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+
+ auto internal_context = CreateInternalReadContext(read_context);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
PrepareDataSplit2()));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ auto fields_with_row_kind = read_schema->fields();
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
+
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow::struct_(fields_with_row_kind),
{R"([
+ [0, 0, 1, 100.0, true, "new_apple"],
+ [0, 1, 1, 133.3, false, null],
+ [0, 2, 1, 144.4, true, "orange"]
+ ])"},
+ &expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckResult(result_array, expected_array, read_schema);
+}
+
+TEST_P(MergeFileSplitReadTest, TestReadWithPredicate) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ ReadContextBuilder context_builder(path);
+
+ std::vector<DataField> raw_read_fields = {DataField(1, arrow::field("k1",
arrow::int32())),
+ DataField(3, arrow::field("p1",
arrow::int32())),
+ DataField(5, arrow::field("s1",
arrow::utf8())),
+ DataField(4, arrow::field("s0",
arrow::utf8())),
+ DataField(6, arrow::field("v0",
arrow::float64())),
+ DataField(7, arrow::field("v1",
arrow::boolean()))};
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+
+ context_builder.SetReadSchema({"k1", "p1", "s1", "s0", "v0", "v1"});
+ context_builder.SetOptions({{Options::SEQUENCE_FIELD, "s0,s1"},
+ {Options::MERGE_ENGINE, "deduplicate"},
+ {Options::IGNORE_DELETE, "true"}});
+ AddOptions(&context_builder);
+
+ // less_than will be ignore as it is partition predicate
+ auto less_than = PredicateBuilder::GreaterThan(/*field_index=*/1,
/*field_name=*/"p1",
+ FieldType::INT,
Literal(-1));
+ // greater_or_equal is key predicate, will always be pushed down
+ auto greater_or_equal =
PredicateBuilder::GreaterOrEqual(/*field_index=*/0, /*field_name=*/"k1",
+ FieldType::INT,
Literal(1));
+ // greater_than is value predicate, will be pushed down while the number
of sorted run in
+ // section equals 1
+ auto greater_than = PredicateBuilder::GreaterThan(/*field_index=*/4,
/*field_name=*/"v0",
+ FieldType::DOUBLE,
Literal(150.0));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> predicate_result,
+ PredicateBuilder::And({less_than, greater_or_equal,
greater_than}));
+ context_builder.SetPredicate(predicate_result);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+
+ auto internal_context = CreateInternalReadContext(read_context);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
PrepareDataSplit()));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ auto fields_with_row_kind = read_schema->fields();
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
+
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow::struct_(fields_with_row_kind),
{R"([
+ [0, 0, 0, "see", "apple", 110.0, false],
+ [0, 1, 0, "you", "banana", 11.1, false],
+ [0, 0, 0, "later", "car", 12.2, true],
+ [0, 1, 0, "!", "driver", 13.3, false],
+ [0, 2, 0, "!", "driver", 13.3, false],
+ [0, 1, 1, "you", "zoo", 130.0, false]
+
+ ])"},
+ &expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckResult(result_array, expected_array, read_schema);
+}
+
+TEST_P(MergeFileSplitReadTest, TestReadWithAlterTable) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ ReadContextBuilder context_builder(path);
+
+ std::vector<DataField> raw_read_fields = {DataField(0, arrow::field("k1",
arrow::int32())),
+ DataField(1, arrow::field("k0",
arrow::int32())),
+ DataField(2, arrow::field("p0",
arrow::int32())),
+ DataField(3, arrow::field("p1",
arrow::int32())),
+ DataField(4, arrow::field("s1",
arrow::utf8())),
+ DataField(5, arrow::field("s0",
arrow::binary())),
+ DataField(6, arrow::field("v0",
arrow::int32())),
+ DataField(7, arrow::field("v1",
arrow::utf8())),
+ DataField(8, arrow::field("v2",
arrow::int32()))};
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+
+ context_builder.SetReadSchema({"k1", "k0", "p0", "p1", "s1", "s0", "v0",
"v1", "v2"});
+ context_builder.SetOptions({{Options::SEQUENCE_FIELD, "s0,s1"},
+ {Options::MERGE_ENGINE, "deduplicate"},
+ {Options::IGNORE_DELETE, "true"}});
+ AddOptions(&context_builder);
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+
+ auto internal_context = CreateInternalReadContext(read_context,
/*schema_id=*/7);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
PrepareDataSplit()));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ auto fields_with_row_kind = read_schema->fields();
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
+
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow::struct_(fields_with_row_kind),
{R"([
+ [0, 0, 0, 0, 0, "apple", "see", 110, "false", null],
+ [0, 0, 1, 0, 0, "banana", "you", 11, "false", null],
+ [0, 1, 0, 0, 0, "car", "later", 12, "true", null],
+ [0, 1, 1, 0, 0, "driver", "!", 13, "false", null],
+ [0, 1, 2, 0, 0, "driver", "!", 13, "false", null],
+ [0, 100, 200, 0, 0, "max", "number", 140, "false", null],
+ [0, 0, 1, 0, 1, "zoo", "you", 130, "false", null],
+ [0, 0, 0, 1, 0, "elephant", "hi", 120, "false", null]
+ ])"},
+ &expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckResult(result_array, expected_array, read_schema);
+}
+
+TEST_P(MergeFileSplitReadTest, TestReadWithAlterTableWithReverseSequence) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ ReadContextBuilder context_builder(path);
+
+ std::vector<DataField> raw_read_fields = {DataField(8, arrow::field("v2",
arrow::int32())),
+ DataField(3, arrow::field("p1",
arrow::int32())),
+ DataField(1, arrow::field("k0",
arrow::int32())),
+ DataField(2, arrow::field("p0",
arrow::int32())),
+ DataField(5, arrow::field("s0",
arrow::binary())),
+ DataField(6, arrow::field("v0",
arrow::int32()))};
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+
+ context_builder.SetReadSchema({"v2", "p1", "k0", "p0", "s0", "v0"});
+ context_builder.SetOptions({{Options::SEQUENCE_FIELD, "s0,s1"},
+ {Options::MERGE_ENGINE, "deduplicate"},
+ {Options::IGNORE_DELETE, "true"}});
+ AddOptions(&context_builder);
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+
+ auto internal_context = CreateInternalReadContext(read_context,
/*schema_id=*/7);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
PrepareDataSplit()));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ auto fields_with_row_kind = read_schema->fields();
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
+
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow::struct_(fields_with_row_kind),
{R"([
+ [0, null, 0, 0, 0, "see", 110],
+ [0, null, 0, 1, 0, "you", 11],
+ [0, null, 0, 0, 0, "later", 12],
+ [0, null, 0, 1, 0, "!", 13],
+ [0, null, 0, 2, 0, "!", 13],
+ [0, null, 0, 200, 0, "number", 140],
+ [0, null, 1, 1, 0, "you", 130],
+ [0, null, 0, 0, 1, "hi", 120]
+ ])"},
+ &expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckResult(result_array, expected_array, read_schema);
+}
+
+TEST_P(MergeFileSplitReadTest, TestAggregateMergeEngine) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ ReadContextBuilder context_builder(path);
+
+ std::vector<DataField> raw_read_fields = {DataField(1, arrow::field("k1",
arrow::int32())),
+ DataField(3, arrow::field("p1",
arrow::int32())),
+ DataField(5, arrow::field("s1",
arrow::utf8())),
+ DataField(6, arrow::field("v0",
arrow::float64())),
+ DataField(7, arrow::field("v1",
arrow::boolean()))};
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+
+ context_builder.SetReadSchema({"k1", "p1", "s1", "v0", "v1"});
+ context_builder.SetOptions({{Options::SEQUENCE_FIELD, "s0,s1"},
+ {Options::MERGE_ENGINE, "aggregation"},
+ {"fields.v1.aggregate-function", "bool_and"},
+ {"fields.v0.aggregate-function", "sum"},
+ {Options::IGNORE_DELETE, "true"}});
+ AddOptions(&context_builder);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+ auto internal_context = CreateInternalReadContext(read_context);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
PrepareDataSplit()));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ auto fields_with_row_kind = read_schema->fields();
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
+
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow::struct_(fields_with_row_kind),
{R"([
+ [0, 0, 0, "see", 120.0, false],
+ [0, 1, 0, "you", 122.2, false],
+ [0, 0, 0, "later", 124.4, true],
+ [0, 1, 0, "!", 13.3, false],
+ [0, 2, 0, "!", 13.3, false],
+ [0, 200, 0, "number",140.4, false],
+ [0, 1, 1, "you", 160.0, false],
+ [0, 0, 0, "hi", 140.0, false]
+ ])"},
+ &expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckResult(result_array, expected_array, read_schema);
+}
+
+TEST_P(MergeFileSplitReadTest, TestPartialUpdateMergeEngine) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ ReadContextBuilder context_builder(path);
+
+ std::vector<DataField> raw_read_fields = {DataField(1, arrow::field("k1",
arrow::int32())),
+ DataField(3, arrow::field("p1",
arrow::int32())),
+ DataField(5, arrow::field("s1",
arrow::utf8())),
+ DataField(6, arrow::field("v0",
arrow::float64()))};
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+
+ context_builder.SetReadSchema({"k1", "p1", "s1", "v0"});
+ context_builder.SetOptions({{Options::SEQUENCE_FIELD, "s0,s1"},
+ {Options::MERGE_ENGINE, "partial-update"},
+ {"fields.v1.sequence-group", "v0"},
+ {"fields.v0.aggregate-function",
"first_value"},
+ {Options::IGNORE_DELETE, "true"}});
+ AddOptions(&context_builder);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+ auto internal_context = CreateInternalReadContext(read_context);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
PrepareDataSplit()));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ auto fields_with_row_kind = read_schema->fields();
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
+
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow::struct_(fields_with_row_kind),
{R"([
+ [0, 0, 0, "see", 110.0],
+ [0, 1, 0, "you", 11.1],
+ [0, 0, 0, "later", 112.2],
+ [0, 1, 0, "!", 13.3],
+ [0, 2, 0, "!", 13.3],
+ [0, 200, 0, "number",140.4],
+ [0, 1, 1, "you", 30.0],
+ [0, 0, 0, "hi", 120.0]
+ ])"},
+ &expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckResult(result_array, expected_array, read_schema);
+}
+
+TEST_P(MergeFileSplitReadTest, TestPartialUpdateMergeEngineWithIgnoreDelete) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_partial_update.db/pk_table_partial_update";
+ ReadContextBuilder context_builder(path);
+
+ std::vector<DataField> raw_read_fields = {DataField(0, arrow::field("k0",
arrow::int32())),
+ DataField(1, arrow::field("k1",
arrow::int32())),
+ DataField(2, arrow::field("v0",
arrow::float64())),
+ DataField(3, arrow::field("v1",
arrow::boolean())),
+ DataField(4, arrow::field("v2",
arrow::utf8()))};
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+
+ context_builder.SetReadSchema({"k0", "k1", "v0", "v1", "v2"});
+ context_builder.SetOptions(
+ {{Options::MERGE_ENGINE, "partial-update"}, {Options::IGNORE_DELETE,
"true"}});
+ AddOptions(&context_builder);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+ auto internal_context = CreateInternalReadContext(read_context);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
PrepareDataSplit2()));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ auto fields_with_row_kind = read_schema->fields();
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status = arrow::ipc::internal::json::ChunkedArrayFromJSON(
+ arrow::struct_(fields_with_row_kind),
+ {R"([ [0, 0, 1, 100.0, true, "new_apple"], [0, 1, 0, 2.0, false,
null], [0, 1, 1, 133.3,
+ false, "banana"], [0, 2, 1, 144.4, true, "orange"]
+
+ ])"},
+ &expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckResult(result_array, expected_array, read_schema);
+}
+
+TEST_P(MergeFileSplitReadTest,
TestPartialUpdateMergeEngineWithRemoveRecordOnDelete) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_partial_update.db/pk_table_partial_update";
+ ReadContextBuilder context_builder(path);
+
+ std::vector<DataField> raw_read_fields = {DataField(0, arrow::field("k0",
arrow::int32())),
+ DataField(1, arrow::field("k1",
arrow::int32())),
+ DataField(2, arrow::field("v0",
arrow::float64())),
+ DataField(3, arrow::field("v1",
arrow::boolean())),
+ DataField(4, arrow::field("v2",
arrow::utf8()))};
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+
+ context_builder.SetReadSchema({"k0", "k1", "v0", "v1", "v2"});
+ context_builder.SetOptions({{Options::MERGE_ENGINE, "partial-update"},
+
{Options::PARTIAL_UPDATE_REMOVE_RECORD_ON_DELETE, "true"}});
+ AddOptions(&context_builder);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+ auto internal_context = CreateInternalReadContext(read_context);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
PrepareDataSplit2()));
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+
ReadResultCollector::CollectResult(batch_reader.get()));
+
+ auto fields_with_row_kind = read_schema->fields();
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status = arrow::ipc::internal::json::ChunkedArrayFromJSON(
+ arrow::struct_(fields_with_row_kind),
+ {R"([ [0, 0, 1, 100.0, true, "new_apple"], [0, 1, 1, 133.3, false,
"banana"], [0, 2, 1,
+ 144.4, true, "orange"]
+
+ ])"},
+ &expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckResult(result_array, expected_array, read_schema);
+}
+
+TEST_P(MergeFileSplitReadTest, TestEmptyPlan) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_partial_update.db/pk_table_partial_update";
+ ReadContextBuilder context_builder(path);
+
+ std::vector<DataField> raw_read_fields = {DataField(0, arrow::field("k0",
arrow::int32())),
+ DataField(1, arrow::field("k1",
arrow::int32())),
+ DataField(2, arrow::field("v0",
arrow::float64())),
+ DataField(3, arrow::field("v1",
arrow::boolean())),
+ DataField(4, arrow::field("v2",
arrow::utf8()))};
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+
+ context_builder.SetReadSchema({"k0", "k1", "v0", "v1", "v2"});
+ context_builder.SetOptions({{Options::MERGE_ENGINE, "partial-update"},
+
{Options::PARTIAL_UPDATE_REMOVE_RECORD_ON_DELETE, "true"}});
+ AddOptions(&context_builder);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+ auto internal_context = CreateInternalReadContext(read_context);
+ std::vector<std::shared_ptr<DataSplit>> empty_data_split;
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
empty_data_split));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> read_result,
+
ReadResultCollector::CollectResult(batch_reader.get()));
+ // empty result with null pointer batch
+ ASSERT_FALSE(read_result);
+}
+
+TEST_P(MergeFileSplitReadTest, TestIOException) {
+ std::string path =
+ paimon::test::GetDataDir() +
"/parquet/pk_table_with_mor.db/pk_table_with_mor";
+ std::vector<DataField> raw_read_fields = {DataField(1, arrow::field("k1",
arrow::int32())),
+ DataField(3, arrow::field("p1",
arrow::int32())),
+ DataField(5, arrow::field("s1",
arrow::utf8())),
+ DataField(6, arrow::field("v0",
arrow::float64())),
+ DataField(7, arrow::field("v1",
arrow::boolean()))};
+
+ ReadContextBuilder context_builder(path);
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+
+ context_builder.SetReadSchema({"k1", "p1", "s1", "v0", "v1"});
+ context_builder.SetOptions({{Options::SEQUENCE_FIELD, "s0,s1"},
+ {Options::MERGE_ENGINE, "deduplicate"},
+ {Options::IGNORE_DELETE, "true"}});
+ AddOptions(&context_builder);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+
+ auto internal_context = CreateInternalReadContext(read_context);
+
+ auto fields_with_row_kind = read_schema->fields();
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
+
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow::struct_(fields_with_row_kind),
{R"([
+ [0, 0, 0, "see", 110.0, false],
+ [0, 1, 0, "you", 11.1, false],
+ [0, 0, 0, "later", 12.2, true],
+ [0, 1, 0, "!", 13.3, false],
+ [0, 2, 0, "!", 13.3, false],
+ [0, 200, 0, "number",140.4, false],
+ [0, 1, 1, "you", 130.0, false],
+ [0, 0, 0, "hi", 120.0, false]
+ ])"},
+ &expected_array);
+ ASSERT_TRUE(array_status.ok());
+
+ bool run_complete = false;
+ auto io_hook = IOHook::GetInstance();
+ for (size_t i = 0; i < 300; i++) {
+ ScopeGuard guard([&io_hook]() { io_hook->Clear(); });
+ io_hook->Reset(i, IOHook::Mode::RETURN_ERROR);
+ auto batch_reader = CreateReader(internal_context, PrepareDataSplit());
+ CHECK_HOOK_STATUS(batch_reader.status(), i);
+ auto read_result =
ReadResultCollector::CollectResult(batch_reader.value().get());
+ CHECK_HOOK_STATUS(read_result.status(), i);
+ auto result_array = read_result.value();
+ CheckResult(result_array, expected_array, read_schema);
+ run_complete = true;
+ break;
+ }
+ ASSERT_TRUE(run_complete);
+}
+
+TEST_P(MergeFileSplitReadTest, Test09VersionWithoutInlineFieldId) {
+ std::string path = paimon::test::GetDataDir() + "/orc/pk_09.db/pk_09";
+ ReadContextBuilder context_builder(path);
+ std::vector<DataField> raw_read_fields = {DataField(3, arrow::field("f3",
arrow::float64())),
+ DataField(2, arrow::field("f2",
arrow::int32())),
+ DataField(0, arrow::field("f0",
arrow::utf8())),
+ DataField(1, arrow::field("f1",
arrow::int32()))};
+ auto read_schema =
DataField::ConvertDataFieldsToArrowSchema(raw_read_fields);
+ ASSERT_TRUE(read_schema);
+ context_builder.SetReadSchema({"f3", "f2", "f0", "f1"});
+ context_builder.SetOptions({{Options::FILE_FORMAT, "orc"},
+ {Options::MERGE_ENGINE, "deduplicate"},
+ {"orc.read.enable-metrics", "true"}});
+ AddOptions(&context_builder);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<ReadContext> read_context,
context_builder.Finish());
+ auto meta1 = std::make_shared<DataFileMeta>(
+ "data-00e3ed53-16ba-4537-9264-b7dc03fefc65-0.orc", /*file_size=*/803,
/*row_count=*/1,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({std::string("Tony"), 0},
pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({std::string("Tony"), 0},
pool_.get()),
+ /*key_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Tony"), 0},
{std::string("Tony"), 0},
+ {0, 0}, pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Tony"), 10, 0, 14.1},
+ {std::string("Tony"), 10, 0, 14.1},
{0, 0, 0, 0},
+ pool_.get()),
+ /*min_sequence_number=*/5, /*max_sequence_number=*/5, /*schema_id=*/0,
+ /*level=*/0, /*extra_files=*/std::vector<std::optional<std::string>>(),
+ /*creation_time=*/Timestamp(0ll, 0),
+ /*delete_row_count=*/1, /*embedded_index=*/nullptr,
FileSource::Append(),
+ /*value_stats_cols=*/std::nullopt, /*external_path=*/std::nullopt,
+ /*first_row_id=*/std::nullopt,
+ /*write_cols=*/std::nullopt);
+ auto meta2 = std::make_shared<DataFileMeta>(
+ "data-6871b960-edd9-40fc-9859-aaca9ea205cf-0.orc", /*file_size=*/887,
/*row_count=*/5,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({std::string("Alex"), 0},
pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({std::string("Tony"), 0},
pool_.get()),
+ /*key_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alex"), 0},
{std::string("Tony"), 0},
+ {0, 0}, pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alex"), 10, 0, 12.1},
+ {std::string("Tony"), 10, 0, 17.1},
{0, 0, 0, 0},
+ pool_.get()),
+ /*min_sequence_number=*/0, /*max_sequence_number=*/4, /*schema_id=*/0,
+ /*level=*/0, /*extra_files=*/std::vector<std::optional<std::string>>(),
+ /*creation_time=*/Timestamp(0ll, 0),
+ /*delete_row_count=*/0, /*embedded_index=*/nullptr,
FileSource::Append(),
+ /*value_stats_cols=*/std::nullopt, /*external_path=*/std::nullopt,
+ /*first_row_id=*/std::nullopt,
+ /*write_cols=*/std::nullopt);
+ DataSplitImpl::Builder builder(
+ BinaryRowGenerator::GenerateRow({10}, pool_.get()),
+ /*bucket=*/1, /*bucket_path=*/
+ paimon::test::GetDataDir() + "/orc/pk_09.db/pk_09/f1=10/bucket-1/",
{meta1, meta2});
+ ASSERT_OK_AND_ASSIGN(auto data_split,
+
builder.WithSnapshot(5).IsStreaming(false).RawConvertible(false).Build());
+ auto internal_context = CreateInternalReadContext(read_context);
+ ASSERT_OK_AND_ASSIGN(auto batch_reader, CreateReader(internal_context,
{data_split}));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+
ReadResultCollector::CollectResult(batch_reader.get()));
+ auto fields_with_row_kind = read_schema->fields();
+ fields_with_row_kind.insert(fields_with_row_kind.begin(),
+ arrow::field("_VALUE_KIND", arrow::int8()));
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
+
arrow::ipc::internal::json::ChunkedArrayFromJSON(arrow::struct_(fields_with_row_kind),
{R"([
+ [0, 16.1, 0, "Alex", 10],
+ [0, 12.1, 0, "Bob", 10],
+ [0, 17.1, 0, "David", 10],
+ [0, 13.1, 0, "Emily", 10]
+ ])"},
+ &expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckResult(result_array, expected_array, read_schema);
+
+ batch_reader->Close();
+ auto read_metrics = batch_reader->GetReaderMetrics();
+ ASSERT_OK_AND_ASSIGN(uint64_t io_count,
read_metrics->GetCounter("orc.read.io.count"));
+ ASSERT_GT(io_count, 0);
+ ASSERT_OK_AND_ASSIGN(uint64_t latency,
+
read_metrics->GetCounter("orc.read.inclusive.latency.us"));
+ ASSERT_GT(latency, 0);
+}
+
+INSTANTIATE_TEST_SUITE_P(UseMinHeapAndEnablePrefetchAndEnableMultiThreadProject,
+ MergeFileSplitReadTest,
+ ::testing::Combine(::testing::Bool(),
::testing::Bool(),
+ ::testing::Bool()));
+
+} // namespace paimon::test