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 ef38dd2 feat(core): introduce merge tree writer (#82)
ef38dd2 is described below
commit ef38dd266fc332119efc1df6522fcd5617b3073a
Author: Zhang Jiawei <[email protected]>
AuthorDate: Tue Jun 16 09:10:17 2026 +0800
feat(core): introduce merge tree writer (#82)
---
src/paimon/core/mergetree/merge_tree_writer.cpp | 351 +++++
src/paimon/core/mergetree/merge_tree_writer.h | 143 ++
.../core/mergetree/merge_tree_writer_test.cpp | 1533 ++++++++++++++++++++
3 files changed, 2027 insertions(+)
diff --git a/src/paimon/core/mergetree/merge_tree_writer.cpp
b/src/paimon/core/mergetree/merge_tree_writer.cpp
new file mode 100644
index 0000000..dbca52e
--- /dev/null
+++ b/src/paimon/core/mergetree/merge_tree_writer.cpp
@@ -0,0 +1,351 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "paimon/core/mergetree/merge_tree_writer.h"
+
+#include <algorithm>
+#include <cassert>
+#include <unordered_set>
+#include <utility>
+
+#include "arrow/api.h"
+#include "arrow/c/abi.h"
+#include "arrow/c/helpers.h"
+#include "paimon/common/metrics/metrics_impl.h"
+#include "paimon/common/table/special_fields.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/scope_guard.h"
+#include "paimon/core/disk/io_manager.h"
+#include "paimon/core/io/async_key_value_producer_and_consumer.h"
+#include "paimon/core/io/compact_increment.h"
+#include "paimon/core/io/data_file_path_factory.h"
+#include "paimon/core/io/data_increment.h"
+#include "paimon/core/io/key_value_data_file_writer.h"
+#include "paimon/core/io/key_value_meta_projection_consumer.h"
+#include "paimon/core/io/key_value_record_reader.h"
+#include "paimon/core/io/row_to_arrow_array_converter.h"
+#include "paimon/core/io/single_file_writer.h"
+#include "paimon/core/manifest/file_source.h"
+#include "paimon/core/mergetree/compact/sort_merge_reader_with_loser_tree.h"
+#include "paimon/core/mergetree/write_buffer.h"
+#include "paimon/core/utils/commit_increment.h"
+#include "paimon/format/file_format.h"
+#include "paimon/format/writer_builder.h"
+
+namespace paimon {
+class FormatStatsExtractor;
+
+Result<std::shared_ptr<MergeTreeWriter>> MergeTreeWriter::Create(
+ int64_t last_sequence_number, const std::vector<std::string>&
trimmed_primary_keys,
+ const std::shared_ptr<DataFilePathFactory>& path_factory,
+ const std::shared_ptr<FieldsComparator>& key_comparator,
+ const std::shared_ptr<FieldsComparator>& user_defined_seq_comparator,
+ const std::shared_ptr<MergeFunctionWrapper<KeyValue>>&
merge_function_wrapper,
+ int64_t schema_id, const std::shared_ptr<arrow::Schema>& value_schema,
+ const CoreOptions& options, const std::shared_ptr<CompactManager>&
compact_manager,
+ const std::shared_ptr<IOManager>& io_manager, bool
enable_multi_thread_spill,
+ const std::shared_ptr<MemoryPool>& pool) {
+ auto write_schema =
SpecialFields::CompleteSequenceAndValueKindField(value_schema);
+ PAIMON_ASSIGN_OR_RAISE(
+ std::unique_ptr<WriteBuffer> write_buffer,
+ WriteBuffer::Create(last_sequence_number, value_schema,
trimmed_primary_keys,
+ options.GetSequenceField(), key_comparator,
user_defined_seq_comparator,
+ merge_function_wrapper, options, io_manager,
enable_multi_thread_spill,
+ pool));
+ return std::shared_ptr<MergeTreeWriter>(
+ new MergeTreeWriter(pool, trimmed_primary_keys, options, path_factory,
key_comparator,
+ user_defined_seq_comparator,
merge_function_wrapper, schema_id,
+ write_schema, compact_manager,
std::move(write_buffer)));
+}
+
+MergeTreeWriter::MergeTreeWriter(
+ const std::shared_ptr<MemoryPool>& pool, const std::vector<std::string>&
trimmed_primary_keys,
+ const CoreOptions& options, const std::shared_ptr<DataFilePathFactory>&
path_factory,
+ const std::shared_ptr<FieldsComparator>& key_comparator,
+ const std::shared_ptr<FieldsComparator>& user_defined_seq_comparator,
+ const std::shared_ptr<MergeFunctionWrapper<KeyValue>>&
merge_function_wrapper,
+ int64_t schema_id, const std::shared_ptr<arrow::Schema>& write_schema,
+ const std::shared_ptr<CompactManager>& compact_manager,
+ std::unique_ptr<WriteBuffer>&& write_buffer)
+ : pool_(pool),
+ trimmed_primary_keys_(trimmed_primary_keys),
+ options_(options),
+ path_factory_(path_factory),
+ key_comparator_(key_comparator),
+ user_defined_seq_comparator_(user_defined_seq_comparator),
+ merge_function_wrapper_(merge_function_wrapper),
+ schema_id_(schema_id),
+ write_schema_(write_schema),
+ compact_manager_(compact_manager),
+ write_buffer_(std::move(write_buffer)),
+ metrics_(std::make_shared<MetricsImpl>()) {}
+
+Status MergeTreeWriter::DoClose() {
+ // Request cancellation and wait for running compaction to exit.
+ // This avoids reusing cancellation state while an old task is still
running.
+ compact_manager_->CancelAndWaitCompaction();
+ PAIMON_RETURN_NOT_OK(Sync());
+ PAIMON_RETURN_NOT_OK(compact_manager_->Close());
+
+ // delete temporary files
+ std::vector<std::shared_ptr<DataFileMeta>> delete_files;
+ delete_files.reserve(new_files_.size() + compact_after_.size());
+ delete_files.insert(delete_files.end(), new_files_.begin(),
new_files_.end());
+ for (const auto& file : compact_after_) {
+ // Upgrade file is required by previous snapshot, so we should ensure
that this file is
+ // not the output of upgraded.
+ auto in_compact_before =
+ std::any_of(compact_before_.begin(), compact_before_.end(),
+ [&file](const std::shared_ptr<DataFileMeta>&
candidate) {
+ return candidate->file_name == file->file_name;
+ });
+ if (!in_compact_before) {
+ delete_files.push_back(file);
+ }
+ }
+ for (const auto& file : delete_files) {
+ // Keep Java parity: temporary file cleanup is quiet.
+ [[maybe_unused]] auto s =
options_.GetFileSystem()->Delete(path_factory_->ToPath(file));
+ }
+
+ write_buffer_->Clear();
+ new_files_.clear();
+ deleted_files_.clear();
+ compact_before_.clear();
+ compact_after_.clear();
+
+ if (compact_deletion_file_) {
+ compact_deletion_file_->Clean();
+ compact_deletion_file_.reset();
+ }
+ return Status::OK();
+}
+
+Status MergeTreeWriter::FlushMemory() {
+ PAIMON_ASSIGN_OR_RAISE(bool has_remaining_quota,
write_buffer_->FlushMemory());
+ if (!has_remaining_quota) {
+
PAIMON_RETURN_NOT_OK(FlushWriteBuffer(/*wait_for_latest_compaction=*/false,
+
/*forced_full_compaction=*/false));
+ }
+ return Status::OK();
+}
+
+Status MergeTreeWriter::Write(std::unique_ptr<RecordBatch>&& moved_batch) {
+ PAIMON_ASSIGN_OR_RAISE(bool has_remaining_quota,
write_buffer_->Write(std::move(moved_batch)));
+ if (!has_remaining_quota) {
+ return FlushWriteBuffer(/*wait_for_latest_compaction=*/false,
+ /*forced_full_compaction=*/false);
+ }
+ return Status::OK();
+}
+
+Status MergeTreeWriter::Compact(bool full_compaction) {
+ return FlushWriteBuffer(/*wait_for_latest_compaction=*/true,
full_compaction);
+}
+
+Status MergeTreeWriter::Sync() {
+ return TrySyncLatestCompaction(/*blocking=*/true);
+}
+
+Status MergeTreeWriter::TrySyncLatestCompaction(bool blocking) {
+ PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<CompactResult>>
result,
+ compact_manager_->GetCompactionResult(blocking));
+ if (result) {
+ PAIMON_RETURN_NOT_OK(UpdateCompactResult(result.value()));
+ }
+ return Status::OK();
+}
+
+Status MergeTreeWriter::UpdateCompactResult(const
std::shared_ptr<CompactResult>& compact_result) {
+ std::unordered_set<std::string> after_files;
+ after_files.reserve(compact_result->After().size());
+ for (const auto& file : compact_result->After()) {
+ after_files.insert(file->file_name);
+ }
+
+ auto in_compact_before = [this](const std::string& file_name) {
+ return std::any_of(compact_before_.begin(), compact_before_.end(),
+ [&file_name](const std::shared_ptr<DataFileMeta>&
meta) {
+ return meta->file_name == file_name;
+ });
+ };
+
+ for (const auto& file : compact_result->Before()) {
+ auto compact_after_it =
+ std::find_if(compact_after_.begin(), compact_after_.end(),
+ [&file](const std::shared_ptr<DataFileMeta>&
candidate) {
+ return candidate->file_name == file->file_name;
+ });
+ if (compact_after_it != compact_after_.end()) {
+ compact_after_.erase(compact_after_it);
+ // This is an intermediate file (not a new data file), which is no
longer needed
+ // after compaction and can be deleted directly, but upgrade file
is required by
+ // previous snapshot and following snapshot, so we should ensure:
+ // 1. This file is not the output of upgraded.
+ // 2. This file is not the input of upgraded.
+ if (!in_compact_before(file->file_name) &&
+ after_files.find(file->file_name) == after_files.end()) {
+ auto fs = options_.GetFileSystem();
+ [[maybe_unused]] auto s =
fs->Delete(path_factory_->ToPath(file));
+ }
+ } else {
+ compact_before_.push_back(file);
+ }
+ }
+
+ compact_after_.insert(compact_after_.end(),
compact_result->After().begin(),
+ compact_result->After().end());
+ // TODO(yonghao.fyh): support compact changelog
+ return UpdateCompactDeletionFile(compact_result->DeletionFile());
+}
+
+Status MergeTreeWriter::UpdateCompactDeletionFile(
+ const std::shared_ptr<CompactDeletionFile>& new_deletion_file) {
+ if (new_deletion_file) {
+ if (compact_deletion_file_ == nullptr) {
+ compact_deletion_file_ = new_deletion_file;
+ } else {
+ PAIMON_ASSIGN_OR_RAISE(compact_deletion_file_,
+
new_deletion_file->MergeOldFile(compact_deletion_file_));
+ }
+ }
+ return Status::OK();
+}
+
+Result<CommitIncrement> MergeTreeWriter::PrepareCommit(bool wait_compaction) {
+ PAIMON_RETURN_NOT_OK(FlushWriteBuffer(wait_compaction,
/*forced_full_compaction=*/false));
+ if (options_.CommitForceCompact()) {
+ wait_compaction = true;
+ }
+ // Decide again whether to wait here.
+ // For example, in the case of repeated failures in writing, it is
possible that Level 0
+ // files were successfully committed, but failed to restart during the
compaction phase,
+ // which may result in an increasing number of Level 0 files. This wait
can avoid this
+ // situation.
+ if (compact_manager_->ShouldWaitForPreparingCheckpoint()) {
+ wait_compaction = true;
+ }
+ PAIMON_RETURN_NOT_OK(TrySyncLatestCompaction(wait_compaction));
+ return DrainIncrement();
+}
+
+Result<bool> MergeTreeWriter::CompactNotCompleted() {
+
PAIMON_RETURN_NOT_OK(compact_manager_->TriggerCompaction(/*full_compaction=*/false));
+ return compact_manager_->CompactNotCompleted();
+}
+
+Status MergeTreeWriter::FlushWriteBuffer(bool wait_for_latest_compaction,
+ bool forced_full_compaction) {
+ if (!write_buffer_->IsEmpty()) {
+ if (compact_manager_->ShouldWaitForLatestCompaction()) {
+ wait_for_latest_compaction = true;
+ }
+ auto cleanup_guard = ScopeGuard([&]() { write_buffer_->Clear(); });
+ // 1. flush write buffer to get sorted readers
+
PAIMON_ASSIGN_OR_RAISE(std::vector<std::unique_ptr<KeyValueRecordReader>>
readers,
+ write_buffer_->CreateReaders());
+ // 2. prepare loser tree sort merge reader
+ auto sort_merge_reader =
std::make_unique<SortMergeReaderWithLoserTree>(
+ std::move(readers), key_comparator_, user_defined_seq_comparator_,
+ merge_function_wrapper_);
+ // 3. project key value to arrow array
+ auto create_consumer = [target_schema = write_schema_, pool = pool_]()
+ -> Result<std::unique_ptr<RowToArrowArrayConverter<KeyValue,
KeyValueBatch>>> {
+ return KeyValueMetaProjectionConsumer::Create(target_schema, pool);
+ };
+ // consumer batch size is WriteBatchSize
+ auto async_key_value_producer_consumer =
+ std::make_unique<AsyncKeyValueProducerAndConsumer<KeyValue,
KeyValueBatch>>(
+ std::move(sort_merge_reader), create_consumer,
options_.GetWriteBatchSize(),
+ /*projection_thread_num=*/1, pool_);
+ auto rolling_writer = CreateRollingRowWriter();
+ ScopeGuard write_guard([&]() -> void {
+ rolling_writer->Abort();
+ async_key_value_producer_consumer->Close();
+ });
+ while (true) {
+ PAIMON_ASSIGN_OR_RAISE(KeyValueBatch key_value_batch,
+
async_key_value_producer_consumer->NextBatch());
+ if (key_value_batch.batch == nullptr) {
+ break;
+ }
+
PAIMON_RETURN_NOT_OK(rolling_writer->Write(std::move(key_value_batch)));
+ }
+ PAIMON_RETURN_NOT_OK(rolling_writer->Close());
+ PAIMON_ASSIGN_OR_RAISE(std::vector<std::shared_ptr<DataFileMeta>>
flushed_files,
+ rolling_writer->GetResult());
+ async_key_value_producer_consumer->Close();
+ write_guard.Release();
+
+ for (const auto& flushed_file : flushed_files) {
+ new_files_.emplace_back(flushed_file);
+ PAIMON_RETURN_NOT_OK(compact_manager_->AddNewFile(flushed_file));
+ }
+ metrics_->Merge(rolling_writer->GetMetrics());
+ }
+ PAIMON_RETURN_NOT_OK(TrySyncLatestCompaction(wait_for_latest_compaction));
+
PAIMON_RETURN_NOT_OK(compact_manager_->TriggerCompaction(forced_full_compaction));
+ return Status::OK();
+}
+
+Result<CommitIncrement> MergeTreeWriter::DrainIncrement() {
+ DataIncrement data_increment(std::move(new_files_),
std::move(deleted_files_), {});
+ CompactIncrement compact_increment(std::move(compact_before_),
std::move(compact_after_), {});
+ auto drain_deletion_file = compact_deletion_file_;
+
+ new_files_.clear();
+ deleted_files_.clear();
+ compact_before_.clear();
+ compact_after_.clear();
+ compact_deletion_file_ = nullptr;
+
+ return CommitIncrement(data_increment, compact_increment,
drain_deletion_file);
+}
+
+std::unique_ptr<RollingFileWriter<KeyValueBatch,
std::shared_ptr<DataFileMeta>>>
+MergeTreeWriter::CreateRollingRowWriter() const {
+ auto create_file_writer = [&]()
+ -> Result<std::unique_ptr<SingleFileWriter<KeyValueBatch,
std::shared_ptr<DataFileMeta>>>> {
+ ::ArrowSchema arrow_schema;
+ ScopeGuard guard([&arrow_schema]() {
ArrowSchemaRelease(&arrow_schema); });
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*write_schema_,
&arrow_schema));
+ auto format = options_.GetWriteFileFormat(/*level=*/0);
+ PAIMON_ASSIGN_OR_RAISE(
+ std::shared_ptr<WriterBuilder> writer_builder,
+ format->CreateWriterBuilder(&arrow_schema,
options_.GetWriteBatchSize()));
+ writer_builder->WithMemoryPool(pool_);
+ PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*write_schema_,
&arrow_schema));
+ PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<FormatStatsExtractor>
stats_extractor,
+ format->CreateStatsExtractor(&arrow_schema));
+ auto converter = [](KeyValueBatch key_value_batch, ArrowArray* array)
-> Status {
+ ArrowArrayMove(key_value_batch.batch.get(), array);
+ return Status::OK();
+ };
+ auto writer = std::make_unique<KeyValueDataFileWriter>(
+ options_.GetWriteFileCompression(0), converter, schema_id_,
/*level=*/0,
+ FileSource::Append(), trimmed_primary_keys_, stats_extractor,
write_schema_,
+ path_factory_->IsExternalPath(), pool_);
+ PAIMON_RETURN_NOT_OK(
+ writer->Init(options_.GetFileSystem(), path_factory_->NewPath(),
writer_builder));
+ return writer;
+ };
+ return std::make_unique<RollingFileWriter<KeyValueBatch,
std::shared_ptr<DataFileMeta>>>(
+ options_.GetTargetFileSize(/*has_primary_key=*/true),
create_file_writer);
+}
+
+} // namespace paimon
diff --git a/src/paimon/core/mergetree/merge_tree_writer.h
b/src/paimon/core/mergetree/merge_tree_writer.h
new file mode 100644
index 0000000..bc1a1b4
--- /dev/null
+++ b/src/paimon/core/mergetree/merge_tree_writer.h
@@ -0,0 +1,143 @@
+/*
+ * 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 <vector>
+
+#include "arrow/api.h"
+#include "paimon/common/utils/fields_comparator.h"
+#include "paimon/core/compact/compact_manager.h"
+#include "paimon/core/core_options.h"
+#include "paimon/core/io/data_file_meta.h"
+#include "paimon/core/io/data_file_path_factory.h"
+#include "paimon/core/io/rolling_file_writer.h"
+#include "paimon/core/key_value.h"
+#include "paimon/core/mergetree/compact/merge_function_wrapper.h"
+#include "paimon/core/mergetree/write_buffer.h"
+#include "paimon/core/utils/batch_writer.h"
+#include "paimon/core/utils/commit_increment.h"
+#include "paimon/record_batch.h"
+#include "paimon/result.h"
+#include "paimon/status.h"
+
+namespace arrow {
+class Array;
+class DataType;
+class Schema;
+class StructArray;
+} // namespace arrow
+
+namespace paimon {
+class DataFilePathFactory;
+class IOManager;
+class FieldsComparator;
+class MemoryPool;
+class Metrics;
+template <typename T>
+class MergeFunctionWrapper;
+
+class MergeTreeWriter : public BatchWriter {
+ public:
+ static Result<std::shared_ptr<MergeTreeWriter>> Create(
+ int64_t last_sequence_number, const std::vector<std::string>&
trimmed_primary_keys,
+ const std::shared_ptr<DataFilePathFactory>& path_factory,
+ const std::shared_ptr<FieldsComparator>& key_comparator,
+ const std::shared_ptr<FieldsComparator>& user_defined_seq_comparator,
+ const std::shared_ptr<MergeFunctionWrapper<KeyValue>>&
merge_function_wrapper,
+ int64_t schema_id, const std::shared_ptr<arrow::Schema>& value_schema,
+ const CoreOptions& options, const std::shared_ptr<CompactManager>&
compact_manager,
+ const std::shared_ptr<IOManager>& io_manager, bool
enable_multi_thread_spill,
+ const std::shared_ptr<MemoryPool>& pool);
+
+ Status Write(std::unique_ptr<RecordBatch>&& batch) override;
+
+ Status Compact(bool full_compaction) override;
+
+ Result<bool> CompactNotCompleted() override;
+
+ Status Sync() override;
+
+ Result<CommitIncrement> PrepareCommit(bool wait_compaction) override;
+
+ uint64_t GetMemoryUsage() const override {
+ return write_buffer_->GetMemoryUsage();
+ }
+
+ Status FlushMemory() override;
+
+ Status Close() override {
+ return DoClose();
+ }
+
+ std::shared_ptr<Metrics> GetMetrics() const override {
+ return metrics_;
+ }
+
+ private:
+ Status DoClose();
+
+ Status FlushWriteBuffer(bool wait_for_latest_compaction, bool
forced_full_compaction);
+ Result<CommitIncrement> DrainIncrement();
+
+ std::unique_ptr<RollingFileWriter<KeyValueBatch,
std::shared_ptr<DataFileMeta>>>
+ CreateRollingRowWriter() const;
+
+ Status TrySyncLatestCompaction(bool blocking);
+ Status UpdateCompactResult(const std::shared_ptr<CompactResult>&
compact_result);
+ Status UpdateCompactDeletionFile(const
std::shared_ptr<CompactDeletionFile>& new_deletion_file);
+
+ private:
+ MergeTreeWriter(const std::shared_ptr<MemoryPool>& pool,
+ const std::vector<std::string>& trimmed_primary_keys,
+ const CoreOptions& options,
+ const std::shared_ptr<DataFilePathFactory>& path_factory,
+ const std::shared_ptr<FieldsComparator>& key_comparator,
+ const std::shared_ptr<FieldsComparator>&
user_defined_seq_comparator,
+ const std::shared_ptr<MergeFunctionWrapper<KeyValue>>&
merge_function_wrapper,
+ int64_t schema_id, const std::shared_ptr<arrow::Schema>&
write_schema,
+ const std::shared_ptr<CompactManager>& compact_manager,
+ std::unique_ptr<WriteBuffer>&& write_buffer);
+
+ std::shared_ptr<MemoryPool> pool_;
+ std::vector<std::string> trimmed_primary_keys_;
+ CoreOptions options_;
+ std::shared_ptr<DataFilePathFactory> path_factory_;
+ std::shared_ptr<FieldsComparator> key_comparator_;
+ std::shared_ptr<FieldsComparator> user_defined_seq_comparator_;
+ std::shared_ptr<MergeFunctionWrapper<KeyValue>> merge_function_wrapper_;
+ int64_t schema_id_;
+ // write_schema = value_schema + special fields
+ std::shared_ptr<arrow::Schema> write_schema_;
+
+ std::shared_ptr<CompactManager> compact_manager_;
+
+ std::unique_ptr<WriteBuffer> write_buffer_;
+
+ std::shared_ptr<Metrics> metrics_;
+
+ std::vector<std::shared_ptr<DataFileMeta>> new_files_;
+ std::vector<std::shared_ptr<DataFileMeta>> deleted_files_;
+ std::vector<std::shared_ptr<DataFileMeta>> compact_before_;
+ std::vector<std::shared_ptr<DataFileMeta>> compact_after_;
+
+ std::shared_ptr<CompactDeletionFile> compact_deletion_file_;
+};
+} // namespace paimon
diff --git a/src/paimon/core/mergetree/merge_tree_writer_test.cpp
b/src/paimon/core/mergetree/merge_tree_writer_test.cpp
new file mode 100644
index 0000000..b4abb51
--- /dev/null
+++ b/src/paimon/core/mergetree/merge_tree_writer_test.cpp
@@ -0,0 +1,1533 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "paimon/core/mergetree/merge_tree_writer.h"
+
+#include <cassert>
+#include <cstddef>
+#include <functional>
+#include <map>
+#include <optional>
+#include <utility>
+
+#include "arrow/api.h"
+#include "arrow/array/array_base.h"
+#include "arrow/c/abi.h"
+#include "arrow/c/bridge.h"
+#include "arrow/ipc/json_simple.h"
+#include "gtest/gtest.h"
+#include "paimon/common/factories/io_hook.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/compact/noop_compact_manager.h"
+#include "paimon/core/disk/io_manager.h"
+#include "paimon/core/io/compact_increment.h"
+#include "paimon/core/io/data_file_path_factory.h"
+#include "paimon/core/io/data_increment.h"
+#include "paimon/core/manifest/file_source.h"
+#include "paimon/core/mergetree/compact/deduplicate_merge_function.h"
+#include "paimon/core/mergetree/compact/reducer_merge_function_wrapper.h"
+#include "paimon/core/utils/commit_increment.h"
+#include "paimon/defs.h"
+#include "paimon/format/file_format.h"
+#include "paimon/format/file_format_factory.h"
+#include "paimon/fs/file_system.h"
+#include "paimon/fs/local/local_file_system.h"
+#include "paimon/memory/memory_pool.h"
+#include "paimon/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/test_helper.h"
+#include "paimon/testing/utils/testharness.h"
+
+namespace paimon {
+template <typename T>
+class MergeFunctionWrapper;
+} // namespace paimon
+
+namespace paimon::test {
+class MergeTreeWriterTest : public ::testing::TestWithParam<bool> {
+ public:
+ class FakeCompactManager : public paimon::CompactManager {
+ public:
+ Status AddNewFile(const std::shared_ptr<DataFileMeta>& file) override {
+ return Status::OK();
+ }
+ std::vector<std::shared_ptr<DataFileMeta>> AllFiles() const override {
+ static std::vector<std::shared_ptr<DataFileMeta>> empty;
+ return empty;
+ }
+ Status TriggerCompaction(bool full_compaction) override {
+ return Status::OK();
+ }
+ Result<std::optional<std::shared_ptr<CompactResult>>>
GetCompactionResult(
+ bool blocking) override {
+ get_result_blocking_calls.push_back(blocking);
+ return std::optional<std::shared_ptr<CompactResult>>();
+ }
+ void RequestCancelCompaction() override {}
+ void WaitForCompactionToExit() override {}
+ bool CompactNotCompleted() const override {
+ return false;
+ }
+ bool ShouldWaitForLatestCompaction() const override {
+ return true;
+ }
+ bool ShouldWaitForPreparingCheckpoint() const override {
+ return true;
+ }
+ Status Close() override {
+ return Status::OK();
+ }
+
+ std::vector<bool> get_result_blocking_calls;
+ };
+
+ void SetUp() override {
+ pool_ = GetDefaultPool();
+ file_system_ = std::make_shared<LocalFileSystem>();
+ value_fields_ = {DataField(0, arrow::field("f0", arrow::utf8())),
+ DataField(1, arrow::field("f1", arrow::int32())),
+ DataField(2, arrow::field("f2", arrow::int32())),
+ DataField(3, arrow::field("f3", arrow::float64()))};
+ value_schema_ =
DataField::ConvertDataFieldsToArrowSchema(value_fields_);
+ value_type_ =
DataField::ConvertDataFieldsToArrowStructType(value_fields_);
+ primary_keys_ = {"f0"};
+ ASSERT_OK_AND_ASSIGN(key_comparator_,
+ FieldsComparator::Create({value_fields_[0]},
+
/*is_ascending_order=*/true));
+ std::vector<DataField> write_fields = {SpecialFields::SequenceNumber(),
+ SpecialFields::ValueKind()};
+ write_fields.insert(write_fields.end(), value_fields_.begin(),
value_fields_.end());
+ write_type_ =
DataField::ConvertDataFieldsToArrowStructType(write_fields);
+
+ auto mfunc =
std::make_unique<DeduplicateMergeFunction>(/*ignore_delete=*/false);
+ merge_function_wrapper_ =
std::make_shared<ReducerMergeFunctionWrapper>(std::move(mfunc));
+ noop_compact_manager_ = std::make_shared<NoopCompactManager>();
+ }
+ void TearDown() override {}
+
+ std::unique_ptr<RecordBatch> CreateBatch(
+ const std::shared_ptr<arrow::Array>& array,
+ const std::vector<RecordBatch::RowKind>& row_kinds) const {
+ ::ArrowArray c_array;
+ EXPECT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ RecordBatchBuilder batch_builder(&c_array);
+ batch_builder.SetRowKinds(row_kinds);
+ EXPECT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
batch_builder.Finish());
+ return batch;
+ }
+
+ void WriteBatch(const std::shared_ptr<arrow::Array>& array,
+ const std::vector<RecordBatch::RowKind>& row_kinds,
+ MergeTreeWriter* writer) const {
+ auto batch = CreateBatch(array, row_kinds);
+ ASSERT_OK(writer->Write(std::move(batch)));
+ }
+
+ void CheckFileContent(const std::string& data_file_name,
+ const std::shared_ptr<arrow::ChunkedArray>&
expected_array) const {
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> input_stream,
+ file_system_->Open(data_file_name));
+ ASSERT_TRUE(input_stream);
+ ASSERT_OK_AND_ASSIGN(auto file_format, FileFormatFactory::Get("orc",
/*options=*/{}));
+ ASSERT_OK_AND_ASSIGN(auto reader_builder,
+
file_format->CreateReaderBuilder(/*batch_size=*/10));
+ ASSERT_OK_AND_ASSIGN(auto orc_batch_reader,
reader_builder->Build(input_stream));
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result_array,
+
ReadResultCollector::CollectResult(orc_batch_reader.get()));
+ ASSERT_TRUE(expected_array->Equals(result_array)) <<
result_array->ToString();
+ }
+
+ std::shared_ptr<DataFileMeta> CreateMeta(const std::string& name, int32_t
level) const {
+ return std::make_shared<DataFileMeta>(
+ name, /*file_size=*/100, /*row_count=*/1,
DataFileMeta::EmptyMinKey(),
+ DataFileMeta::EmptyMaxKey(), SimpleStats::EmptyStats(),
SimpleStats::EmptyStats(),
+ /*min_sequence_number=*/0, /*max_sequence_number=*/1,
/*schema_id=*/0, level,
+ /*extra_files=*/std::vector<std::optional<std::string>>(),
Timestamp(),
+ /*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);
+ }
+
+ Result<std::shared_ptr<MergeTreeWriter>> CreateMergeWriter(
+ int64_t last_sequence_number, const std::string& temp_dir,
+ const std::shared_ptr<DataFilePathFactory>& path_factory, int64_t
schema_id,
+ const CoreOptions& options,
+ const std::shared_ptr<FieldsComparator>& user_defined_seq_comparator =
nullptr,
+ const std::shared_ptr<CompactManager>& compact_manager = nullptr)
const {
+ std::shared_ptr<CompactManager> writer_compact_manager =
+ compact_manager ? compact_manager : noop_compact_manager_;
+ std::shared_ptr<IOManager> io_manager =
+ GetParam() ? std::make_shared<IOManager>(temp_dir + "/tmp",
file_system_) : nullptr;
+ return MergeTreeWriter::Create(
+ last_sequence_number, primary_keys_, path_factory, key_comparator_,
+ user_defined_seq_comparator, merge_function_wrapper_, schema_id,
value_schema_, options,
+ writer_compact_manager, io_manager,
/*enable_multi_thread_spill=*/false, pool_);
+ }
+
+ private:
+ std::shared_ptr<MemoryPool> pool_;
+ std::shared_ptr<FileSystem> file_system_;
+ std::vector<DataField> value_fields_;
+ std::shared_ptr<arrow::Schema> value_schema_;
+ std::shared_ptr<arrow::DataType> value_type_;
+ std::vector<std::string> primary_keys_;
+ std::shared_ptr<arrow::DataType> write_type_;
+ std::shared_ptr<FieldsComparator> key_comparator_;
+ std::shared_ptr<MergeFunctionWrapper<KeyValue>> merge_function_wrapper_;
+ std::shared_ptr<NoopCompactManager> noop_compact_manager_;
+};
+
+TEST_P(MergeTreeWriterTest, TestSimple) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT,
"orc"}}));
+
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+ std::string uuid = path_factory->uuid_;
+
+ ASSERT_OK_AND_ASSIGN(auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/-1,
dir->Str(), path_factory,
+ /*schema_id=*/1, options));
+
+ // write batch
+ std::shared_ptr<arrow::Array> array1 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Lucy", 20, 1, 14.1],
+ ["Paul", 20, 1, null],
+ ["Alice", 10, 0, 13.1]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array1, /*row_kinds=*/{}, merge_writer.get());
+
+ // prepare commit
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit_increment,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ ASSERT_OK(merge_writer->Close());
+
+ // check data file exist and read ok
+ std::string expected_data_file_name = "data-" + uuid + "-0.orc";
+ std::string expected_data_file_path = dir->Str() + "/" +
expected_data_file_name;
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStatus> data_file_status,
+
options.GetFileSystem()->GetFileStatus(expected_data_file_path));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
arrow::ipc::internal::json::ChunkedArrayFromJSON(write_type_, {R"([
+ [2, 0, "Alice", 10, 0, 13.1],
+ [0, 0, "Lucy", 20, 1, 14.1],
+ [1, 0, "Paul", 20, 1, null]
+ ])"},
+
&expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckFileContent(expected_data_file_path, expected_array);
+
+ // check data file meta
+ ASSERT_TRUE(commit_increment.GetCompactIncrement().IsEmpty());
+ ASSERT_EQ(1, commit_increment.GetNewFilesIncrement().NewFiles().size());
+ auto expected_data_file_meta = std::make_shared<DataFileMeta>(
+ expected_data_file_name, /*file_size=*/data_file_status->GetLen(),
/*row_count=*/3,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({std::string("Alice")},
pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({std::string("Paul")},
pool_.get()),
+ /*key_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice")},
{std::string("Paul")}, {0},
+ pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice"), 10, 0, 13.1},
+ {std::string("Paul"), 20, 1, 14.1},
{0, 0, 0, 1},
+ pool_.get()),
+ /*min_sequence_number=*/0, /*max_sequence_number=*/2, /*schema_id=*/1,
+ /*level=*/0, /*extra_files=*/std::vector<std::optional<std::string>>(),
+
/*creation_time=*/commit_increment.GetNewFilesIncrement().NewFiles()[0]->creation_time,
+ /*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);
+ DataIncrement expected_data_increment({expected_data_file_meta},
/*deleted_files=*/{},
+ /*changelog_files=*/{});
+ ASSERT_EQ(expected_data_increment,
commit_increment.GetNewFilesIncrement());
+}
+
+TEST_P(MergeTreeWriterTest, TestWriteMultiBatch) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT,
"orc"}}));
+
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+ std::string uuid = path_factory->uuid_;
+
+ ASSERT_OK_AND_ASSIGN(auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/9,
dir->Str(), path_factory,
+ /*schema_id=*/0, options));
+ // batch1
+ std::shared_ptr<arrow::Array> array1 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Lucy", 20, 1, 14.1],
+ ["Paul", 20, 1, null],
+ ["Alice", 10, 0, 13.1],
+ ["Paul", 20, 1, 15.1]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array1, /*row_kinds=*/{}, merge_writer.get());
+ // batch2
+ std::shared_ptr<arrow::Array> array2 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Lucy", 20, 1, 114.1],
+ ["Skye", 10, 0, 118.1],
+ ["Alice", 10, 0, 113.1]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array2, /*row_kinds=*/{}, merge_writer.get());
+
+ // prepare commit
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit_increment,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ ASSERT_OK(merge_writer->Close());
+
+ // check data file exist and read ok
+ std::string expected_data_file_name = "data-" + uuid + "-0.orc";
+ std::string expected_data_file_path = dir->Str() + "/" +
expected_data_file_name;
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStatus> data_file_status,
+
options.GetFileSystem()->GetFileStatus(expected_data_file_path));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
arrow::ipc::internal::json::ChunkedArrayFromJSON(write_type_, {R"([
+ [16, 0, "Alice", 10, 0, 113.1],
+ [14, 0, "Lucy", 20, 1, 114.1],
+ [13, 0, "Paul", 20, 1, 15.1],
+ [15, 0, "Skye", 10, 0, 118.1]
+ ])"},
+
&expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckFileContent(expected_data_file_path, expected_array);
+
+ // check data file meta
+ ASSERT_TRUE(commit_increment.GetCompactIncrement().IsEmpty());
+ ASSERT_EQ(1, commit_increment.GetNewFilesIncrement().NewFiles().size());
+ auto expected_data_file_meta = std::make_shared<DataFileMeta>(
+ expected_data_file_name, /*file_size=*/data_file_status->GetLen(),
/*row_count=*/4,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({std::string("Alice")},
pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({std::string("Skye")},
pool_.get()),
+ /*key_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice")},
{std::string("Skye")}, {0},
+ pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice"), 10, 0, 15.1},
+ {std::string("Skye"), 20, 1, 118.1},
{0, 0, 0, 0},
+ pool_.get()),
+ /*min_sequence_number=*/13, /*max_sequence_number=*/16,
/*schema_id=*/0,
+ /*level=*/0, /*extra_files=*/std::vector<std::optional<std::string>>(),
+
/*creation_time=*/commit_increment.GetNewFilesIncrement().NewFiles()[0]->creation_time,
+ /*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);
+ DataIncrement expected_data_increment({expected_data_file_meta},
/*deleted_files=*/{},
+ /*changelog_files=*/{});
+ ASSERT_EQ(expected_data_increment,
commit_increment.GetNewFilesIncrement());
+}
+
+TEST_P(MergeTreeWriterTest, TestWriteWithDeleteRow) {
+ ASSERT_OK_AND_ASSIGN(
+ CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
{Options::SEQUENCE_FIELD, "f1"}}));
+
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+ std::string uuid = path_factory->uuid_;
+
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<FieldsComparator>
user_defined_seq_comparator,
+ FieldsComparator::Create({value_fields_[1]},
+
/*is_ascending_order=*/true));
+ assert(user_defined_seq_comparator);
+ ASSERT_OK_AND_ASSIGN(auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/9,
dir->Str(), path_factory,
+ /*schema_id=*/0, options,
user_defined_seq_comparator));
+ // batch1
+ std::shared_ptr<arrow::Array> array1 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Lucy", 20, 1, 14.1],
+ ["Paul", 20, 1, null],
+ ["Alice", 10, 0, 13.1],
+ ["Paul", 10, 1, 15.1]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array1,
+ {RecordBatch::RowKind::INSERT, RecordBatch::RowKind::INSERT,
+ RecordBatch::RowKind::DELETE, RecordBatch::RowKind::INSERT},
+ merge_writer.get());
+
+ // prepare commit
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit_increment,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ ASSERT_OK(merge_writer->Close());
+
+ // check data file exist and read ok
+ std::string expected_data_file_name = "data-" + uuid + "-0.orc";
+ std::string expected_data_file_path = dir->Str() + "/" +
expected_data_file_name;
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStatus> data_file_status,
+
options.GetFileSystem()->GetFileStatus(expected_data_file_path));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
arrow::ipc::internal::json::ChunkedArrayFromJSON(write_type_, {R"([
+ [12, 3, "Alice", 10, 0, 13.1],
+ [10, 0, "Lucy", 20, 1, 14.1],
+ [11, 0, "Paul", 20, 1, null]
+ ])"},
+
&expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckFileContent(expected_data_file_path, expected_array);
+
+ // check data file meta
+ ASSERT_TRUE(commit_increment.GetCompactIncrement().IsEmpty());
+ ASSERT_EQ(1, commit_increment.GetNewFilesIncrement().NewFiles().size());
+ auto expected_data_file_meta = std::make_shared<DataFileMeta>(
+ expected_data_file_name, /*file_size=*/data_file_status->GetLen(),
/*row_count=*/3,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({std::string("Alice")},
pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({std::string("Paul")},
pool_.get()),
+ /*key_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice")},
{std::string("Paul")}, {0},
+ pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice"), 10, 0, 13.1},
+ {std::string("Paul"), 20, 1, 14.1},
{0, 0, 0, 1},
+ pool_.get()),
+ /*min_sequence_number=*/10, /*max_sequence_number=*/12,
/*schema_id=*/0,
+ /*level=*/0, /*extra_files=*/std::vector<std::optional<std::string>>(),
+
/*creation_time=*/commit_increment.GetNewFilesIncrement().NewFiles()[0]->creation_time,
+ /*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);
+ DataIncrement expected_data_increment({expected_data_file_meta},
/*deleted_files=*/{},
+ /*changelog_files=*/{});
+ ASSERT_EQ(expected_data_increment,
commit_increment.GetNewFilesIncrement());
+}
+
+TEST_P(MergeTreeWriterTest, TestMultiplePrepareCommit) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
+ {"orc.write.enable-metrics",
"true"}}));
+
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+ std::string uuid = path_factory->uuid_;
+
+ ASSERT_OK_AND_ASSIGN(auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/9,
dir->Str(), path_factory,
+ /*schema_id=*/0, options));
+ // batch1
+ std::shared_ptr<arrow::Array> array1 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Lucy", 20, 1, 14.1],
+ ["Paul", 20, 1, null],
+ ["Alice", 10, 0, 13.1],
+ ["Paul", 20, 1, 15.1]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array1, /*row_kinds=*/{}, merge_writer.get());
+ // prepare commit1
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit_increment1,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ // check metrics
+ auto metrics = merge_writer->GetMetrics();
+ ASSERT_OK_AND_ASSIGN(uint64_t write_io_count,
metrics->GetCounter("orc.write.io.count"));
+ ASSERT_GT(write_io_count, 0);
+
+ // batch2
+ std::shared_ptr<arrow::Array> array2 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Lucy", 20, 1, 114.1],
+ ["Skye", 10, 0, 118.1],
+ ["Alice", 10, 0, 113.1]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array2, /*row_kinds=*/{}, merge_writer.get());
+ // prepare commit2
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit_increment2,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ // check metrics
+ metrics = merge_writer->GetMetrics();
+ ASSERT_OK_AND_ASSIGN(uint64_t write_io_count2,
metrics->GetCounter("orc.write.io.count"));
+ ASSERT_GT(write_io_count2, write_io_count);
+
+ ASSERT_OK(merge_writer->Close());
+
+ // check data file exist and read ok
+ std::string expected_data_file_name1 = "data-" + uuid + "-0.orc";
+ std::string expected_data_file_name2 = "data-" + uuid + "-1.orc";
+
+ std::string expected_data_file_dir = dir->Str() + "/";
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<FileStatus> data_file_status1,
+ options.GetFileSystem()->GetFileStatus(expected_data_file_dir +
expected_data_file_name1));
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<FileStatus> data_file_status2,
+ options.GetFileSystem()->GetFileStatus(expected_data_file_dir +
expected_data_file_name2));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array1;
+ auto array_status =
arrow::ipc::internal::json::ChunkedArrayFromJSON(write_type_, {R"([
+ [12, 0, "Alice", 10, 0, 13.1],
+ [10, 0, "Lucy", 20, 1, 14.1],
+ [13, 0, "Paul", 20, 1, 15.1]
+ ])"},
+
&expected_array1);
+ ASSERT_TRUE(array_status.ok());
+ CheckFileContent(expected_data_file_dir + expected_data_file_name1,
expected_array1);
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array2;
+ array_status =
arrow::ipc::internal::json::ChunkedArrayFromJSON(write_type_, {R"([
+ [16, 0, "Alice", 10, 0, 113.1],
+ [14, 0, "Lucy", 20, 1, 114.1],
+ [15, 0, "Skye", 10, 0, 118.1]
+ ])"},
+
&expected_array2);
+ ASSERT_TRUE(array_status.ok());
+ CheckFileContent(expected_data_file_dir + expected_data_file_name2,
expected_array2);
+
+ // check data file meta
+ ASSERT_TRUE(commit_increment1.GetCompactIncrement().IsEmpty());
+ ASSERT_TRUE(commit_increment2.GetCompactIncrement().IsEmpty());
+ ASSERT_EQ(1, commit_increment1.GetNewFilesIncrement().NewFiles().size());
+ ASSERT_EQ(1, commit_increment2.GetNewFilesIncrement().NewFiles().size());
+ auto expected_data_file_meta1 = std::make_shared<DataFileMeta>(
+ expected_data_file_name1, /*file_size=*/data_file_status1->GetLen(),
/*row_count=*/3,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({std::string("Alice")},
pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({std::string("Paul")},
pool_.get()),
+ /*key_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice")},
{std::string("Paul")}, {0},
+ pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice"), 10, 0, 13.1},
+ {std::string("Paul"), 20, 1, 15.1},
{0, 0, 0, 0},
+ pool_.get()),
+ /*min_sequence_number=*/10, /*max_sequence_number=*/13,
/*schema_id=*/0,
+ /*level=*/0, /*extra_files=*/std::vector<std::optional<std::string>>(),
+
/*creation_time=*/commit_increment1.GetNewFilesIncrement().NewFiles()[0]->creation_time,
+ /*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 expected_data_file_meta2 = std::make_shared<DataFileMeta>(
+ expected_data_file_name2, /*file_size=*/data_file_status2->GetLen(),
/*row_count=*/3,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({std::string("Alice")},
pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({std::string("Skye")},
pool_.get()),
+ /*key_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice")},
{std::string("Skye")}, {0},
+ pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice"), 10, 0, 113.1},
+ {std::string("Skye"), 20, 1, 118.1},
{0, 0, 0, 0},
+ pool_.get()),
+ /*min_sequence_number=*/14, /*max_sequence_number=*/16,
/*schema_id=*/0,
+ /*level=*/0, /*extra_files=*/std::vector<std::optional<std::string>>(),
+
/*creation_time=*/commit_increment2.GetNewFilesIncrement().NewFiles()[0]->creation_time,
+ /*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);
+ DataIncrement expected_data_increment1({expected_data_file_meta1},
+ /*deleted_files=*/{},
+ /*changelog_files=*/{});
+ ASSERT_EQ(expected_data_increment1,
commit_increment1.GetNewFilesIncrement());
+
+ DataIncrement expected_data_increment2({expected_data_file_meta2},
+ /*deleted_files=*/{},
+ /*changelog_files=*/{});
+ ASSERT_EQ(expected_data_increment2,
commit_increment2.GetNewFilesIncrement());
+}
+
+TEST_P(MergeTreeWriterTest, TestPrepareCommitForEmptyData) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT,
"orc"}}));
+
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+ std::string uuid = path_factory->uuid_;
+
+ ASSERT_OK_AND_ASSIGN(auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/-1,
dir->Str(), path_factory,
+ /*schema_id=*/0, options));
+
+ // prepare commit, without write
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit_increment,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ // check data file meta empty
+ ASSERT_TRUE(commit_increment.GetCompactIncrement().IsEmpty());
+ ASSERT_TRUE(commit_increment.GetNewFilesIncrement().NewFiles().empty());
+
+ // write empty batch
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_,
R"([])").ValueOrDie();
+ WriteBatch(array, /*row_kinds=*/{}, merge_writer.get());
+ // prepare commit, without write
+ ASSERT_OK_AND_ASSIGN(commit_increment,
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ // check data file meta empty
+ ASSERT_TRUE(commit_increment.GetCompactIncrement().IsEmpty());
+ ASSERT_TRUE(commit_increment.GetNewFilesIncrement().NewFiles().empty());
+
+ ASSERT_OK(merge_writer->Close());
+
+ // check data file not exist
+ std::string expected_data_file_name = "data-" + uuid + "-0.orc";
+ std::string expected_data_file_path = dir->Str() + "/" +
expected_data_file_name;
+
ASSERT_FALSE(options.GetFileSystem()->Exists(expected_data_file_path).value());
+}
+
+TEST_P(MergeTreeWriterTest, TestCloseBeforePrepareCommit) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT,
"orc"}}));
+
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+ std::string uuid = path_factory->uuid_;
+
+ ASSERT_OK_AND_ASSIGN(auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/-1,
dir->Str(), path_factory,
+ /*schema_id=*/0, options));
+
+ // write batch
+ std::shared_ptr<arrow::Array> array1 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Lucy", 20, 1, 14.1],
+ ["Paul", 20, 1, null],
+ ["Alice", 10, 0, 13.1]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array1, /*row_kinds=*/{}, merge_writer.get());
+ ASSERT_OK(merge_writer->Close());
+}
+
+TEST_P(MergeTreeWriterTest, TestCloseDeletesUncommittedFiles) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT,
"orc"}}));
+
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+ std::string uuid = path_factory->uuid_;
+
+ ASSERT_OK_AND_ASSIGN(auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/-1,
dir->Str(), path_factory,
+ /*schema_id=*/0, options));
+
+ std::shared_ptr<arrow::Array> array1 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Lucy", 20, 1, 14.1],
+ ["Paul", 20, 1, null],
+ ["Alice", 10, 0, 13.1]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array1, /*row_kinds=*/{}, merge_writer.get());
+
+ // Force a flush to materialize file on disk, but do not call
PrepareCommit.
+ ASSERT_OK(merge_writer->Compact(/*full_compaction=*/false));
+
+ std::string expected_data_file_path = dir->Str() + "/data-" + uuid +
"-0.orc";
+
ASSERT_TRUE(options.GetFileSystem()->Exists(expected_data_file_path).value());
+
+ ASSERT_OK(merge_writer->Close());
+
ASSERT_FALSE(options.GetFileSystem()->Exists(expected_data_file_path).value());
+}
+
+TEST_P(MergeTreeWriterTest, TestAutoFlush) {
+ // each batch is a file due to WRITE_BUFFER_SIZE
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
+ {Options::WRITE_BUFFER_SIZE,
"1"},
+
{Options::WRITE_BUFFER_SPILLABLE, "false"}}));
+
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+ std::string uuid = path_factory->uuid_;
+
+ ASSERT_OK_AND_ASSIGN(auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/9,
dir->Str(), path_factory,
+ /*schema_id=*/0, options));
+ // batch1
+ std::shared_ptr<arrow::Array> array1 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Lucy", 20, 1, 14.1],
+ ["Paul", 20, 1, null],
+ ["Alice", 10, 0, 13.1],
+ ["Paul", 20, 1, 15.1]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array1, /*row_kinds=*/{}, merge_writer.get());
+
+ // batch2
+ std::shared_ptr<arrow::Array> array2 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Lucy", 20, 1, 114.1],
+ ["Skye", 10, 0, 118.1],
+ ["Alice", 10, 0, 113.1]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array2, /*row_kinds=*/{}, merge_writer.get());
+ // prepare commit
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit_increment,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ ASSERT_OK(merge_writer->Close());
+
+ // check data file exist and read ok
+ std::string expected_data_file_name1 = "data-" + uuid + "-0.orc";
+ std::string expected_data_file_name2 = "data-" + uuid + "-1.orc";
+
+ std::string expected_data_file_dir = dir->Str() + "/";
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<FileStatus> data_file_status1,
+ options.GetFileSystem()->GetFileStatus(expected_data_file_dir +
expected_data_file_name1));
+ ASSERT_OK_AND_ASSIGN(
+ std::unique_ptr<FileStatus> data_file_status2,
+ options.GetFileSystem()->GetFileStatus(expected_data_file_dir +
expected_data_file_name2));
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array1;
+ auto array_status =
arrow::ipc::internal::json::ChunkedArrayFromJSON(write_type_, {R"([
+ [12, 0, "Alice", 10, 0, 13.1],
+ [10, 0, "Lucy", 20, 1, 14.1],
+ [13, 0, "Paul", 20, 1, 15.1]
+ ])"},
+
&expected_array1);
+ ASSERT_TRUE(array_status.ok());
+ CheckFileContent(expected_data_file_dir + expected_data_file_name1,
expected_array1);
+
+ std::shared_ptr<arrow::ChunkedArray> expected_array2;
+ array_status =
arrow::ipc::internal::json::ChunkedArrayFromJSON(write_type_, {R"([
+ [16, 0, "Alice", 10, 0, 113.1],
+ [14, 0, "Lucy", 20, 1, 114.1],
+ [15, 0, "Skye", 10, 0, 118.1]
+ ])"},
+
&expected_array2);
+ ASSERT_TRUE(array_status.ok());
+ CheckFileContent(expected_data_file_dir + expected_data_file_name2,
expected_array2);
+
+ // check data file meta
+ ASSERT_TRUE(commit_increment.GetCompactIncrement().IsEmpty());
+ ASSERT_EQ(2, commit_increment.GetNewFilesIncrement().NewFiles().size());
+ auto expected_data_file_meta1 = std::make_shared<DataFileMeta>(
+ expected_data_file_name1, /*file_size=*/data_file_status1->GetLen(),
/*row_count=*/3,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({std::string("Alice")},
pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({std::string("Paul")},
pool_.get()),
+ /*key_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice")},
{std::string("Paul")}, {0},
+ pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice"), 10, 0, 13.1},
+ {std::string("Paul"), 20, 1, 15.1},
{0, 0, 0, 0},
+ pool_.get()),
+ /*min_sequence_number=*/10, /*max_sequence_number=*/13,
/*schema_id=*/0,
+ /*level=*/0, /*extra_files=*/std::vector<std::optional<std::string>>(),
+
/*creation_time=*/commit_increment.GetNewFilesIncrement().NewFiles()[0]->creation_time,
+ /*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 expected_data_file_meta2 = std::make_shared<DataFileMeta>(
+ expected_data_file_name2, /*file_size=*/data_file_status2->GetLen(),
/*row_count=*/3,
+ /*min_key=*/BinaryRowGenerator::GenerateRow({std::string("Alice")},
pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({std::string("Skye")},
pool_.get()),
+ /*key_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice")},
{std::string("Skye")}, {0},
+ pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice"), 10, 0, 113.1},
+ {std::string("Skye"), 20, 1, 118.1},
{0, 0, 0, 0},
+ pool_.get()),
+ /*min_sequence_number=*/14, /*max_sequence_number=*/16,
/*schema_id=*/0,
+ /*level=*/0, /*extra_files=*/std::vector<std::optional<std::string>>(),
+
/*creation_time=*/commit_increment.GetNewFilesIncrement().NewFiles()[1]->creation_time,
+ /*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);
+ DataIncrement expected_data_increment({expected_data_file_meta1,
expected_data_file_meta2},
+ /*deleted_files=*/{},
+ /*changelog_files=*/{});
+ ASSERT_EQ(expected_data_increment,
commit_increment.GetNewFilesIncrement());
+}
+
+TEST_P(MergeTreeWriterTest, TestIOException) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT,
"orc"}}));
+
+ bool run_complete = false;
+ auto io_hook = IOHook::GetInstance();
+ for (size_t i = 0; i < 200; i++) {
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ ScopeGuard guard([&io_hook]() { io_hook->Clear(); });
+ io_hook->Reset(i, IOHook::Mode::RETURN_ERROR);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc",
options.DataFilePrefix(), nullptr));
+ std::string uuid = path_factory->uuid_;
+
+ auto merge_writer_result = CreateMergeWriter(
+ /*last_sequence_number=*/-1, dir->Str(), path_factory,
/*schema_id=*/0, options);
+ CHECK_HOOK_STATUS(merge_writer_result.status(), i);
+ auto merge_writer = std::move(merge_writer_result).value();
+
+ // write batch
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Lucy", 20, 1, 14.1],
+ ["Paul", 20, 1, null],
+ ["Alice", 10, 0, 13.1]
+ ])")
+ .ValueOrDie();
+
+ ::ArrowArray c_array;
+ ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
+ RecordBatchBuilder batch_builder(&c_array);
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
batch_builder.Finish());
+ CHECK_HOOK_STATUS(merge_writer->Write(std::move(batch)), i);
+ auto commit_increment =
merge_writer->PrepareCommit(/*wait_compaction=*/false);
+ CHECK_HOOK_STATUS(commit_increment.status(), i);
+
ASSERT_FALSE(commit_increment.value().GetNewFilesIncrement().NewFiles().empty());
+ ASSERT_OK(merge_writer->Close());
+ run_complete = true;
+ break;
+ }
+ ASSERT_TRUE(run_complete);
+}
+
+TEST_P(MergeTreeWriterTest, TestBulkData) {
+ // each batch is a file due to WRITE_BUFFER_SIZE
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
+ {Options::WRITE_BUFFER_SIZE,
"1"},
+
{Options::WRITE_BUFFER_SPILLABLE, "false"}}));
+
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+ std::string uuid = path_factory->uuid_;
+
+ ASSERT_OK_AND_ASSIGN(auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/-1,
dir->Str(), path_factory,
+ /*schema_id=*/0, options));
+ // multi batch
+ size_t batch_size = 500;
+ for (size_t i = 0; i < batch_size; ++i) {
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Lucy", 20, 1, 14.1],
+ ["Paul", 20, 1, null],
+ ["Alice", 10, 0, 13.1],
+ ["Paul", 20, 1, 15.1]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array, /*row_kinds=*/{}, merge_writer.get());
+ }
+
+ // prepare commit
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit_increment,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ ASSERT_OK(merge_writer->Close());
+
+ std::string expected_data_file_dir = dir->Str() + "/";
+ ASSERT_TRUE(commit_increment.GetCompactIncrement().IsEmpty());
+ ASSERT_EQ(batch_size,
commit_increment.GetNewFilesIncrement().NewFiles().size());
+
+ for (size_t i = 0; i < batch_size; ++i) {
+ std::string expected_data_file_name = "data-" + uuid + "-" +
std::to_string(i) + ".orc";
+ // check data file exist and read ok
+ ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStatus> data_file_status,
+
options.GetFileSystem()->GetFileStatus(expected_data_file_dir +
+
expected_data_file_name));
+ // check data file meta
+ auto expected_data_file_meta = std::make_shared<DataFileMeta>(
+ expected_data_file_name, /*file_size=*/data_file_status->GetLen(),
/*row_count=*/3,
+
/*min_key=*/BinaryRowGenerator::GenerateRow({std::string("Alice")},
pool_.get()),
+ /*max_key=*/BinaryRowGenerator::GenerateRow({std::string("Paul")},
pool_.get()),
+ /*key_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice")},
{std::string("Paul")}, {0},
+ pool_.get()),
+ /*value_stats=*/
+ BinaryRowGenerator::GenerateStats({std::string("Alice"), 10, 0,
13.1},
+ {std::string("Paul"), 20, 1,
15.1}, {0, 0, 0, 0},
+ pool_.get()),
+ /*min_sequence_number=*/i * 4, /*max_sequence_number=*/i * 4 + 3,
/*schema_id=*/0,
+ /*level=*/0,
/*extra_files=*/std::vector<std::optional<std::string>>(),
+
/*creation_time=*/commit_increment.GetNewFilesIncrement().NewFiles()[i]->creation_time,
+ /*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);
+ ASSERT_EQ(*commit_increment.GetNewFilesIncrement().NewFiles()[i],
*expected_data_file_meta);
+ }
+}
+
+TEST_P(MergeTreeWriterTest, TestShouldWait) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT,
"orc"}}));
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+
+ auto fake_compact_manager = std::make_shared<FakeCompactManager>();
+ ASSERT_OK_AND_ASSIGN(
+ auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/-1, dir->Str(),
path_factory, /*schema_id=*/0,
+ options, /*user_defined_seq_comparator=*/nullptr,
fake_compact_manager));
+
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Lucy", 20, 1, 14.1],
+ ["Paul", 20, 1, null],
+ ["Alice", 10, 0, 13.1]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array, /*row_kinds=*/{}, merge_writer.get());
+ ASSERT_TRUE(fake_compact_manager->get_result_blocking_calls.empty());
+
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit_increment,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ ASSERT_EQ(fake_compact_manager->get_result_blocking_calls.size(), 2u);
+ ASSERT_TRUE(fake_compact_manager->get_result_blocking_calls[0]);
+ ASSERT_TRUE(fake_compact_manager->get_result_blocking_calls[1]);
+ ASSERT_OK(merge_writer->Close());
+}
+
+TEST_P(MergeTreeWriterTest, TestUpdateCompactResultDeleteIntermediateFile) {
+ // TODO(lisizhuo.lsz): test UpdateCompactResult in inte compaction test.
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT,
"orc"}}));
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+
+ auto fake_compact_manager = std::make_shared<FakeCompactManager>();
+ ASSERT_OK_AND_ASSIGN(
+ auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/-1, dir->Str(),
path_factory, /*schema_id=*/0,
+ options, /*user_defined_seq_comparator=*/nullptr,
fake_compact_manager));
+
+ // Round 1: Before=[A], After=[X] => compact_before_=[A],
compact_after_=[X]
+ // Round 2: Before=[X], After=[Y] => X is in compact_after_, so it's an
intermediate file
+ auto file_a = CreateMeta("file_a", /*level=*/0);
+ auto file_x = CreateMeta("file_x", /*level=*/0);
+ auto file_y = CreateMeta("file_y", /*level=*/1);
+
+ merge_writer->compact_before_ = {file_a};
+ merge_writer->compact_after_ = {file_x};
+
+ auto before = std::vector<std::shared_ptr<DataFileMeta>>({file_x});
+ auto after = std::vector<std::shared_ptr<DataFileMeta>>({file_y});
+ auto compact_result = std::make_shared<CompactResult>(before, after);
+ ASSERT_OK(merge_writer->UpdateCompactResult(compact_result));
+ ASSERT_EQ(merge_writer->compact_before_,
std::vector<std::shared_ptr<DataFileMeta>>({file_a}));
+ ASSERT_EQ(merge_writer->compact_after_,
std::vector<std::shared_ptr<DataFileMeta>>({file_y}));
+}
+
+TEST_P(MergeTreeWriterTest, TestUpdateCompactResultWithFileInCompactAfter) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT,
"orc"}}));
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+
+ auto fake_compact_manager = std::make_shared<FakeCompactManager>();
+ ASSERT_OK_AND_ASSIGN(
+ auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/-1, dir->Str(),
path_factory, /*schema_id=*/0,
+ options, /*user_defined_seq_comparator=*/nullptr,
fake_compact_manager));
+
+ // Round 1: Before=[A], After=[X@level0] => compact_after_ = [X@level0]
+ // Round 2 (upgrade): Before=[X@level0], After=[X@level1]
+ // X is in compact_after_, but also in after_files => should NOT be
deleted.
+ auto file_a = CreateMeta("file_a", /*level=*/0);
+ auto file_x_level0 = CreateMeta("file_x_level0", /*level=*/0);
+ auto file_x_level1 = CreateMeta("file_x_level1", /*level=*/1);
+
+ merge_writer->compact_before_ = {file_a};
+ merge_writer->compact_after_ = {file_x_level0};
+
+ auto before = std::vector<std::shared_ptr<DataFileMeta>>({file_x_level0});
+ auto after = std::vector<std::shared_ptr<DataFileMeta>>({file_x_level1});
+ auto compact_result = std::make_shared<CompactResult>(before, after);
+ ASSERT_OK(merge_writer->UpdateCompactResult(compact_result));
+ ASSERT_EQ(merge_writer->compact_before_,
std::vector<std::shared_ptr<DataFileMeta>>({file_a}));
+ ASSERT_EQ(merge_writer->compact_after_,
+ std::vector<std::shared_ptr<DataFileMeta>>({file_x_level1}));
+}
+
+TEST_P(MergeTreeWriterTest, TestUpdateCompactResultWithFileInCompactBefore) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT,
"orc"}}));
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+
+ auto fake_compact_manager = std::make_shared<FakeCompactManager>();
+ ASSERT_OK_AND_ASSIGN(
+ auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/-1, dir->Str(),
path_factory, /*schema_id=*/0,
+ options, /*user_defined_seq_comparator=*/nullptr,
fake_compact_manager));
+
+ // Round 1 (upgrade): Before=[X@level0], After=[X@level1]
+ // X is not in compact_after_ yet, so it goes to compact_before_ = [X].
+ // compact_after_ = [X@level1].
+ // Round 2: Before=[X@level1], After=[Y]
+ // X@level1 is in compact_after_, so it's an intermediate file candidate.
+ // But in_compact_before(X) is true (from round 1), so X should NOT be
deleted.
+ auto file_x = CreateMeta("file_x", /*level=*/0);
+ auto file_x_level1 = CreateMeta("file_x_level1", /*level=*/1);
+ auto file_y = CreateMeta("file_y", /*level=*/1);
+
+ merge_writer->compact_before_ = {file_x};
+ merge_writer->compact_after_ = {file_x_level1};
+
+ auto before = std::vector<std::shared_ptr<DataFileMeta>>({file_x_level1});
+ auto after = std::vector<std::shared_ptr<DataFileMeta>>({file_y});
+ auto compact_result = std::make_shared<CompactResult>(before, after);
+ ASSERT_OK(merge_writer->UpdateCompactResult(compact_result));
+ ASSERT_EQ(merge_writer->compact_before_,
std::vector<std::shared_ptr<DataFileMeta>>({file_x}));
+ ASSERT_EQ(merge_writer->compact_after_,
std::vector<std::shared_ptr<DataFileMeta>>({file_y}));
+}
+
+TEST_P(MergeTreeWriterTest,
TestCloseSkipsDeleteForUpgradedFilesInCompactAfter) {
+ // Verifies that DoClose does NOT delete files in compact_after_ that also
appear
+ // in compact_before_ (i.e., upgraded files required by previous
snapshots).
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT,
"orc"}}));
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+
+ auto fake_compact_manager = std::make_shared<FakeCompactManager>();
+ ASSERT_OK_AND_ASSIGN(
+ auto merge_writer,
+ CreateMergeWriter(/*last_sequence_number=*/-1, dir->Str(),
path_factory, /*schema_id=*/0,
+ options, /*user_defined_seq_comparator=*/nullptr,
fake_compact_manager));
+
+ // Create real files on disk to verify deletion behavior
+ std::string upgraded_file_name = "data-upgraded-0.orc";
+ std::string intermediate_file_name = "data-intermediate-0.orc";
+ std::string upgraded_file_path = dir->Str() + "/" + upgraded_file_name;
+ std::string intermediate_file_path = dir->Str() + "/" +
intermediate_file_name;
+
+ // Create placeholder files on disk
+ ASSERT_OK_AND_ASSIGN(auto out1,
+ options.GetFileSystem()->Create(upgraded_file_path,
/*overwrite=*/true));
+ ASSERT_OK(out1->Close());
+ ASSERT_OK_AND_ASSIGN(auto out2,
options.GetFileSystem()->Create(intermediate_file_path,
+
/*overwrite=*/true));
+ ASSERT_OK(out2->Close());
+
+ ASSERT_TRUE(options.GetFileSystem()->Exists(upgraded_file_path).value());
+
ASSERT_TRUE(options.GetFileSystem()->Exists(intermediate_file_path).value());
+
+ auto upgraded_file = CreateMeta(upgraded_file_name, /*level=*/1);
+ auto intermediate_file = CreateMeta(intermediate_file_name, /*level=*/1);
+
+ // Setup: upgraded_file appears in both compact_before_ and compact_after_
+ // (simulating an upgrade operation where the file is promoted to a higher
level).
+ // intermediate_file only appears in compact_after_ (normal compaction
output).
+ merge_writer->compact_before_ = {upgraded_file};
+ merge_writer->compact_after_ = {upgraded_file, intermediate_file};
+
+ ASSERT_OK(merge_writer->Close());
+
+ // upgraded_file should NOT be deleted (it's in compact_before_)
+ ASSERT_TRUE(options.GetFileSystem()->Exists(upgraded_file_path).value())
+ << "Upgraded file should be preserved because it exists in
compact_before_";
+
+ // intermediate_file SHOULD be deleted (it's only in compact_after_)
+
ASSERT_FALSE(options.GetFileSystem()->Exists(intermediate_file_path).value())
+ << "Intermediate file should be deleted because it's not in
compact_before_";
+}
+
+TEST_F(MergeTreeWriterTest, TestSpillWithSameKeyDeduplicate) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
+ {Options::WRITE_BUFFER_SIZE,
"1"},
+ {Options::WRITE_ONLY,
"true"}}));
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+ std::string uuid = path_factory->uuid_;
+
+ std::shared_ptr<IOManager> io_manager =
+ std::make_shared<IOManager>(dir->Str() + "/tmp", file_system_);
+ ASSERT_OK_AND_ASSIGN(
+ auto merge_writer,
+ MergeTreeWriter::Create(/*last_sequence_number=*/-1, primary_keys_,
path_factory,
+ key_comparator_,
/*user_defined_seq_comparator=*/nullptr,
+ merge_function_wrapper_, /*schema_id=*/0,
value_schema_, options,
+ noop_compact_manager_, io_manager,
+ /*enable_multi_thread_spill=*/false, pool_));
+
+ std::shared_ptr<arrow::Array> batch1 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Alice", 1, 0, 1.0],
+ ["Bob", 2, 0, 2.0]
+ ])")
+ .ValueOrDie();
+ std::shared_ptr<arrow::Array> batch2 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Alice", 10, 0, 10.0],
+ ["Charlie", 3, 0, 3.0]
+ ])")
+ .ValueOrDie();
+
+ WriteBatch(batch1, /*row_kinds=*/{}, merge_writer.get());
+ WriteBatch(batch2, /*row_kinds=*/{}, merge_writer.get());
+ // WRITE_BUFFER_SIZE=1 causes UpdateSpillParameters() to clamp
actual_max_fan_in_ to 2,
+ // triggering leveled merge after 2 spill files are produced, merging them
into 1.
+ ASSERT_EQ(1u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+
+ std::shared_ptr<arrow::Array> batch3 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Bob", 20, 0, 20.0],
+ ["Charlie", 30, 0, 30.0]
+ ])")
+ .ValueOrDie();
+ WriteBatch(batch3, /*row_kinds=*/{}, merge_writer.get());
+
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit_increment,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ ASSERT_EQ(0u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+ ASSERT_OK(merge_writer->Close());
+
+ // All three keys deduplicated: Alice(seq=2), Bob(seq=4), Charlie(seq=5).
+ ASSERT_EQ(1, commit_increment.GetNewFilesIncrement().NewFiles().size());
+ std::string expected_data_file_path = dir->Str() + "/data-" + uuid +
"-0.orc";
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
arrow::ipc::internal::json::ChunkedArrayFromJSON(write_type_, {R"([
+ [2, 0, "Alice", 10, 0, 10.0],
+ [4, 0, "Bob", 20, 0, 20.0],
+ [5, 0, "Charlie", 30, 0, 30.0]
+ ])"},
+
&expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckFileContent(expected_data_file_path, expected_array);
+}
+
+TEST_F(MergeTreeWriterTest, TestIntermediateMergeSpillFileBound) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
+ {Options::WRITE_BUFFER_SIZE,
"1"},
+
{Options::LOCAL_SORT_MAX_NUM_FILE_HANDLES, "2"},
+ {Options::WRITE_ONLY,
"true"}}));
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+ std::string uuid = path_factory->uuid_;
+
+ std::shared_ptr<IOManager> io_manager =
+ std::make_shared<IOManager>(dir->Str() + "/tmp", file_system_);
+ ASSERT_OK_AND_ASSIGN(
+ auto merge_writer,
+ MergeTreeWriter::Create(/*last_sequence_number=*/-1, primary_keys_,
path_factory,
+ key_comparator_,
/*user_defined_seq_comparator=*/nullptr,
+ merge_function_wrapper_, /*schema_id=*/0,
value_schema_, options,
+ noop_compact_manager_, io_manager,
+ /*enable_multi_thread_spill=*/false, pool_));
+
+ std::shared_ptr<arrow::Array> batch1 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Alice", 1, 0, 1.0]
+ ])")
+ .ValueOrDie();
+ std::shared_ptr<arrow::Array> batch2 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Bob", 2, 0, 2.0]
+ ])")
+ .ValueOrDie();
+ std::shared_ptr<arrow::Array> batch3 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Alice", 3, 0, 3.0]
+ ])")
+ .ValueOrDie();
+
+ WriteBatch(batch1, /*row_kinds=*/{}, merge_writer.get());
+ // Level 0: [A], total = 1
+ ASSERT_EQ(1u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+
+ WriteBatch(batch2, /*row_kinds=*/{}, merge_writer.get());
+ // Level 0: [A,B] hits max_fan_in=2, merge -> Level 0: [], Level 1: [C],
total = 1
+ ASSERT_EQ(1u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+
+ WriteBatch(batch3, /*row_kinds=*/{}, merge_writer.get());
+ // Level 0: [D], Level 1: [C], total = 2 (no single level exceeds
max_fan_in)
+ ASSERT_EQ(2u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit_increment,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ ASSERT_EQ(0u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+ ASSERT_OK(merge_writer->Close());
+
+ ASSERT_EQ(1, commit_increment.GetNewFilesIncrement().NewFiles().size());
+ std::string expected_data_file_path = dir->Str() + "/data-" + uuid +
"-0.orc";
+ std::shared_ptr<arrow::ChunkedArray> expected_array;
+ auto array_status =
arrow::ipc::internal::json::ChunkedArrayFromJSON(write_type_, {R"([
+ [2, 0, "Alice", 3, 0, 3.0],
+ [1, 0, "Bob", 2, 0, 2.0]
+ ])"},
+
&expected_array);
+ ASSERT_TRUE(array_status.ok());
+ CheckFileContent(expected_data_file_path, expected_array);
+}
+
+TEST_F(MergeTreeWriterTest, TestDiskQuotaExhaustedFallsBackToFlushWriteBuffer)
{
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
+ {Options::WRITE_BUFFER_SIZE,
"1"},
+
{Options::WRITE_BUFFER_SPILL_MAX_DISK_SIZE, "1"},
+ {Options::WRITE_ONLY,
"true"}}));
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+
+ std::shared_ptr<IOManager> io_manager =
+ std::make_shared<IOManager>(dir->Str() + "/tmp", file_system_);
+ ASSERT_OK_AND_ASSIGN(
+ auto merge_writer,
+ MergeTreeWriter::Create(/*last_sequence_number=*/-1, primary_keys_,
path_factory,
+ key_comparator_,
/*user_defined_seq_comparator=*/nullptr,
+ merge_function_wrapper_, /*schema_id=*/0,
value_schema_, options,
+ noop_compact_manager_, io_manager,
+ /*enable_multi_thread_spill=*/false, pool_));
+
+ // Phase 1: Manual FlushMemory path — disk quota exhausted causes fallback.
+ std::shared_ptr<arrow::Array> array1 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Alice", 1, 0, 1.0],
+ ["Bob", 2, 0, 2.0],
+ ["Charlie", 3, 0, 3.0]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array1, /*row_kinds=*/{}, merge_writer.get());
+
+ ASSERT_EQ(merge_writer->GetMemoryUsage(), 0);
+
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit1,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ ASSERT_EQ(0u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+ ASSERT_EQ(1, commit1.GetNewFilesIncrement().NewFiles().size());
+ ASSERT_EQ(3, commit1.GetNewFilesIncrement().NewFiles()[0]->row_count);
+
+ // Phase 2: Auto-spill path — WRITE_BUFFER_SIZE=1 triggers spill on each
WriteBatch.
+ // batch1 spills successfully, but disk quota is now exhausted.
+ std::shared_ptr<arrow::Array> batch1 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Dave", 4, 0, 4.0]
+ ])")
+ .ValueOrDie();
+ WriteBatch(batch1, /*row_kinds=*/{}, merge_writer.get());
+
+ // batch2: spill -> quota exhausted -> FlushWriteBuffer produces a data
file.
+ std::shared_ptr<arrow::Array> batch2 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Eve", 5, 0, 5.0]
+ ])")
+ .ValueOrDie();
+ WriteBatch(batch2, /*row_kinds=*/{}, merge_writer.get());
+
+ // batch3: another round after flush, accumulates into a fresh buffer.
+ std::shared_ptr<arrow::Array> batch3 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Frank", 6, 0, 6.0]
+ ])")
+ .ValueOrDie();
+ WriteBatch(batch3, /*row_kinds=*/{}, merge_writer.get());
+
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit2,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ ASSERT_EQ(0u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+ ASSERT_OK(merge_writer->Close());
+
+ ASSERT_EQ(3, commit2.GetNewFilesIncrement().NewFiles().size());
+ for (const auto& file_meta : commit2.GetNewFilesIncrement().NewFiles()) {
+ ASSERT_EQ(1, file_meta->row_count);
+ }
+}
+
+TEST_F(MergeTreeWriterTest,
TestFlushMemoryQuotaExhaustedFallsBackToFlushWriteBuffer) {
+ // WRITE_BUFFER_SIZE is large enough so WriteBatch does NOT auto-spill.
+ // SPILL_MAX_DISK_SIZE is tiny so the first FlushMemory() exhausts the
quota,
+ // triggering the fallback path: FlushMemory() -> quota exhausted ->
FlushWriteBuffer.
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
+ {Options::WRITE_BUFFER_SIZE,
"4096000"},
+
{Options::WRITE_BUFFER_SPILL_MAX_DISK_SIZE, "1b"},
+ {Options::WRITE_ONLY,
"true"}}));
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+
+ std::shared_ptr<IOManager> io_manager =
+ std::make_shared<IOManager>(dir->Str() + "/tmp", file_system_);
+ ASSERT_OK_AND_ASSIGN(
+ auto merge_writer,
+ MergeTreeWriter::Create(/*last_sequence_number=*/-1, primary_keys_,
path_factory,
+ key_comparator_,
/*user_defined_seq_comparator=*/nullptr,
+ merge_function_wrapper_, /*schema_id=*/0,
value_schema_, options,
+ noop_compact_manager_, io_manager,
+ /*enable_multi_thread_spill=*/false, pool_));
+
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Alice", 1, 0, 1.0],
+ ["Bob", 2, 0, 2.0]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array, /*row_kinds=*/{}, merge_writer.get());
+
+ ASSERT_GT(merge_writer->GetMemoryUsage(), 0);
+
+ // FlushMemory: spill succeeds but disk quota is exhausted -> falls back
to FlushWriteBuffer.
+ ASSERT_OK(merge_writer->FlushMemory());
+ ASSERT_EQ(merge_writer->GetMemoryUsage(), 0);
+ ASSERT_EQ(0u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+
+ // PrepareCommit should produce a data file (from FlushWriteBuffer
fallback).
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ ASSERT_EQ(0u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+ ASSERT_EQ(1, commit.GetNewFilesIncrement().NewFiles().size());
+ ASSERT_EQ(2, commit.GetNewFilesIncrement().NewFiles()[0]->row_count);
+ ASSERT_OK(merge_writer->Close());
+}
+
+TEST_F(MergeTreeWriterTest, TestCloseDeletesSpillTempFiles) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
+ {Options::WRITE_BUFFER_SIZE,
"1"},
+ {Options::WRITE_ONLY,
"true"}}));
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+
+ std::shared_ptr<IOManager> io_manager =
+ std::make_shared<IOManager>(dir->Str() + "/tmp", file_system_);
+ ASSERT_OK_AND_ASSIGN(
+ auto merge_writer,
+ MergeTreeWriter::Create(/*last_sequence_number=*/-1, primary_keys_,
path_factory,
+ key_comparator_,
/*user_defined_seq_comparator=*/nullptr,
+ merge_function_wrapper_, /*schema_id=*/0,
value_schema_, options,
+ noop_compact_manager_, io_manager,
+ /*enable_multi_thread_spill=*/false, pool_));
+
+ std::shared_ptr<arrow::Array> array =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Alice", 1, 0, 1.0],
+ ["Bob", 2, 0, 2.0]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array, /*row_kinds=*/{}, merge_writer.get());
+
+ ASSERT_EQ(1u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+ ASSERT_OK(merge_writer->Close());
+ ASSERT_EQ(0u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+}
+
+TEST_F(MergeTreeWriterTest, TestMultiplePrepareCommitWithSpill) {
+ ASSERT_OK_AND_ASSIGN(
+ CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
{Options::WRITE_ONLY, "true"}}));
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc", options.DataFilePrefix(),
nullptr));
+ std::string uuid = path_factory->uuid_;
+
+ std::shared_ptr<IOManager> io_manager =
+ std::make_shared<IOManager>(dir->Str() + "/tmp", file_system_);
+ ASSERT_OK_AND_ASSIGN(
+ auto merge_writer,
+ MergeTreeWriter::Create(/*last_sequence_number=*/-1, primary_keys_,
path_factory,
+ key_comparator_,
/*user_defined_seq_comparator=*/nullptr,
+ merge_function_wrapper_, /*schema_id=*/0,
value_schema_, options,
+ noop_compact_manager_, io_manager,
+ /*enable_multi_thread_spill=*/false, pool_));
+
+ std::shared_ptr<arrow::Array> array1 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Alice", 1, 0, 1.0],
+ ["Bob", 2, 0, 2.0]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array1, /*row_kinds=*/{}, merge_writer.get());
+ ASSERT_OK(merge_writer->FlushMemory());
+ ASSERT_EQ(1u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit1,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ ASSERT_EQ(0u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+ ASSERT_EQ(1, commit1.GetNewFilesIncrement().NewFiles().size());
+
+ std::string expected_path1 = dir->Str() + "/data-" + uuid + "-0.orc";
+ std::shared_ptr<arrow::ChunkedArray> expected_array1;
+ auto status1 =
arrow::ipc::internal::json::ChunkedArrayFromJSON(write_type_, {R"([
+ [0, 0, "Alice", 1, 0, 1.0],
+ [1, 0, "Bob", 2, 0, 2.0]
+ ])"},
+
&expected_array1);
+ ASSERT_TRUE(status1.ok());
+ CheckFileContent(expected_path1, expected_array1);
+
+ std::shared_ptr<arrow::Array> array2 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Dave", 4, 0, 4.0],
+ ["Eve", 5, 0, 5.0]
+ ])")
+ .ValueOrDie();
+ WriteBatch(array2, /*row_kinds=*/{}, merge_writer.get());
+ ASSERT_OK(merge_writer->FlushMemory());
+ ASSERT_EQ(1u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+
+ ASSERT_OK_AND_ASSIGN(CommitIncrement commit2,
+
merge_writer->PrepareCommit(/*wait_compaction=*/false));
+ ASSERT_EQ(0u, TestHelper::CountChannelFiles(file_system_, dir->Str() +
"/tmp"));
+ ASSERT_EQ(1, commit2.GetNewFilesIncrement().NewFiles().size());
+
+ std::string expected_path2 = dir->Str() + "/data-" + uuid + "-1.orc";
+ std::shared_ptr<arrow::ChunkedArray> expected_array2;
+ auto status2 =
arrow::ipc::internal::json::ChunkedArrayFromJSON(write_type_, {R"([
+ [2, 0, "Dave", 4, 0, 4.0],
+ [3, 0, "Eve", 5, 0, 5.0]
+ ])"},
+
&expected_array2);
+ ASSERT_TRUE(status2.ok());
+ CheckFileContent(expected_path2, expected_array2);
+
+ ASSERT_OK(merge_writer->Close());
+}
+
+TEST_F(MergeTreeWriterTest, TestSpillWithIOException) {
+ ASSERT_OK_AND_ASSIGN(CoreOptions options,
+ CoreOptions::FromMap({{Options::FILE_FORMAT, "orc"},
+ {Options::WRITE_BUFFER_SIZE,
"1"},
+ {Options::WRITE_ONLY, "true"},
+
{Options::LOCAL_SORT_MAX_NUM_FILE_HANDLES, "2"}}));
+
+ bool run_complete = false;
+ auto io_hook = IOHook::GetInstance();
+ for (size_t i = 0; i < 2000; i++) {
+ auto dir = UniqueTestDirectory::Create();
+ ASSERT_TRUE(dir);
+ auto path_factory = std::make_shared<DataFilePathFactory>();
+ ASSERT_OK(path_factory->Init(dir->Str(), "orc",
options.DataFilePrefix(), nullptr));
+
+ std::shared_ptr<IOManager> io_manager =
+ std::make_shared<IOManager>(dir->Str() + "/tmp", file_system_);
+ ASSERT_OK_AND_ASSIGN(
+ auto merge_writer,
+ MergeTreeWriter::Create(/*last_sequence_number=*/-1,
primary_keys_, path_factory,
+ key_comparator_,
/*user_defined_seq_comparator=*/nullptr,
+ merge_function_wrapper_, /*schema_id=*/0,
value_schema_,
+ options, noop_compact_manager_, io_manager,
+ /*enable_multi_thread_spill=*/false,
pool_));
+
+ ScopeGuard guard([&io_hook]() { io_hook->Clear(); });
+ io_hook->Reset(i, IOHook::Mode::RETURN_ERROR);
+ // Write 4 batches, each with 2 rows sharing the same key to exercise
deduplication.
+ // Batch 1: triggers spill file 1
+ std::shared_ptr<arrow::Array> batch1 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Alice", 1, 0, 1.0],
+ ["Bob", 2, 0, 2.0]
+ ])")
+ .ValueOrDie();
+ auto b1 = CreateBatch(batch1, {});
+ CHECK_HOOK_STATUS(merge_writer->Write(std::move(b1)), i);
+
+ // Batch 2: triggers spill file 2 → intermediate merge (merge 2 files
into 1)
+ std::shared_ptr<arrow::Array> batch2 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Alice", 10, 0, 10.0],
+ ["Charlie", 3, 0, 3.0]
+ ])")
+ .ValueOrDie();
+ auto b2 = CreateBatch(batch2, {});
+ CHECK_HOOK_STATUS(merge_writer->Write(std::move(b2)), i);
+
+ // Batch 3: triggers spill file at level 0 again
+ std::shared_ptr<arrow::Array> batch3 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Bob", 20, 0, 20.0],
+ ["Dave", 4, 0, 4.0]
+ ])")
+ .ValueOrDie();
+ auto b3 = CreateBatch(batch3, {});
+ CHECK_HOOK_STATUS(merge_writer->Write(std::move(b3)), i);
+
+ // Batch 4: triggers spill file at level 0 → another merge at level 0,
+ // then level 1 has 2 files → merge at level 1 as well.
+ std::shared_ptr<arrow::Array> batch4 =
+ arrow::ipc::internal::json::ArrayFromJSON(value_type_, R"([
+ ["Charlie", 30, 0, 30.0],
+ ["Eve", 5, 0, 5.0]
+ ])")
+ .ValueOrDie();
+ auto b4 = CreateBatch(batch4, {});
+ CHECK_HOOK_STATUS(merge_writer->Write(std::move(b4)), i);
+
+ // PrepareCommit: triggers FlushWriteBuffer → CreateReaders
(RunFinalCleanupIfNeeded)
+ // → sort merge → write output data file
+ auto commit_increment =
merge_writer->PrepareCommit(/*wait_compaction=*/false);
+ CHECK_HOOK_STATUS(commit_increment.status(), i);
+
ASSERT_FALSE(commit_increment.value().GetNewFilesIncrement().NewFiles().empty());
+
+ // Verify deduplication: Alice(seq=2), Bob(seq=4), Charlie(seq=5),
Dave(seq=6), Eve(seq=7)
+ ASSERT_EQ(1,
commit_increment.value().GetNewFilesIncrement().NewFiles().size());
+ ASSERT_EQ(5,
commit_increment.value().GetNewFilesIncrement().NewFiles()[0]->row_count);
+
+ ASSERT_OK(merge_writer->Close());
+ run_complete = true;
+ break;
+ }
+ ASSERT_TRUE(run_complete);
+}
+
+INSTANTIATE_TEST_SUITE_P(WithOptionalIOManager, MergeTreeWriterTest,
+ ::testing::Values(false, true));
+
+} // namespace paimon::test