blob: e520d43e9da60e4af72f0a3780c0fa5ca4c592e5 [file]
/*
* 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 <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <deque>
#include <functional>
#include <future>
#include <iterator>
#include <limits>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <set>
#include <string>
#include <thread>
#include <tuple>
#include <utility>
#include <vector>
#include "arrow/api.h"
#include "arrow/c/bridge.h"
#include "arrow/ipc/json_simple.h"
#include "fmt/format.h"
#include "gtest/gtest.h"
#include "paimon/catalog/catalog.h"
#include "paimon/catalog/identifier.h"
#include "paimon/commit_context.h"
#include "paimon/common/data/variant/variant_type_utils.h"
#include "paimon/common/table/special_fields.h"
#include "paimon/common/types/data_field.h"
#include "paimon/common/utils/path_util.h"
#include "paimon/common/utils/scope_guard.h"
#include "paimon/core/core_options.h"
#include "paimon/core/deletionvectors/bitmap_deletion_vector.h"
#include "paimon/core/deletionvectors/deletion_vectors_index_file.h"
#include "paimon/core/operation/commit/manifest_entry_changes.h"
#include "paimon/core/operation/commit/realtime_commit_properties.h"
#include "paimon/core/operation/commit/realtime_deduplicate_compaction_changes_provider.h"
#include "paimon/core/operation/metrics/commit_metrics.h"
#include "paimon/core/realtime/realtime_context_impl.h"
#include "paimon/core/realtime/realtime_schema_layout.h"
#include "paimon/core/schema/schema_manager.h"
#include "paimon/core/schema/table_schema.h"
#include "paimon/core/table/sink/commit_message_impl.h"
#include "paimon/core/table/source/data_split_impl.h"
#include "paimon/core/table/source/realtime_split.h"
#include "paimon/core/utils/snapshot_manager.h"
#include "paimon/data/shredding/map_shared_shredding_schema_utils.h"
#include "paimon/data/variant.h"
#include "paimon/defs.h"
#include "paimon/file_store_commit.h"
#include "paimon/file_store_write.h"
#include "paimon/fs/file_system.h"
#include "paimon/fs/local/local_file_system.h"
#include "paimon/memory/memory_pool.h"
#include "paimon/orphan_files_cleaner.h"
#include "paimon/predicate/function.h"
#include "paimon/predicate/predicate.h"
#include "paimon/predicate/predicate_builder.h"
#include "paimon/read_context.h"
#include "paimon/reader/count_reader.h"
#include "paimon/realtime/arrow_realtime_store_factory.h"
#include "paimon/realtime/realtime_context.h"
#include "paimon/realtime/realtime_snapshot_offsets.h"
#include "paimon/realtime/realtime_store.h"
#include "paimon/record_batch.h"
#include "paimon/scan_context.h"
#include "paimon/table/source/table_read.h"
#include "paimon/table/source/table_scan.h"
#include "paimon/testing/mock/mock_index_path_factory.h"
#include "paimon/testing/utils/blocking_file_system.h"
#include "paimon/testing/utils/dict_array_converter.h"
#include "paimon/testing/utils/read_result_collector.h"
#include "paimon/testing/utils/test_helper.h"
#include "paimon/testing/utils/testharness.h"
#include "paimon/testing/utils/variant_test_data.h"
#include "paimon/write_context.h"
namespace paimon::test {
namespace {
class TrackingRealtimeReadView final : public RealtimeReadView {
public:
explicit TrackingRealtimeReadView(std::shared_ptr<RealtimeReadView> delegate)
: delegate_(std::move(delegate)) {}
std::optional<OffsetRange> GetOffsetRange() const override {
return delegate_->GetOffsetRange();
}
const std::shared_ptr<RealtimeReadView>& Delegate() const {
return delegate_;
}
private:
std::shared_ptr<RealtimeReadView> delegate_;
};
class DelegatingRealtimeStore : public RealtimeStore {
public:
explicit DelegatingRealtimeStore(const std::shared_ptr<RealtimeStore>& delegate)
: delegate_(delegate) {}
Status Write(RealtimeWriteBatch&& batch) override {
return delegate_->Write(std::move(batch));
}
Result<std::optional<std::shared_ptr<RealtimeSegmentHandle>>> SealForCommit() override {
return delegate_->SealForCommit();
}
Result<std::vector<std::unique_ptr<BatchReader>>> CreateCommitReaders(
const std::shared_ptr<RealtimeSegmentHandle>& segment) override {
return delegate_->CreateCommitReaders(segment);
}
Result<std::shared_ptr<RealtimeReadView>> AcquireReadView() override {
return delegate_->AcquireReadView();
}
Result<std::vector<std::unique_ptr<BatchReader>>> CreateQueryReaders(
const std::shared_ptr<RealtimeReadView>& view,
const RealtimeQueryContext& context) override {
return delegate_->CreateQueryReaders(view, context);
}
Status AdvanceCommittedOffset(int64_t committed_offset) override {
return delegate_->AdvanceCommittedOffset(committed_offset);
}
RealtimeStoreDataUsage GetDataUsage() const override {
return delegate_->GetDataUsage();
}
uint64_t GetMemoryUsage() const override {
return delegate_->GetMemoryUsage();
}
protected:
std::shared_ptr<RealtimeStore> delegate_;
};
class DecoratingRealtimeStoreFactory final : public RealtimeStoreFactory {
public:
using Decorator =
std::function<std::shared_ptr<RealtimeStore>(const std::shared_ptr<RealtimeStore>&)>;
explicit DecoratingRealtimeStoreFactory(Decorator decorator)
: decorator_(std::move(decorator)) {}
Result<std::shared_ptr<RealtimeStore>> Create(RealtimeStoreCreateRequest&& request) override {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<RealtimeStore> delegate,
delegate_.Create(std::move(request)));
return decorator_(delegate);
}
private:
ArrowRealtimeStoreFactory delegate_;
Decorator decorator_;
};
template <typename Store, typename... Args>
std::shared_ptr<RealtimeStoreFactory> MakeDecoratingFactory(Args... args) {
return std::make_shared<DecoratingRealtimeStoreFactory>(
[=](const std::shared_ptr<RealtimeStore>& delegate) -> std::shared_ptr<RealtimeStore> {
return std::make_shared<Store>(delegate, args...);
});
}
class QueryTrackingRealtimeStore final : public DelegatingRealtimeStore {
public:
QueryTrackingRealtimeStore(const std::shared_ptr<RealtimeStore>& delegate,
const std::shared_ptr<std::atomic<bool>>& saw_query_predicate,
const std::shared_ptr<std::weak_ptr<RealtimeReadView>>& query_view)
: DelegatingRealtimeStore(delegate),
saw_query_predicate_(saw_query_predicate),
query_view_(query_view) {}
Result<std::shared_ptr<RealtimeReadView>> AcquireReadView() override {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<RealtimeReadView> delegate_view,
delegate_->AcquireReadView());
return std::shared_ptr<RealtimeReadView>(
std::make_shared<TrackingRealtimeReadView>(delegate_view));
}
Result<std::vector<std::unique_ptr<BatchReader>>> CreateQueryReaders(
const std::shared_ptr<RealtimeReadView>& view,
const RealtimeQueryContext& context) override {
if (context.predicate) {
saw_query_predicate_->store(true, std::memory_order_release);
}
*query_view_ = view;
std::shared_ptr<TrackingRealtimeReadView> tracking_view =
std::dynamic_pointer_cast<TrackingRealtimeReadView>(view);
if (!tracking_view) {
return Status::Invalid("query tracking store received an unexpected read view");
}
return delegate_->CreateQueryReaders(tracking_view->Delegate(), context);
}
private:
std::shared_ptr<std::atomic<bool>> saw_query_predicate_;
std::shared_ptr<std::weak_ptr<RealtimeReadView>> query_view_;
};
// Publishes a competing snapshot after the attempt has already materialized its DVs.
class InterleavingSnapshotFileSystem final : public LocalFileSystem {
public:
InterleavingSnapshotFileSystem(std::string snapshot_path, std::function<Status()> interleave)
: snapshot_path_(std::move(snapshot_path)), interleave_(std::move(interleave)) {}
Status AtomicStore(const std::string& path, const std::string& content) override {
if (path == snapshot_path_ && interleave_) {
std::function<Status()> interleave = std::move(interleave_);
PAIMON_RETURN_NOT_OK(interleave());
return Status::IOError("injected snapshot publication conflict");
}
return FileSystem::AtomicStore(path, content);
}
private:
std::string snapshot_path_;
std::function<Status()> interleave_;
};
class SnapshotConflictFileSystem final : public LocalFileSystem {
public:
explicit SnapshotConflictFileSystem(std::string conflict_snapshot_path)
: conflict_snapshot_path_(std::move(conflict_snapshot_path)) {}
void SetPassthroughChecksForCurrentThread(int32_t checks) {
std::lock_guard<std::mutex> lock(mutex_);
passthrough_checks_[std::this_thread::get_id()] = checks;
}
Result<bool> Exists(const std::string& path) const override {
if (path != conflict_snapshot_path_) {
return LocalFileSystem::Exists(path);
}
{
std::unique_lock<std::mutex> lock(mutex_);
if (conflict_snapshot_store_finished_) {
lock.unlock();
return LocalFileSystem::Exists(path);
}
if (conflict_snapshot_leader_selected_ &&
conflict_snapshot_leader_ == std::this_thread::get_id()) {
// Local AtomicStore rechecks the destination through virtual Exists during rename.
lock.unlock();
return LocalFileSystem::Exists(path);
}
auto passthrough_iter = passthrough_checks_.find(std::this_thread::get_id());
if (passthrough_iter != passthrough_checks_.end() && passthrough_iter->second > 0) {
--passthrough_iter->second;
lock.unlock();
return LocalFileSystem::Exists(path);
}
// Make both committers race for the same target snapshot. One observes it as absent
// and commits it; the other waits for that store and then reports the conflict.
++conflict_snapshot_attempts_;
cv_.notify_all();
if (!cv_.wait_for(lock, std::chrono::seconds(10),
[this]() { return conflict_snapshot_attempts_ >= 2; })) {
return Status::IOError("timed out waiting for concurrent snapshot commit");
}
if (!conflict_snapshot_leader_selected_) {
conflict_snapshot_leader_selected_ = true;
conflict_snapshot_leader_ = std::this_thread::get_id();
return false;
}
if (!cv_.wait_for(lock, std::chrono::seconds(10),
[this]() { return conflict_snapshot_store_finished_; })) {
return Status::IOError("timed out serializing conflicting snapshot commits");
}
}
return LocalFileSystem::Exists(path);
}
Status AtomicStore(const std::string& path, const std::string& content) override {
Status status = FileSystem::AtomicStore(path, content);
if (path == conflict_snapshot_path_) {
{
std::lock_guard<std::mutex> lock(mutex_);
conflict_snapshot_store_finished_ = true;
}
cv_.notify_all();
}
return status;
}
int32_t ConflictSnapshotAttempts() const {
std::lock_guard<std::mutex> lock(mutex_);
return conflict_snapshot_attempts_;
}
private:
std::string conflict_snapshot_path_;
mutable std::mutex mutex_;
mutable std::condition_variable cv_;
mutable std::map<std::thread::id, int32_t> passthrough_checks_;
mutable int32_t conflict_snapshot_attempts_ = 0;
mutable bool conflict_snapshot_leader_selected_ = false;
mutable std::thread::id conflict_snapshot_leader_;
mutable bool conflict_snapshot_store_finished_ = false;
};
} // namespace
namespace {
constexpr char kDropPartitionCommitUser[] = "drop_partition_commit_user";
constexpr char kRollbackCommitUser[] = "rollback_commit_user";
constexpr char kTruncateCommitUser[] = "truncate_commit_user";
} // namespace
class UnsupportedFunction : public Function {
public:
Type GetType() const override {
return Type::EQUAL;
}
std::string ToString() const override {
return "unsupported";
}
};
class UnsupportedPredicate : public Predicate {
public:
bool operator==(const Predicate&) const override {
return false;
}
const Function& GetFunction() const override {
return function_;
}
std::shared_ptr<Predicate> Negate() const override {
return nullptr;
}
std::string ToString() const override {
return function_.ToString();
}
private:
UnsupportedFunction function_;
};
class ConcurrentTestState {
public:
void WaitForStart() {
ready_threads_.fetch_add(1, std::memory_order_release);
while (!start_.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
}
void StartWhenReady(int32_t worker_count) {
while (ready_threads_.load(std::memory_order_acquire) < worker_count) {
std::this_thread::yield();
}
start_.store(true, std::memory_order_release);
}
void RecordError(const Status& status) {
RecordError(status.ToString());
}
void RecordError(std::string error) {
{
std::lock_guard<std::mutex> lock(mutex);
errors_.push_back(std::move(error));
}
stop_.store(true, std::memory_order_release);
progress_cv.notify_all();
snapshot_cv.notify_all();
}
bool RecordErrorIfNotOk(const Status& status) {
if (status.ok()) {
return false;
}
RecordError(status);
return true;
}
template <typename T>
bool RecordErrorIfNotOk(const Result<T>& result) {
if (result.ok()) {
return false;
}
RecordError(result.status());
return true;
}
bool ShouldStop() const {
return stop_.load(std::memory_order_acquire);
}
const std::vector<std::string>& Errors() const {
return errors_;
}
std::mutex mutex;
std::condition_variable progress_cv;
std::condition_variable snapshot_cv;
private:
std::atomic<bool> start_{false};
std::atomic<bool> stop_{false};
std::atomic<int32_t> ready_threads_{0};
std::vector<std::string> errors_;
};
class RealtimeWriteInteTest : public ::testing::Test {
protected:
using Row = std::tuple<int64_t, std::string, std::string>;
struct RealtimeMetricValues {
double building_memory = 0;
double sealed_memory = 0;
double total_memory = 0;
double building_rows = 0;
double sealed_rows = 0;
double total_rows = 0;
};
struct DiskFileLayout {
bool has_level0_file = false;
bool has_high_level_file = false;
bool has_high_level_deletion_vector = false;
};
static Result<DiskFileLayout> InspectDiskFiles(
const std::vector<std::shared_ptr<Split>>& splits) {
DiskFileLayout layout;
for (const std::shared_ptr<Split>& split : splits) {
std::shared_ptr<DataSplitImpl> data_split =
std::dynamic_pointer_cast<DataSplitImpl>(split);
if (!data_split) {
return Status::Invalid("expected a data split");
}
const std::vector<std::shared_ptr<DataFileMeta>>& files = data_split->DataFiles();
const std::vector<std::optional<DeletionFile>>& deletion_files =
data_split->DeletionFiles();
if (!deletion_files.empty() && deletion_files.size() != files.size()) {
return Status::Invalid("deletion files must be empty or match data files");
}
for (size_t i = 0; i < files.size(); ++i) {
if (files[i]->level == 0) {
layout.has_level0_file = true;
} else if (files[i]->level > 0) {
layout.has_high_level_file = true;
if (!deletion_files.empty() && deletion_files[i]) {
layout.has_high_level_deletion_vector = true;
}
} else {
return Status::Invalid("data file has a negative level");
}
}
}
return layout;
}
void SetUp() override {
pool_ = GetDefaultPool();
dir_ = UniqueTestDirectory::Create("local");
ASSERT_NE(nullptr, dir_);
table_path_ = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
fields_ = {arrow::field("id", arrow::int64()), arrow::field("payload", arrow::utf8()),
arrow::field("pt", arrow::utf8())};
schema_ = arrow::schema(fields_);
options_ = {
{Options::FILE_FORMAT, "orc"},
{Options::FILE_SYSTEM, "local"},
{Options::BUCKET, "1"},
{Options::BUCKET_KEY, "id"},
{Options::TARGET_FILE_SIZE, "1048576"},
{Options::REALTIME_ENABLED, "true"},
};
}
void TearDown() override {
dir_.reset();
}
void CreateTable(const std::vector<std::string>& partition_keys) const {
auto c_schema = std::make_unique<ArrowSchema>();
ASSERT_TRUE(arrow::ExportSchema(*schema_, c_schema.get()).ok());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<Catalog> catalog,
Catalog::Create(dir_->Str(), options_));
ASSERT_OK(catalog->CreateDatabase("foo", {}, /*ignore_if_exists=*/false));
ASSERT_OK(catalog->CreateTable(Identifier("foo", "bar"), c_schema.get(), partition_keys,
/*primary_keys=*/{}, options_,
/*ignore_if_exists=*/false));
}
void CreatePkTable(const std::vector<std::string>& partition_keys = {},
const std::vector<std::string>& primary_keys = {"id"}) const {
auto c_schema = std::make_unique<ArrowSchema>();
ASSERT_TRUE(arrow::ExportSchema(*schema_, c_schema.get()).ok());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<Catalog> catalog,
Catalog::Create(dir_->Str(), options_));
ASSERT_OK(catalog->CreateDatabase("foo", {}, /*ignore_if_exists=*/false));
std::vector<std::string> table_primary_keys = partition_keys;
table_primary_keys.insert(table_primary_keys.end(), primary_keys.begin(),
primary_keys.end());
ASSERT_OK(catalog->CreateTable(Identifier("foo", "bar"), c_schema.get(), partition_keys,
table_primary_keys, options_, /*ignore_if_exists=*/false));
}
void ConfigureRealtimeDeduplicateTable(const std::vector<std::string>& partition_keys = {}) {
ConfigureRealtimeDeduplicateTable(partition_keys, "bitmap");
}
void ConfigureRealtimeDeduplicateTable(const std::vector<std::string>& partition_keys,
const std::string& offset_index_type) {
fields_ = {
arrow::field(SpecialFields::RealtimeOffset().Name(), arrow::int64(),
/*nullable=*/false),
arrow::field("id", arrow::int64(), /*nullable=*/false),
arrow::field("payload", arrow::utf8()),
arrow::field("pt", arrow::utf8()),
};
schema_ = arrow::schema(fields_);
options_[Options::FILE_FORMAT] = "parquet";
options_[Options::DELETION_VECTORS_ENABLED] = "true";
options_[Options::REALTIME_DEDUPLICATE_KEY_FIELDS] = "id";
options_["file-index." + offset_index_type + ".columns"] =
SpecialFields::RealtimeOffset().Name();
CreateTable(partition_keys);
}
Result<std::unique_ptr<FileStoreWrite>> CreateRealtimeWriter(
const std::shared_ptr<RealtimeContext>& realtime_context) const {
return CreateRealtimeWriter(realtime_context, /*temp_directory=*/"");
}
Result<std::unique_ptr<FileStoreWrite>> CreateRealtimeWriter(
const std::shared_ptr<RealtimeContext>& realtime_context,
const std::string& temp_directory) const {
WriteContextBuilder builder(table_path_, commit_user_);
builder.SetOptions(options_).WithStreamingMode(true).WithRealtimeContext(realtime_context);
if (!temp_directory.empty()) {
builder.WithTempDirectory(temp_directory);
}
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<WriteContext> context, builder.Finish());
return FileStoreWrite::Create(std::move(context));
}
Result<std::unique_ptr<FileStoreWrite>> CreateRealtimeWriter() const {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
return CreateRealtimeWriter(realtime_context);
}
bool WaitForChannelFileCount(const std::string& temp_directory, int64_t expected_count) const {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2);
while (std::chrono::steady_clock::now() < deadline) {
if (TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory) ==
expected_count) {
return true;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
return TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory) ==
expected_count;
}
void ResetExternalOffset(const std::map<std::string, std::string>& partition, int32_t bucket,
int64_t next_offset) const {
std::lock_guard<std::mutex> lock(external_offsets_mutex_);
next_external_offsets_[RealtimePartitionBucket(partition, bucket)] = next_offset;
}
Result<std::unique_ptr<RecordBatch>> MakeBatch(const std::vector<Row>& rows,
bool partitioned) const {
return MakeBatch(rows, partitioned, /*bucket=*/0);
}
Result<std::unique_ptr<RecordBatch>> MakeBatch(const std::vector<Row>& rows, bool partitioned,
int32_t bucket) const {
return MakeBatch(rows, partitioned, bucket, /*row_kinds=*/{});
}
Result<std::unique_ptr<RecordBatch>> MakeBatch(
const std::vector<Row>& rows, bool partitioned, int32_t bucket,
const std::vector<RecordBatch::RowKind>& row_kinds) const {
if (rows.empty()) {
return Status::Invalid("cannot create an empty test batch");
}
const std::string& partition = std::get<2>(rows.front());
std::map<std::string, std::string> batch_partition;
if (partitioned) {
batch_partition = {{"pt", partition}};
}
int64_t first_offset = 0;
{
std::lock_guard<std::mutex> lock(external_offsets_mutex_);
int64_t& next_offset =
next_external_offsets_[RealtimePartitionBucket(batch_partition, bucket)];
first_offset = next_offset;
next_offset += static_cast<int64_t>(rows.size());
}
std::string json = "[";
for (size_t i = 0; i < rows.size(); ++i) {
const auto& [id, payload, pt] = rows[i];
if (partitioned && pt != partition) {
return Status::Invalid("one test batch must contain only one partition");
}
if (i > 0) {
json += ",";
}
json += fmt::format(R"([{}, {}, "{}", "{}"])", first_offset + static_cast<int64_t>(i),
id, payload, pt);
}
json += "]";
const RealtimeStoreMode store_mode =
options_.count(Options::REALTIME_DEDUPLICATE_KEY_FIELDS) > 0
? RealtimeStoreMode::DEDUPLICATE
: RealtimeStoreMode::APPEND_ONLY;
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<RealtimeSchemaLayout> schema_layout,
RealtimeSchemaLayout::Create(store_mode, schema_));
const std::shared_ptr<arrow::Schema>& realtime_schema = schema_layout->InputSchema();
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
arrow::ipc::internal::json::ArrayFromJSON(
arrow::struct_(realtime_schema->fields()), json));
ArrowArray c_array;
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array));
return RecordBatchBuilder(&c_array)
.SetRowKinds(row_kinds)
.SetPartition(batch_partition)
.SetBucket(bucket)
.Finish();
}
Result<std::unique_ptr<RecordBatch>> MakeLogicalBatch(const std::vector<Row>& rows,
int32_t bucket) const {
if (rows.empty()) {
return Status::Invalid("cannot create an empty test batch");
}
std::string json = "[";
for (size_t i = 0; i < rows.size(); ++i) {
const auto& [id, payload, partition] = rows[i];
if (i > 0) {
json += ",";
}
json += fmt::format(R"([{}, "{}", "{}"])", id, payload, partition);
}
json += "]";
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
std::shared_ptr<arrow::Array> array,
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(fields_), json));
ArrowArray c_array;
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array));
return RecordBatchBuilder(&c_array).SetBucket(bucket).Finish();
}
Result<std::unique_ptr<RecordBatch>> MakeDatePartitionBatch(
int64_t first_id, int64_t count, int32_t date, const std::string& partition) const {
if (count <= 0) {
return Status::Invalid("cannot create an empty test batch");
}
int64_t first_offset = 0;
{
std::lock_guard<std::mutex> lock(external_offsets_mutex_);
int64_t& next_offset =
next_external_offsets_[RealtimePartitionBucket({{"pt", partition}}, /*bucket=*/0)];
first_offset = next_offset;
next_offset += count;
}
std::string json = "[";
for (int64_t i = 0; i < count; ++i) {
if (i > 0) {
json += ",";
}
int64_t id = first_id + i;
json += fmt::format(R"([{}, {}, "value-{}", {}])", first_offset + i, id, id, date);
}
json += "]";
PAIMON_ASSIGN_OR_RAISE(
std::unique_ptr<RealtimeSchemaLayout> schema_layout,
RealtimeSchemaLayout::Create(RealtimeStoreMode::APPEND_ONLY, schema_));
const std::shared_ptr<arrow::Schema>& realtime_schema = schema_layout->InputSchema();
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
arrow::ipc::internal::json::ArrayFromJSON(
arrow::struct_(realtime_schema->fields()), json));
ArrowArray c_array;
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array));
return RecordBatchBuilder(&c_array).SetPartition({{"pt", partition}}).SetBucket(0).Finish();
}
Result<std::unique_ptr<RecordBatch>> MakeUnpartitionedBatchFromJson(
const std::string& json) const {
PAIMON_ASSIGN_OR_RAISE(
std::unique_ptr<RealtimeSchemaLayout> schema_layout,
RealtimeSchemaLayout::Create(RealtimeStoreMode::APPEND_ONLY, schema_));
const std::shared_ptr<arrow::Schema>& realtime_schema = schema_layout->InputSchema();
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
arrow::ipc::internal::json::ArrayFromJSON(
arrow::struct_(realtime_schema->fields()), json));
ArrowArray c_array;
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array));
return RecordBatchBuilder(&c_array).SetBucket(0).Finish();
}
static std::vector<Row> MakeRows(int64_t first_id, int64_t count,
const std::string& partition) {
std::vector<Row> rows;
rows.reserve(count);
for (int64_t i = 0; i < count; ++i) {
int64_t id = first_id + i;
rows.emplace_back(id, "value-" + std::to_string(id), partition);
}
return rows;
}
Result<int64_t> Commit(const std::vector<RealtimeCommitProgress>& realtime_commits,
int64_t commit_identifier) const {
CommitContextBuilder builder(table_path_, commit_user_);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<CommitContext> context,
builder.SetOptions(options_).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(context)));
return commit->CommitWithProgress(realtime_commits, commit_identifier,
/*watermark=*/std::nullopt);
}
Result<int64_t> WriteCommitAndRefresh(const std::vector<Row>& rows, bool partitioned,
int32_t bucket,
const std::vector<RecordBatch::RowKind>& row_kinds,
int64_t commit_identifier, FileStoreWrite* writer) const {
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, partitioned, bucket, row_kinds));
PAIMON_RETURN_NOT_OK(writer->Write(std::move(batch)));
PAIMON_ASSIGN_OR_RAISE(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(commit_identifier));
PAIMON_ASSIGN_OR_RAISE(int64_t snapshot, Commit(progress, commit_identifier));
PAIMON_RETURN_NOT_OK(writer->RefreshCommittedSnapshot(snapshot));
return snapshot;
}
Result<int64_t> DropPartition(const std::map<std::string, std::string>& partition,
int64_t commit_identifier) const {
CommitContextBuilder builder(table_path_, kDropPartitionCommitUser);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<CommitContext> context,
builder.SetOptions(options_).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(context)));
PAIMON_RETURN_NOT_OK(commit->DropPartition({partition}, commit_identifier));
PAIMON_ASSIGN_OR_RAISE(CoreOptions options, CoreOptions::FromMap(options_));
SnapshotManager snapshot_manager(options.GetFileSystem(), table_path_);
PAIMON_ASSIGN_OR_RAISE(std::optional<Snapshot> latest_snapshot,
snapshot_manager.LatestSnapshot());
if (!latest_snapshot) {
return Status::Invalid("drop partition did not produce a snapshot");
}
return latest_snapshot->Id();
}
Result<int64_t> TruncateTable(int64_t commit_identifier) const {
CommitContextBuilder builder(table_path_, kTruncateCommitUser);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<CommitContext> context,
builder.SetOptions(options_).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(context)));
PAIMON_RETURN_NOT_OK(commit->TruncateTable(commit_identifier));
PAIMON_ASSIGN_OR_RAISE(CoreOptions options, CoreOptions::FromMap(options_));
SnapshotManager snapshot_manager(options.GetFileSystem(), table_path_);
PAIMON_ASSIGN_OR_RAISE(std::optional<Snapshot> latest_snapshot,
snapshot_manager.LatestSnapshot());
if (!latest_snapshot) {
return Status::Invalid("truncate did not produce a snapshot");
}
return latest_snapshot->Id();
}
Result<int64_t> RollbackToAsLatest(int64_t target_snapshot_id) const {
CommitContextBuilder builder(table_path_, kRollbackCommitUser);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<CommitContext> context,
builder.SetOptions(options_).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(context)));
PAIMON_ASSIGN_OR_RAISE(bool rolled_back, commit->RollbackToAsLatest(target_snapshot_id));
if (!rolled_back) {
return Status::Invalid("failed to commit rollback snapshot");
}
PAIMON_ASSIGN_OR_RAISE(CoreOptions options, CoreOptions::FromMap(options_));
SnapshotManager snapshot_manager(options.GetFileSystem(), table_path_);
PAIMON_ASSIGN_OR_RAISE(std::optional<Snapshot> latest_snapshot,
snapshot_manager.LatestSnapshot());
if (!latest_snapshot) {
return Status::Invalid("rollback did not produce a snapshot");
}
return latest_snapshot->Id();
}
Result<std::vector<std::shared_ptr<CommitMessage>>> CompactAndPrepareCommit(
const std::vector<RealtimePartitionBucket>& partition_buckets, int64_t commit_identifier,
bool full_compaction, std::unique_ptr<FileStoreWrite>* compactor) const {
WriteContextBuilder builder(table_path_, "regression-compactor");
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<WriteContext> context,
builder.SetOptions(options_).WithStreamingMode(true).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreWrite> writer,
FileStoreWrite::Create(std::move(context)));
for (const RealtimePartitionBucket& scope : partition_buckets) {
PAIMON_RETURN_NOT_OK(writer->Compact(scope.partition, scope.bucket, full_compaction));
}
PAIMON_ASSIGN_OR_RAISE(std::vector<std::shared_ptr<CommitMessage>> messages,
writer->PrepareCommit(/*wait_compaction=*/true, commit_identifier));
*compactor = std::move(writer);
return messages;
}
Result<Snapshot> CommitCompaction(const std::vector<std::shared_ptr<CommitMessage>>& messages,
int64_t commit_identifier) const {
CommitContextBuilder builder(table_path_, "regression-compactor");
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<CommitContext> context,
builder.SetOptions(options_).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(context)));
PAIMON_RETURN_NOT_OK(commit->Commit(messages, commit_identifier));
SnapshotManager snapshot_manager(dir_->GetFileSystem(), table_path_);
PAIMON_ASSIGN_OR_RAISE(std::optional<Snapshot> snapshot, snapshot_manager.LatestSnapshot());
if (!snapshot) {
return Status::Invalid("compaction did not produce a snapshot");
}
return *snapshot;
}
Result<Snapshot> CompactAndCommit(const std::map<std::string, std::string>& partition,
int32_t bucket, int64_t commit_identifier,
bool full_compaction = true) const {
WriteContextBuilder write_builder(table_path_, commit_user_);
write_builder.SetOptions(options_).WithStreamingMode(true).WithTempDirectory(
PathUtil::JoinPath(dir_->Str(), "compact-tmp"));
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<WriteContext> write_context, write_builder.Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreWrite> compaction_writer,
FileStoreWrite::Create(std::move(write_context)));
PAIMON_RETURN_NOT_OK(compaction_writer->Compact(partition, bucket, full_compaction));
PAIMON_ASSIGN_OR_RAISE(
std::vector<std::shared_ptr<CommitMessage>> compaction_messages,
compaction_writer->PrepareCommit(/*wait_compaction=*/true, commit_identifier));
if (compaction_messages.empty()) {
return Status::Invalid("compaction did not produce a commit message");
}
CommitContextBuilder commit_builder(table_path_, commit_user_);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<CommitContext> commit_context,
commit_builder.SetOptions(options_).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreCommit> compaction_commit,
FileStoreCommit::Create(std::move(commit_context)));
PAIMON_RETURN_NOT_OK(compaction_commit->Commit(compaction_messages, commit_identifier));
PAIMON_RETURN_NOT_OK(compaction_writer->Close());
PAIMON_ASSIGN_OR_RAISE(CoreOptions core_options, CoreOptions::FromMap(options_));
SnapshotManager snapshot_manager(core_options.GetFileSystem(), table_path_);
PAIMON_ASSIGN_OR_RAISE(std::optional<Snapshot> compact_snapshot,
snapshot_manager.LatestSnapshot());
if (!compact_snapshot) {
return Status::Invalid("compaction did not produce a snapshot");
}
return compact_snapshot.value();
}
Result<Snapshot> CompactCommitAndRefresh(const std::map<std::string, std::string>& partition,
int32_t bucket, int64_t commit_identifier,
bool full_compaction, FileStoreWrite* writer) const {
PAIMON_ASSIGN_OR_RAISE(
Snapshot snapshot,
CompactAndCommit(partition, bucket, commit_identifier, full_compaction));
PAIMON_RETURN_NOT_OK(writer->RefreshCommittedSnapshot(snapshot.Id()));
return snapshot;
}
Result<int32_t> ExpireSnapshots() const {
CommitContextBuilder builder(table_path_, commit_user_);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<CommitContext> context,
builder.SetOptions(options_).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(context)));
return commit->Expire();
}
Result<std::shared_ptr<Plan>> CreatePlan(
const std::shared_ptr<RealtimeContext>& realtime_context,
const std::shared_ptr<Predicate>& predicate) const {
ScanContextBuilder scan_builder(table_path_);
if (realtime_context) {
scan_builder.WithRealtimeContext(realtime_context);
}
scan_builder.SetPredicate(predicate).WithMemoryPool(pool_);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ScanContext> scan_context,
scan_builder.SetOptions(options_).Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableScan> scan,
TableScan::Create(std::move(scan_context)));
return scan->CreatePlan();
}
Result<std::unique_ptr<BatchReader>> CreateQueryReader(
const std::shared_ptr<Plan>& plan,
const std::shared_ptr<RealtimeContext>& realtime_context) const {
ReadContextBuilder read_builder(table_path_);
read_builder.SetOptions(options_)
.SetReadFieldNames({"id", "payload", "pt"})
.WithRealtimeContext(realtime_context)
.WithMemoryPool(pool_);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReadContext> read_context, read_builder.Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableRead> table_read,
TableRead::Create(std::move(read_context)));
return table_read->CreateReader(plan->Splits());
}
Result<std::unique_ptr<BatchReader>> CreateQueryReader(
const std::shared_ptr<RealtimeContext>& realtime_context) const {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
return CreateQueryReader(plan, realtime_context);
}
Result<std::shared_ptr<arrow::ChunkedArray>> ReadPlan(
const std::shared_ptr<Plan>& plan, const std::shared_ptr<RealtimeContext>& realtime_context,
const std::vector<std::string>& read_fields, const std::shared_ptr<Predicate>& predicate,
bool enable_predicate_filter) const {
ReadContextBuilder read_builder(table_path_);
read_builder.SetOptions(options_)
.SetReadFieldNames(read_fields)
.SetPredicate(predicate)
.EnablePredicateFilter(enable_predicate_filter)
.WithRealtimeContext(realtime_context)
.WithMemoryPool(pool_);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReadContext> read_context, read_builder.Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableRead> table_read,
TableRead::Create(std::move(read_context)));
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<BatchReader> reader,
table_read->CreateReader(plan->Splits()));
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::ChunkedArray> result,
ReadResultCollector::CollectResult(std::move(reader)));
return result;
}
void ReadPlanWithSchemaAndCheck(const std::shared_ptr<Plan>& plan,
const std::shared_ptr<RealtimeContext>& realtime_context,
const std::shared_ptr<arrow::Schema>& read_schema,
const std::string& expected_json) const {
std::unique_ptr<ArrowSchema> c_read_schema = std::make_unique<ArrowSchema>();
ASSERT_TRUE(arrow::ExportSchema(*read_schema, c_read_schema.get()).ok());
ReadContextBuilder read_builder(table_path_);
read_builder.SetOptions(options_)
.SetReadSchema(std::move(c_read_schema))
.WithRealtimeContext(realtime_context)
.WithMemoryPool(pool_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> read_context, read_builder.Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableRead> table_read,
TableRead::Create(std::move(read_context)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<BatchReader> reader,
table_read->CreateReader(plan->Splits()));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result,
ReadResultCollector::CollectResult(std::move(reader)));
arrow::FieldVector result_fields = {arrow::field("_VALUE_KIND", arrow::int8())};
result_fields.insert(result_fields.end(), read_schema->fields().begin(),
read_schema->fields().end());
std::shared_ptr<arrow::Array> expected =
arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(result_fields), expected_json)
.ValueOrDie();
// Match the dictionary and nullability normalization applied by ReadResultCollector.
ASSERT_OK_AND_ASSIGN(
expected, DictArrayConverter::ConvertDictArray(expected, arrow::default_memory_pool()));
ASSERT_NE(nullptr, result);
ASSERT_TRUE(std::make_shared<arrow::ChunkedArray>(expected)->Equals(*result))
<< result->ToString();
}
Result<std::vector<Row>> ReadRows(
const std::shared_ptr<RealtimeContext>& realtime_context) const {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
return ReadRows(plan, realtime_context);
}
Result<std::vector<Row>> ReadRows(
const std::shared_ptr<Plan>& plan,
const std::shared_ptr<RealtimeContext>& realtime_context) const {
return ReadRows(plan, realtime_context, /*predicate=*/nullptr,
/*enable_predicate_filter=*/false);
}
Result<std::vector<Row>> ReadRows(const std::shared_ptr<Plan>& plan,
const std::shared_ptr<RealtimeContext>& realtime_context,
const std::shared_ptr<Predicate>& predicate,
bool enable_predicate_filter) const {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::ChunkedArray> read_result,
ReadPlan(plan, realtime_context, {"id", "payload", "pt"}, predicate,
enable_predicate_filter));
const std::shared_ptr<arrow::ChunkedArray>& result = read_result;
std::vector<Row> rows;
if (!result) {
return rows;
}
for (const std::shared_ptr<arrow::Array>& chunk : result->chunks()) {
std::shared_ptr<arrow::StructArray> data =
std::dynamic_pointer_cast<arrow::StructArray>(chunk);
if (!data || data->num_fields() != 4) {
return Status::Invalid("unexpected real-time test read schema");
}
std::shared_ptr<arrow::Int8Array> row_kinds =
std::dynamic_pointer_cast<arrow::Int8Array>(data->field(0));
std::shared_ptr<arrow::Int64Array> ids =
std::dynamic_pointer_cast<arrow::Int64Array>(data->field(1));
std::shared_ptr<arrow::StringArray> payloads =
std::dynamic_pointer_cast<arrow::StringArray>(data->field(2));
std::shared_ptr<arrow::StringArray> partitions =
std::dynamic_pointer_cast<arrow::StringArray>(data->field(3));
if (!row_kinds || !ids || !payloads || !partitions) {
return Status::Invalid("unexpected real-time test read field type");
}
for (int64_t i = 0; i < data->length(); ++i) {
if (row_kinds->IsNull(i) ||
row_kinds->Value(i) != static_cast<int8_t>(RecordBatch::RowKind::INSERT) ||
ids->IsNull(i) || payloads->IsNull(i) || partitions->IsNull(i)) {
return Status::Invalid("unexpected null or row kind in real-time test result");
}
rows.emplace_back(ids->Value(i), payloads->GetString(i), partitions->GetString(i));
}
}
return rows;
}
Result<std::vector<Row>> ReadRows() const {
return ReadRows(/*realtime_context=*/nullptr);
}
Result<int64_t> CountRows(const std::shared_ptr<Plan>& plan,
const std::shared_ptr<RealtimeContext>& realtime_context) const {
ReadContextBuilder read_builder(table_path_);
read_builder.SetOptions(options_)
.WithRealtimeContext(realtime_context)
.WithMemoryPool(pool_);
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<ReadContext> read_context, read_builder.Finish());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableRead> table_read,
TableRead::Create(std::move(read_context)));
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<CountReader> count_reader,
table_read->CreateCountReader(plan->Splits()));
return count_reader->CountRows();
}
Result<uint64_t> GetRealtimeMemoryUsage(
const std::shared_ptr<RealtimeContext>& realtime_context) const {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<RealtimeContextImpl> realtime_context_impl,
RealtimeContextImpl::Cast(realtime_context));
PAIMON_ASSIGN_OR_RAISE(RealtimeReadState read_state,
realtime_context_impl->AcquireReadState());
uint64_t memory_usage = 0;
for (const RealtimePartitionBucketView& view : read_state.views) {
memory_usage += view.store->GetMemoryUsage();
}
return memory_usage;
}
static Result<RealtimeMetricValues> ReadRealtimeMetrics(
const std::shared_ptr<Metrics>& metrics) {
if (!metrics) {
return Status::Invalid("real-time metrics are null");
}
RealtimeMetricValues result;
PAIMON_ASSIGN_OR_RAISE(result.building_memory,
metrics->GetGauge(RealtimeMetrics::kBuildingMemoryBytes));
PAIMON_ASSIGN_OR_RAISE(result.sealed_memory,
metrics->GetGauge(RealtimeMetrics::kSealedMemoryBytes));
PAIMON_ASSIGN_OR_RAISE(result.total_memory,
metrics->GetGauge(RealtimeMetrics::kTotalMemoryBytes));
PAIMON_ASSIGN_OR_RAISE(result.building_rows,
metrics->GetGauge(RealtimeMetrics::kBuildingRowCount));
PAIMON_ASSIGN_OR_RAISE(result.sealed_rows,
metrics->GetGauge(RealtimeMetrics::kSealedRowCount));
PAIMON_ASSIGN_OR_RAISE(result.total_rows,
metrics->GetGauge(RealtimeMetrics::kTotalRowCount));
return result;
}
Result<std::vector<int64_t>> ReadPkSequences(
const std::shared_ptr<RealtimeContext>& realtime_context) const {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<RealtimeContextImpl> realtime_context_impl,
RealtimeContextImpl::Cast(realtime_context));
PAIMON_ASSIGN_OR_RAISE(RealtimeReadState read_state,
realtime_context_impl->AcquireReadState());
const std::vector<RealtimePartitionBucketView>& views = read_state.views;
if (views.size() != 1) {
return Status::Invalid("expected one PK real-time read view");
}
PAIMON_ASSIGN_OR_RAISE(CoreOptions core_options, CoreOptions::FromMap(options_));
SchemaManager schema_manager(core_options.GetFileSystem(), table_path_);
PAIMON_ASSIGN_OR_RAISE(std::optional<std::shared_ptr<TableSchema>> table_schema,
schema_manager.Latest());
if (!table_schema) {
return Status::Invalid("expected a table schema");
}
auto read_schema = std::make_unique<ArrowSchema>();
std::shared_ptr<arrow::Schema> value_schema =
DataField::ConvertDataFieldsToArrowSchema(table_schema.value()->Fields());
PAIMON_ASSIGN_OR_RAISE(
std::unique_ptr<RealtimeSchemaLayout> schema_layout,
RealtimeSchemaLayout::Create(RealtimeStoreMode::PRIMARY_KEY, value_schema));
PAIMON_RETURN_NOT_OK_FROM_ARROW(
arrow::ExportSchema(*schema_layout->StoreWriteSchema(), read_schema.get()));
ScopeGuard schema_guard([schema = read_schema.get()]() { ArrowSchemaRelease(schema); });
RealtimeQueryContext query_context{read_schema.get(), /*predicate=*/nullptr};
PAIMON_ASSIGN_OR_RAISE(
std::vector<std::unique_ptr<BatchReader>> readers,
views[0].store->CreateQueryReaders(views[0].read_view, query_context));
std::vector<int64_t> sequences;
for (const std::unique_ptr<BatchReader>& reader : readers) {
while (true) {
PAIMON_ASSIGN_OR_RAISE(BatchReader::ReadBatch batch, reader->NextBatch());
if (BatchReader::IsEofBatch(batch)) {
break;
}
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
std::shared_ptr<arrow::Array> imported,
arrow::ImportArray(batch.first.get(), batch.second.get()));
std::shared_ptr<arrow::StructArray> values =
std::dynamic_pointer_cast<arrow::StructArray>(imported);
if (!values) {
return Status::Invalid("PK query reader did not return a StructArray");
}
std::shared_ptr<arrow::Int64Array> sequence_array =
std::dynamic_pointer_cast<arrow::Int64Array>(
values->GetFieldByName(SpecialFields::SequenceNumber().Name()));
if (!sequence_array) {
return Status::Invalid("PK query reader did not return sequence numbers");
}
for (int64_t row = 0; row < sequence_array->length(); ++row) {
sequences.push_back(sequence_array->Value(row));
}
}
reader->Close();
}
return sequences;
}
static std::vector<std::shared_ptr<DataFileMeta>> NewFiles(
const std::vector<RealtimeCommitProgress>& progresses) {
std::vector<std::shared_ptr<DataFileMeta>> files;
for (const RealtimeCommitProgress& progress : progresses) {
std::shared_ptr<CommitMessageImpl> message =
std::dynamic_pointer_cast<CommitMessageImpl>(progress.commit_message);
if (!message) {
continue;
}
const std::vector<std::shared_ptr<DataFileMeta>>& new_files =
message->GetNewFilesIncrement().NewFiles();
files.insert(files.end(), new_files.begin(), new_files.end());
}
return files;
}
static Status ValidateReadPrefix(const std::vector<Row>& rows, int64_t total_rows) {
std::vector<bool> seen(static_cast<size_t>(total_rows), false);
int64_t max_id = -1;
for (const Row& row : rows) {
const auto& [id, payload, partition] = row;
if (id < 0 || id >= total_rows) {
return Status::Invalid("real-time read id is out of range");
}
if (seen[static_cast<size_t>(id)]) {
return Status::Invalid("real-time read contains duplicate ids");
}
if (payload != "value-" + std::to_string(id) || partition != "p0") {
return Status::Invalid("real-time read row does not match its id");
}
seen[static_cast<size_t>(id)] = true;
max_id = std::max(max_id, id);
}
for (int64_t id = 0; id <= max_id; ++id) {
if (!seen[static_cast<size_t>(id)]) {
return Status::Invalid("real-time read contains an id gap");
}
}
return Status::OK();
}
struct FileState {
std::shared_ptr<DataFileMeta> file;
std::string bucket_path;
std::optional<DeletionFile> deletion_file;
std::set<int64_t> deleted_positions;
};
Result<std::vector<FileState>> InspectFiles() const {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Plan> plan,
CreatePlan(/*realtime_context=*/nullptr, /*predicate=*/nullptr));
std::vector<FileState> states;
for (const auto& split : plan->Splits()) {
auto data = std::dynamic_pointer_cast<DataSplitImpl>(split);
if (!data || (!data->DeletionFiles().empty() &&
data->DeletionFiles().size() != data->DataFiles().size())) {
return Status::Invalid("expected data files with matching deletion files");
}
for (size_t i = 0; i < data->DataFiles().size(); ++i) {
FileState state{data->DataFiles()[i], data->BucketPath(), std::nullopt, {}};
if (!data->DeletionFiles().empty() && data->DeletionFiles()[i]) {
state.deletion_file = data->DeletionFiles()[i];
PAIMON_ASSIGN_OR_RAISE(PAIMON_UNIQUE_PTR<DeletionVector> dv,
DeletionVector::Read(dir_->GetFileSystem().get(),
*state.deletion_file, pool_.get()));
PAIMON_RETURN_NOT_OK(dv->ForEachDeletedPosition(
[&](int64_t position) { state.deleted_positions.insert(position); }));
}
states.push_back(std::move(state));
}
}
return states;
}
Result<RealtimeOffsetMap> ReadCommittedOffsets() const {
PAIMON_ASSIGN_OR_RAISE(CoreOptions options, CoreOptions::FromMap(options_));
SnapshotManager snapshot_manager(options.GetFileSystem(), table_path_);
PAIMON_ASSIGN_OR_RAISE(std::optional<Snapshot> snapshot, snapshot_manager.LatestSnapshot());
return RealtimeCommitProperties::ReadOffsets(snapshot, options.GetFileSystem());
}
Status PrepareAndClose(FileStoreWrite* writer) const {
PAIMON_ASSIGN_OR_RAISE(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
static_cast<void>(progress);
return writer->Close();
}
void FinalizeCommitAndCheck(FileStoreWrite* writer,
std::vector<RealtimeCommitProgress> realtime_commits,
int64_t prepare_identifier, std::vector<Row> expected_rows) const {
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> final_commits,
writer->PrepareCommitWithProgress(prepare_identifier));
realtime_commits.insert(realtime_commits.end(),
std::make_move_iterator(final_commits.begin()),
std::make_move_iterator(final_commits.end()));
ASSERT_OK(Commit(realtime_commits, prepare_identifier));
ASSERT_OK(writer->Close());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows());
ASSERT_EQ(expected_rows, actual_rows);
}
void ReplayPkWalAndCommit(const std::vector<Row>& wal,
const std::vector<RecordBatch::RowKind>& row_kinds,
int64_t commit_identifier,
const std::vector<Row>& expected_rows) const {
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(wal, /*partitioned=*/false, /*bucket=*/0, row_kinds));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(commit_identifier));
ASSERT_EQ(1, progress.size());
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, commit_identifier));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK(writer->Close());
writer.reset();
realtime_context.reset();
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows());
ASSERT_EQ(expected_rows, actual_rows);
}
void CheckDropDatePartitionRemovesOffset(bool legacy_partition_name_enabled) {
fields_ = {arrow::field("id", arrow::int64()), arrow::field("payload", arrow::utf8()),
arrow::field("pt", arrow::date32())};
schema_ = arrow::schema(fields_);
options_[Options::PARTITION_GENERATE_LEGACY_NAME] =
legacy_partition_name_enabled ? "true" : "false";
CreateTable(/*partition_keys=*/{"pt"});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
constexpr int32_t kDate = 19723;
constexpr int64_t kRowCount = 3;
const std::string partition = "2024-01-01";
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeDatePartitionBatch(/*first_id=*/0, kRowCount, kDate, partition));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, commits.size());
ASSERT_OK(Commit(commits, /*commit_identifier=*/0));
const std::string normalized_partition =
legacy_partition_name_enabled ? std::to_string(kDate) : partition;
RealtimePartitionBucket partition_bucket({{"pt", normalized_partition}}, /*bucket=*/0);
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap offsets_before_drop, ReadCommittedOffsets());
ASSERT_EQ(1, offsets_before_drop.size());
ASSERT_EQ(kRowCount, offsets_before_drop.at(partition_bucket));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan_before_drop,
CreatePlan(/*realtime_context=*/nullptr, /*predicate=*/nullptr));
ASSERT_OK_AND_ASSIGN(int64_t rows_before_drop,
CountRows(plan_before_drop, /*realtime_context=*/nullptr));
ASSERT_EQ(kRowCount, rows_before_drop);
ASSERT_OK(DropPartition({{"pt", partition}}, /*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap offsets_after_drop, ReadCommittedOffsets());
ASSERT_TRUE(offsets_after_drop.empty());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan_after_drop,
CreatePlan(/*realtime_context=*/nullptr, /*predicate=*/nullptr));
ASSERT_OK_AND_ASSIGN(int64_t rows_after_drop,
CountRows(plan_after_drop, /*realtime_context=*/nullptr));
ASSERT_EQ(0, rows_after_drop);
ASSERT_OK(writer->Close());
}
std::unique_ptr<UniqueTestDirectory> dir_;
std::string table_path_;
std::string commit_user_ = "realtime_commit_user";
arrow::FieldVector fields_;
std::shared_ptr<arrow::Schema> schema_;
std::map<std::string, std::string> options_;
std::shared_ptr<MemoryPool> pool_;
mutable std::mutex external_offsets_mutex_;
mutable std::map<RealtimePartitionBucket, int64_t> next_external_offsets_;
};
TEST_F(RealtimeWriteInteTest, TestRealtimeOperationsRequireEnabledOption) {
CreateTable(/*partition_keys=*/{});
std::map<std::string, std::string> disabled_options = options_;
disabled_options[Options::REALTIME_ENABLED] = "false";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
WriteContextBuilder write_builder(table_path_, commit_user_);
write_builder.SetOptions(disabled_options)
.WithStreamingMode(true)
.WithRealtimeContext(realtime_context);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> write_context, write_builder.Finish());
ASSERT_NOK_WITH_MSG(FileStoreWrite::Create(std::move(write_context)),
"real-time write requires realtime.enabled=true");
ScanContextBuilder scan_builder(table_path_);
scan_builder.SetOptions(disabled_options).WithRealtimeContext(realtime_context);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ScanContext> scan_context, scan_builder.Finish());
ASSERT_NOK_WITH_MSG(TableScan::Create(std::move(scan_context)),
"real-time scan requires realtime.enabled=true");
ReadContextBuilder read_builder(table_path_);
read_builder.SetOptions(disabled_options).WithRealtimeContext(realtime_context);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> read_context, read_builder.Finish());
ASSERT_NOK_WITH_MSG(TableRead::Create(std::move(read_context)),
"real-time read requires realtime.enabled=true");
CommitContextBuilder commit_builder(table_path_, commit_user_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> commit_context,
commit_builder.SetOptions(disabled_options).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(commit_context)));
ASSERT_NOK_WITH_MSG(commit->CommitWithProgress(/*realtime_commits=*/{},
/*commit_identifier=*/0,
/*watermark=*/std::nullopt),
"CommitWithProgress requires realtime.enabled=true");
}
TEST_F(RealtimeWriteInteTest, TestAppendCommitAndRead) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer, CreateRealtimeWriter());
std::vector<Row> rows = MakeRows(/*first_id=*/0, /*count=*/10, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
FinalizeCommitAndCheck(writer.get(), /*realtime_commits=*/{}, /*prepare_identifier=*/0, rows);
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateRollingRestoresEveryOffsetSidecar) {
options_[Options::TARGET_FILE_ROW_NUM] = "2";
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
for (int64_t id = 1; id <= 5; id += 2) {
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch({Row{id, "old", "p0"}, Row{id + 1, "old", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
}
const std::vector<Row> first_expected = {{1, "old", "p0"}, {2, "old", "p0"}, {3, "old", "p0"},
{4, "old", "p0"}, {5, "old", "p0"}, {6, "old", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_prepare, ReadRows(realtime_context));
ASSERT_EQ(first_expected, rows_before_prepare);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, progress.size());
ASSERT_EQ(OffsetRange(0, 6), progress[0].offset_range);
std::shared_ptr<CommitMessageImpl> message =
std::dynamic_pointer_cast<CommitMessageImpl>(progress[0].commit_message);
ASSERT_NE(nullptr, message);
const std::vector<std::shared_ptr<DataFileMeta>>& files =
message->GetNewFilesIncrement().NewFiles();
ASSERT_EQ(3, files.size());
for (const std::shared_ptr<DataFileMeta>& file : files) {
ASSERT_EQ(2, file->row_count);
ASSERT_EQ(1, std::count(file->extra_files.begin(), file->extra_files.end(),
std::optional<std::string>(file->file_name + ".offset")));
}
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_prepare, ReadRows(realtime_context));
ASSERT_EQ(first_expected, rows_after_prepare);
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, /*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_refresh, ReadRows(realtime_context));
ASSERT_EQ(first_expected, rows_before_refresh);
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(first_expected, rows_after_refresh);
ASSERT_OK(writer->Close());
writer.reset();
realtime_context.reset();
// Restart lookup from disk and overwrite one key from each of the three rolled files.
ASSERT_OK_AND_ASSIGN(realtime_context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(writer, CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::vector<Row> restored_rows, ReadRows(realtime_context));
ASSERT_EQ(first_expected, restored_rows);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> update,
MakeBatch({Row{1, "new", "p0"}, Row{3, "new", "p0"}, Row{5, "new", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(update)));
const std::vector<Row> expected = {{2, "old", "p0"}, {4, "old", "p0"}, {6, "old", "p0"},
{1, "new", "p0"}, {3, "new", "p0"}, {5, "new", "p0"}};
ASSERT_OK_AND_ASSIGN(rows_before_prepare, ReadRows(realtime_context));
ASSERT_EQ(expected, rows_before_prepare);
ASSERT_OK_AND_ASSIGN(progress, writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, progress.size());
ASSERT_EQ(OffsetRange(6, 9), progress[0].offset_range);
ASSERT_NE(nullptr, progress[0].deleted_offsets);
ASSERT_EQ(3, progress[0].deleted_offsets->Cardinality());
for (int64_t offset : {0, 2, 4}) {
ASSERT_TRUE(progress[0].deleted_offsets->Contains(offset));
}
ASSERT_OK_AND_ASSIGN(rows_after_prepare, ReadRows(realtime_context));
ASSERT_EQ(expected, rows_after_prepare);
ASSERT_OK_AND_ASSIGN(snapshot_id, Commit(progress, /*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(rows_before_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected, rows_before_refresh);
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(rows_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected, rows_after_refresh);
ASSERT_OK(writer->Close());
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, ReadRows());
ASSERT_EQ(expected, rows);
}
TEST_F(RealtimeWriteInteTest, TestRefreshPreparationAllowsScanWriteAndStoreRegistration) {
options_[Options::BUCKET] = "2";
ConfigureRealtimeDeduplicateTable();
auto fs = std::make_shared<BlockingFileSystem>(dir_->GetFileSystem());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> context, RealtimeContext::Create());
WriteContextBuilder builder(table_path_, commit_user_);
builder.SetOptions(options_)
.WithStreamingMode(true)
.WithRealtimeContext(context)
.WithFileSystem(fs);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> write_context, builder.Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
FileStoreWrite::Create(std::move(write_context)));
const std::vector<Row> initial = {{1, "old", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(initial, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
const auto files = NewFiles(progress);
ASSERT_EQ(1, files.size());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> pinned, CreatePlan(context, /*predicate=*/nullptr));
// Commit the second bucket from a separate context before this writer registers its store.
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> seed_context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> seed_writer,
CreateRealtimeWriter(seed_context));
ASSERT_OK_AND_ASSIGN(batch,
MakeBatch({{2, "second", "p0"}}, /*partitioned=*/false, /*bucket=*/1));
ASSERT_OK(seed_writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> seeded,
seed_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
progress.insert(progress.end(), seeded.begin(), seeded.end());
ASSERT_OK_AND_ASSIGN(int64_t snapshot, Commit(progress, /*commit_identifier=*/0));
ASSERT_OK(seed_writer->Close());
auto open_gate = fs->BlockNext(
BlockingFileSystem::Operation::OPEN,
PathUtil::JoinPath(table_path_, "bucket-0/" + files[0]->file_name + ".offset"));
auto refreshing = std::async(std::launch::async,
[&]() { return writer->RefreshCommittedSnapshot(snapshot); });
std::future<Result<std::vector<Row>>> reading;
std::future<Status> writing;
std::future<Status> registering;
ScopeGuard finish([&]() {
open_gate->Release();
if (reading.valid()) {
reading.wait();
}
if (writing.valid()) {
writing.wait();
}
if (registering.valid()) {
registering.wait();
}
if (refreshing.valid()) {
refreshing.wait();
}
});
ASSERT_TRUE(open_gate->WaitForArrival());
reading = std::async(std::launch::async, [&]() { return ReadRows(context); });
ASSERT_EQ(std::future_status::ready, reading.wait_for(std::chrono::seconds(5)));
ASSERT_OK_AND_ASSIGN(std::vector<Row> visible, reading.get());
std::sort(visible.begin(), visible.end());
ASSERT_EQ((std::vector<Row>{{1, "old", "p0"}, {2, "second", "p0"}}), visible);
writing = std::async(std::launch::async, [&]() -> Status {
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<RecordBatch> update,
MakeBatch({{1, "new", "p0"}}, /*partitioned=*/false));
return writer->Write(std::move(update));
});
ASSERT_EQ(std::future_status::ready, writing.wait_for(std::chrono::seconds(5)));
ASSERT_OK(writing.get());
registering = std::async(std::launch::async, [&]() -> Status {
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<RecordBatch> update,
MakeBatch({{2, "new", "p0"}}, /*partitioned=*/false, /*bucket=*/1));
return writer->Write(std::move(update));
});
ASSERT_EQ(std::future_status::ready, registering.wait_for(std::chrono::seconds(5)));
ASSERT_OK(registering.get());
const std::vector<Row> expected = {{1, "new", "p0"}, {2, "new", "p0"}};
reading = std::async(std::launch::async, [&]() { return ReadRows(context); });
ASSERT_EQ(std::future_status::ready, reading.wait_for(std::chrono::seconds(5)));
ASSERT_OK_AND_ASSIGN(visible, reading.get());
std::sort(visible.begin(), visible.end());
ASSERT_EQ(expected, visible);
open_gate->Release();
ASSERT_OK(refreshing.get());
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues usage, ReadRealtimeMetrics(writer->GetMetrics()));
ASSERT_EQ(0, usage.sealed_rows);
ASSERT_EQ(2, usage.building_rows);
ASSERT_OK_AND_ASSIGN(visible, ReadRows(pinned, context));
ASSERT_EQ(initial, visible);
ASSERT_OK_AND_ASSIGN(visible, ReadRows(context));
std::sort(visible.begin(), visible.end());
ASSERT_EQ(expected, visible);
ASSERT_OK_AND_ASSIGN(progress, writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(2, progress.size());
for (const auto& item : progress) {
ASSERT_EQ(OffsetRange(1, 2), item.offset_range);
ASSERT_NE(nullptr, item.deleted_offsets);
ASSERT_EQ("{0}", item.deleted_offsets->ToString());
}
ASSERT_OK_AND_ASSIGN(snapshot, Commit(progress, /*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot));
ASSERT_OK_AND_ASSIGN(visible, ReadRows());
std::sort(visible.begin(), visible.end());
ASSERT_EQ(expected, visible);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRefreshPreparationFailureKeepsProgressAndCanRetry) {
ConfigureRealtimeDeduplicateTable();
auto fs = std::make_shared<BlockingFileSystem>(dir_->GetFileSystem());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> context, RealtimeContext::Create());
WriteContextBuilder builder(table_path_, commit_user_);
builder.SetOptions(options_)
.WithStreamingMode(true)
.WithRealtimeContext(context)
.WithFileSystem(fs);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> write_context, builder.Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
FileStoreWrite::Create(std::move(write_context)));
const std::vector<Row> initial = {{1, "old", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(initial, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
const auto files = NewFiles(progress);
ASSERT_EQ(1, files.size());
ASSERT_OK_AND_ASSIGN(int64_t snapshot, Commit(progress, /*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues before, ReadRealtimeMetrics(writer->GetMetrics()));
ASSERT_EQ(1, before.sealed_rows);
auto open_gate = fs->BlockNext(
BlockingFileSystem::Operation::OPEN,
PathUtil::JoinPath(table_path_, "bucket-0/" + files[0]->file_name + ".offset"));
open_gate->Release(Status::IOError("injected sidecar open failure"));
ASSERT_NOK_WITH_MSG(writer->RefreshCommittedSnapshot(snapshot),
"injected sidecar open failure");
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues failed, ReadRealtimeMetrics(writer->GetMetrics()));
ASSERT_EQ(before.sealed_rows, failed.sealed_rows);
ASSERT_EQ(before.total_memory, failed.total_memory);
ASSERT_OK_AND_ASSIGN(std::vector<Row> visible, ReadRows(context));
ASSERT_EQ(initial, visible);
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot));
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues after, ReadRealtimeMetrics(writer->GetMetrics()));
ASSERT_EQ(0, after.sealed_rows);
ASSERT_EQ(0, after.total_rows);
ASSERT_OK_AND_ASSIGN(visible, ReadRows(context));
ASSERT_EQ(initial, visible);
ASSERT_OK_AND_ASSIGN(batch, MakeBatch({{1, "new", "p0"}}, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(progress, writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, progress.size());
ASSERT_EQ(OffsetRange(1, 2), progress[0].offset_range);
ASSERT_NE(nullptr, progress[0].deleted_offsets);
ASSERT_EQ("{0}", progress[0].deleted_offsets->ToString());
ASSERT_OK_AND_ASSIGN(snapshot, Commit(progress, /*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot));
ASSERT_OK_AND_ASSIGN(visible, ReadRows());
ASSERT_EQ((std::vector<Row>{{1, "new", "p0"}}), visible);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateWritesDeletionVector) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
const std::vector<Row> first_rows = {{1, "old-one", "p0"}, {2, "two", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch(first_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, first_progress.size());
ASSERT_EQ(OffsetRange(0, 2), first_progress[0].offset_range);
std::shared_ptr<CommitMessageImpl> first_message =
std::dynamic_pointer_cast<CommitMessageImpl>(first_progress[0].commit_message);
ASSERT_NE(nullptr, first_message);
ASSERT_EQ(1, first_message->GetNewFilesIncrement().NewFiles().size());
ASSERT_TRUE(first_message->GetCompactIncrement().NewIndexFiles().empty());
ASSERT_OK_AND_ASSIGN(int64_t first_snapshot_id,
Commit(first_progress, /*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(std::vector<Row> first_rows_before_refresh, ReadRows(realtime_context));
ASSERT_EQ(first_rows, first_rows_before_refresh);
ASSERT_OK(writer->RefreshCommittedSnapshot(first_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> first_rows_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(first_rows, first_rows_after_refresh);
const std::vector<Row> second_rows = {{1, "new-one", "p0"}, {3, "three", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(second_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(second_batch)));
const std::vector<Row> expected_rows = {
{2, "two", "p0"}, {1, "new-one", "p0"}, {3, "three", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, realtime_rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, second_progress.size());
ASSERT_EQ(OffsetRange(2, 4), second_progress[0].offset_range);
std::shared_ptr<CommitMessageImpl> second_message =
std::dynamic_pointer_cast<CommitMessageImpl>(second_progress[0].commit_message);
ASSERT_NE(nullptr, second_message);
ASSERT_EQ(1, second_message->GetNewFilesIncrement().NewFiles().size());
ASSERT_TRUE(second_message->GetCompactIncrement().NewIndexFiles().empty());
ASSERT_NE(nullptr, second_progress[0].deleted_offsets);
ASSERT_FALSE(second_progress[0].deleted_offsets->IsEmpty());
ASSERT_OK_AND_ASSIGN(int64_t second_snapshot_id,
Commit(second_progress, /*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_before_refresh);
ASSERT_OK(writer->RefreshCommittedSnapshot(second_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_after_refresh);
ASSERT_OK(writer->Close());
writer.reset();
realtime_context.reset();
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, disk_rows);
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateRestoresHistoricalSchemas) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer, CreateRealtimeWriter(context));
ASSERT_OK(WriteCommitAndRefresh({{1, "old-one", "p0"}, {2, "old-two", "p0"}},
/*partitioned=*/false, /*bucket=*/0, {}, 0, writer.get()));
ASSERT_OK(WriteCommitAndRefresh({{3, "three", "p0"}}, /*partitioned=*/false,
/*bucket=*/0, {}, 1, writer.get()));
ASSERT_OK(writer->Close());
writer.reset();
context.reset();
// Keep field IDs while renaming the deduplicate key and a regular column, moving the offset
// column, and adding a nullable column. Restore old files and their .offset sidecars by ID.
const auto renamed_key = fields_[1]->WithName("renamed_id");
const auto renamed_payload = fields_[2]->WithName("renamed_payload");
const auto extra = arrow::field("extra", arrow::utf8());
options_[Options::REALTIME_DEDUPLICATE_KEY_FIELDS] = renamed_key->name();
options_[Options::BUCKET_KEY] = renamed_key->name();
ASSERT_OK(TestHelper::WriteNextSchema(
dir_->GetFileSystem(), table_path_,
{DataField(1, renamed_key), DataField(2, renamed_payload), DataField(3, fields_[3]),
DataField(0, fields_[0]), DataField(4, extra)},
/*highest_field_id=*/4, options_));
fields_ = {renamed_key, renamed_payload, fields_[3], fields_[0], extra};
schema_ = arrow::schema(fields_);
ASSERT_OK_AND_ASSIGN(context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(writer, CreateRealtimeWriter(context));
auto array = arrow::ipc::internal::json::ArrayFromJSON(arrow::struct_(schema_->fields()), R"([
[1, "new-one", "p0", 3, "x"],
[2, "deleted", "p0", 4, null],
[4, "four", "p0", 5, "y"]
])")
.ValueOrDie();
ArrowArray c_array;
ASSERT_TRUE(arrow::ExportArray(*array, &c_array).ok());
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> batch,
RecordBatchBuilder(&c_array)
.SetBucket(0)
.SetRowKinds({RecordBatch::RowKind::INSERT, RecordBatch::RowKind::DELETE,
RecordBatch::RowKind::INSERT})
.Finish());
ASSERT_OK(writer->Write(std::move(batch)));
const auto projection = arrow::schema({fields_[0], fields_[1], fields_[4]});
const std::string expected = R"([
[0, 3, "three", null],
[0, 1, "new-one", "x"],
[0, 4, "four", "y"]
])";
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan, CreatePlan(context, /*predicate=*/nullptr));
ReadPlanWithSchemaAndCheck(plan, context, projection, expected);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(2));
ASSERT_OK_AND_ASSIGN(int64_t snapshot, Commit(progress, 2));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot));
ASSERT_OK(writer->Close());
writer.reset();
context.reset();
// Persisted file-position DVs must still hide the overwritten/deleted historical rows.
ASSERT_OK_AND_ASSIGN(context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(writer, CreateRealtimeWriter(context));
ASSERT_OK_AND_ASSIGN(plan, CreatePlan(context, /*predicate=*/nullptr));
ReadPlanWithSchemaAndCheck(plan, context, projection, expected);
ASSERT_OK_AND_ASSIGN(Snapshot compact_snapshot, CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
/*commit_identifier=*/3));
ASSERT_EQ(Snapshot::CommitKind::Compact(), compact_snapshot.GetCommitKind());
ASSERT_OK(writer->RefreshCommittedSnapshot(compact_snapshot.Id()));
ASSERT_OK_AND_ASSIGN(plan, CreatePlan(context, /*predicate=*/nullptr));
ReadPlanWithSchemaAndCheck(plan, context, projection, expected);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateCommitRetryMergesLatestDv) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> initial_batch,
MakeBatch({Row{1, "old-one", "p0"}, Row{2, "two", "p0"}, Row{3, "three", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(initial_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> initial_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, initial_progress.size());
ASSERT_NE(nullptr, initial_progress[0].deleted_offsets);
ASSERT_TRUE(initial_progress[0].deleted_offsets->IsEmpty());
ASSERT_OK_AND_ASSIGN(int64_t initial_snapshot,
Commit(initial_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(initial_snapshot));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> update_batch,
MakeBatch({Row{1, "new-one", "p0"}}, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(update_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, progress.size());
ASSERT_NE(nullptr, progress[0].deleted_offsets);
ASSERT_EQ("{0}", progress[0].deleted_offsets->ToString());
auto initial_message =
std::dynamic_pointer_cast<CommitMessageImpl>(initial_progress[0].commit_message);
auto update_message = std::dynamic_pointer_cast<CommitMessageImpl>(progress[0].commit_message);
ASSERT_NE(nullptr, initial_message);
ASSERT_NE(nullptr, update_message);
ASSERT_TRUE(update_message->GetCompactIncrement().IsEmpty());
std::vector<RealtimeCommitProgress> missing_deletions = progress;
missing_deletions[0].deleted_offsets.reset();
ASSERT_NOK_WITH_MSG(Commit(missing_deletions, /*commit_identifier=*/1),
"requires a deleted-offset bitmap");
const std::vector<std::shared_ptr<DataFileMeta>> initial_files = NewFiles(initial_progress);
ASSERT_EQ(1, initial_files.size());
// A concurrent physical DV update deletes a different row from the same file and bucket.
auto index_paths =
std::make_shared<MockIndexPathFactory>(PathUtil::JoinPath(table_path_, "index"));
DeletionVectorsIndexFile dv_file(dir_->GetFileSystem(), index_paths, /*bitmap64=*/false, pool_);
RoaringBitmap32 deleted_positions;
deleted_positions.Add(1);
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<IndexFileMeta> concurrent_dv,
dv_file.WriteSingleFile({{initial_files[0]->file_name,
std::make_shared<BitmapDeletionVector>(deleted_positions)}}));
auto concurrent_message = std::make_shared<CommitMessageImpl>(
initial_message->Partition(), /*bucket=*/0, /*total_buckets=*/1,
DataIncrement({}, {}, {}, {concurrent_dv}, {}), CompactIncrement({}, {}, {}, {}, {}));
CommitContextBuilder concurrent_builder(table_path_, "concurrent-dv-writer");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> concurrent_context,
concurrent_builder.SetOptions(options_).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> concurrent_commit,
FileStoreCommit::Create(std::move(concurrent_context)));
bool interleaved = false;
auto fs = std::make_shared<InterleavingSnapshotFileSystem>(
PathUtil::JoinPath(table_path_, "snapshot/snapshot-2"), [&]() -> Status {
interleaved = true;
return concurrent_commit->Commit({concurrent_message}, /*commit_identifier=*/0,
/*watermark=*/std::nullopt);
});
CommitContextBuilder builder(table_path_, commit_user_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> context,
builder.SetOptions(options_).WithFileSystem(fs).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(context)));
ASSERT_OK_AND_ASSIGN(
int64_t snapshot_id,
commit->CommitWithProgress(progress, /*commit_identifier=*/1, /*watermark=*/std::nullopt));
ASSERT_TRUE(interleaved);
ASSERT_EQ(3, snapshot_id);
ASSERT_OK_AND_ASSIGN(uint64_t attempts, commit->GetCommitMetrics()->GetCounter(
CommitMetrics::LAST_COMMIT_ATTEMPTS));
ASSERT_EQ(2, attempts);
ASSERT_EQ("{0}", progress[0].deleted_offsets->ToString());
const std::vector<Row> expected_rows = {{3, "three", "p0"}, {1, "new-one", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, disk_rows);
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_before_refresh);
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_after_refresh);
ASSERT_OK_AND_ASSIGN(
int64_t retried_snapshot,
commit->CommitWithProgress(progress, /*commit_identifier=*/1, /*watermark=*/std::nullopt));
ASSERT_EQ(snapshot_id, retried_snapshot);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateCommitRetryRemapsCompactedOffsets) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
for (int64_t id = 1; id <= 2; ++id) {
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch({Row{id, "value", "p0"}}, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/id - 1));
ASSERT_OK_AND_ASSIGN(int64_t snapshot, Commit(progress, /*commit_identifier=*/id - 1));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot));
}
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> delete_batch,
MakeBatch({Row{2, "ignored", "p0"}}, /*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::DELETE}));
ASSERT_OK(writer->Write(std::move(delete_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
ASSERT_EQ(1, progress.size());
ASSERT_NE(nullptr, progress[0].deleted_offsets);
ASSERT_EQ("{1}", progress[0].deleted_offsets->ToString());
bool interleaved = false;
auto fs = std::make_shared<InterleavingSnapshotFileSystem>(
PathUtil::JoinPath(table_path_, "snapshot/snapshot-3"), [&]() -> Status {
PAIMON_ASSIGN_OR_RAISE(
Snapshot compacted,
CompactAndCommit(/*partition=*/{}, /*bucket=*/0, /*commit_identifier=*/1));
if (compacted.Id() != 3 ||
compacted.GetCommitKind() != Snapshot::CommitKind::Compact()) {
return Status::Invalid("expected competing compaction snapshot");
}
interleaved = true;
return Status::OK();
});
CommitContextBuilder builder(table_path_, commit_user_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> context,
builder.SetOptions(options_).WithFileSystem(fs).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(context)));
ASSERT_OK_AND_ASSIGN(
int64_t snapshot_id,
commit->CommitWithProgress(progress, /*commit_identifier=*/2, /*watermark=*/std::nullopt));
ASSERT_TRUE(interleaved);
ASSERT_EQ(4, snapshot_id);
ASSERT_OK_AND_ASSIGN(uint64_t attempts, commit->GetCommitMetrics()->GetCounter(
CommitMetrics::LAST_COMMIT_ATTEMPTS));
ASSERT_EQ(2, attempts);
ASSERT_EQ("{1}", progress[0].deleted_offsets->ToString());
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ((std::vector<Row>{{1, "value", "p0"}}), disk_rows);
ASSERT_OK(writer->Close());
}
class RealtimeDeduplicateCompactionInteTest
: public RealtimeWriteInteTest,
public ::testing::WithParamInterface<std::tuple<bool, bool>> {};
TEST_P(RealtimeDeduplicateCompactionInteTest, TestCommitTransfersLatestDvToCompactedFiles) {
const auto [force_retry, prepare_between_rounds] = GetParam();
options_[Options::COMPACTION_FORCE_REWRITE_ALL_FILES] = "true";
options_[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1B";
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
// Establish D0 before compaction: offset 0 must not appear in either rewrite's output.
const std::vector<std::vector<Row>> batches = {
{{1, "one", "p0"}, {2, "two", "p0"}, {3, "three", "p0"}},
{{4, "four", "p0"}, {5, "five", "p0"}},
{{1, "ignored", "p0"}}};
for (int64_t identifier = 0; identifier < 3; ++identifier) {
const std::vector<RecordBatch::RowKind> kinds =
identifier == 2 ? std::vector<RecordBatch::RowKind>{RecordBatch::RowKind::DELETE}
: std::vector<RecordBatch::RowKind>{};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(batches[identifier], /*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/kinds));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/identifier));
ASSERT_OK_AND_ASSIGN(int64_t snapshot, Commit(progress, /*commit_identifier=*/identifier));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot));
}
WriteContextBuilder write_builder(table_path_, "compactor");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> write_context,
write_builder.SetOptions(options_).WithStreamingMode(true).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> compactor,
FileStoreWrite::Create(std::move(write_context)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> forbidden_batch,
MakeBatch({{99, "not-written", "p0"}}, /*partitioned=*/false));
ASSERT_NOK_WITH_MSG(compactor->Write(std::move(forbidden_batch)), "only supports compaction");
ResetExternalOffset(/*partition=*/{}, /*bucket=*/0, /*next_offset=*/6);
// F1/F2 -> F3 -> F4, with no snapshot publication between the two rewrites.
std::vector<std::shared_ptr<CommitMessage>> messages;
for (int32_t round = 0; round < 2; ++round) {
ASSERT_OK(compactor->Compact(/*partition=*/{}, /*bucket=*/0, /*full_compaction=*/true));
if (prepare_between_rounds || round == 1) {
ASSERT_OK_AND_ASSIGN(
std::vector<std::shared_ptr<CommitMessage>> prepared,
compactor->PrepareCommit(/*wait_compaction=*/true, /*commit_identifier=*/0));
messages.insert(messages.end(), prepared.begin(), prepared.end());
}
}
ManifestEntryChanges changes(1, /*drop_delete_file_stats=*/true);
for (const auto& message : messages) {
ASSERT_OK(changes.Collect(message));
}
ASSERT_OK_AND_ASSIGN(RealtimeCompactionPlan compact_plan,
RealtimeCompactionPlan::Create(changes.compact_table_files));
ASSERT_EQ(1, compact_plan.intermediate_files.size());
std::shared_ptr<DataFileMeta> final_output;
for (const ManifestEntry& entry : compact_plan.delta_files) {
if (entry.Kind() == FileKind::Add()) {
ASSERT_EQ(nullptr, final_output);
final_output = entry.File();
}
}
ASSERT_NE(nullptr, final_output);
ASSERT_EQ(4, final_output->row_count);
ASSERT_FALSE(final_output->extra_files.empty());
// D1 is published against compact-before AFTER both rewrites have completed.
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> update_batch,
MakeBatch(
{{2, "updated", "p0"}, {3, "ignored", "p0"}}, /*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/{RecordBatch::RowKind::UPDATE_AFTER, RecordBatch::RowKind::DELETE}));
ASSERT_OK(writer->Write(std::move(update_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> update_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/3));
ASSERT_OK_AND_ASSIGN(int64_t update_snapshot, Commit(update_progress, /*commit_identifier=*/3));
ASSERT_EQ(4, update_snapshot);
ASSERT_OK(writer->RefreshCommittedSnapshot(update_snapshot));
// D2 also deletes from a non-compacted file. Its DV must survive bucket replacement.
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> concurrent_batch,
MakeBatch(
{{2, "latest", "p0"}, {4, "ignored", "p0"}}, /*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/{RecordBatch::RowKind::UPDATE_AFTER, RecordBatch::RowKind::DELETE}));
ASSERT_OK(writer->Write(std::move(concurrent_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> concurrent_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/4));
bool interleaved = false;
auto publish_build = [&]() -> Status {
PAIMON_ASSIGN_OR_RAISE(int64_t snapshot,
Commit(concurrent_progress, /*commit_identifier=*/4));
if (snapshot != 5) {
return Status::Invalid("expected interleaving build snapshot 5");
}
interleaved = true;
return Status::OK();
};
std::shared_ptr<FileSystem> fs = dir_->GetFileSystem();
if (force_retry) {
fs = std::make_shared<InterleavingSnapshotFileSystem>(
PathUtil::JoinPath(table_path_, "snapshot/snapshot-5"), publish_build);
} else {
ASSERT_OK(publish_build());
}
CommitContextBuilder commit_builder(table_path_, "compactor");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> commit_context,
commit_builder.SetOptions(options_).WithFileSystem(fs).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(commit_context)));
ASSERT_OK(commit->Commit(messages, /*commit_identifier=*/0));
ASSERT_TRUE(interleaved);
// Only unpublished intermediate outputs and their sidecars are reclaimed after publication.
const std::string bucket_path = PathUtil::JoinPath(table_path_, "bucket-0");
const auto& intermediate = compact_plan.intermediate_files[0].File();
ASSERT_EQ(2, intermediate->extra_files.size());
std::vector<std::pair<std::shared_ptr<DataFileMeta>, bool>> expected_files = {
{intermediate, /*exists_after_commit=*/false},
{final_output, /*exists_after_commit=*/true}};
// Original inputs and their sidecars are still referenced by historical snapshots.
for (const ManifestEntry& entry : compact_plan.delta_files) {
if (entry.Kind() == FileKind::Delete()) {
ASSERT_EQ(2, entry.File()->extra_files.size());
expected_files.emplace_back(entry.File(), /*exists_after_commit=*/true);
}
}
ASSERT_EQ(4, expected_files.size());
for (const auto& [file, expected_exists] : expected_files) {
ASSERT_OK_AND_ASSIGN(bool exists, dir_->GetFileSystem()->Exists(
PathUtil::JoinPath(bucket_path, file->file_name)));
ASSERT_EQ(expected_exists, exists);
for (const auto& extra : file->extra_files) {
ASSERT_TRUE(extra);
ASSERT_OK_AND_ASSIGN(
exists, dir_->GetFileSystem()->Exists(PathUtil::JoinPath(bucket_path, *extra)));
ASSERT_EQ(expected_exists, exists);
}
}
ASSERT_OK_AND_ASSIGN(uint64_t attempts, commit->GetCommitMetrics()->GetCounter(
CommitMetrics::LAST_COMMIT_ATTEMPTS));
ASSERT_EQ(force_retry ? 2 : 1, attempts);
const std::vector<Row> expected = {{5, "five", "p0"}, {2, "latest", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected, disk_rows);
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected, realtime_rows);
// Prove migration, not an implicit re-rewrite: F4 is unchanged, with a physical DV {0,1,2}.
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(/*realtime_context=*/nullptr, /*predicate=*/nullptr));
bool checked_output = false;
for (const auto& split : plan->Splits()) {
auto data = std::dynamic_pointer_cast<DataSplitImpl>(split);
ASSERT_NE(nullptr, data);
for (size_t i = 0; i < data->DataFiles().size(); ++i) {
if (data->DataFiles()[i]->file_name != final_output->file_name) {
continue;
}
checked_output = true;
ASSERT_EQ(4, data->DataFiles()[i]->row_count);
ASSERT_EQ(data->DataFiles().size(), data->DeletionFiles().size());
ASSERT_TRUE(data->DeletionFiles()[i]);
ASSERT_OK_AND_ASSIGN(
PAIMON_UNIQUE_PTR<DeletionVector> dv,
DeletionVector::Read(fs.get(), *data->DeletionFiles()[i], pool_.get()));
ASSERT_OK_AND_ASSIGN(int64_t count, dv->GetCardinality());
ASSERT_EQ(3, count);
for (int64_t position = 0; position < 4; ++position) {
ASSERT_OK_AND_ASSIGN(bool deleted, dv->IsDeleted(position));
ASSERT_EQ(position < 3, deleted);
}
}
}
ASSERT_TRUE(checked_output);
ASSERT_OK(writer->RefreshCommittedSnapshot(6));
ASSERT_OK_AND_ASSIGN(realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected, realtime_rows);
ASSERT_OK(writer->Close());
ASSERT_OK(compactor->Close());
// Restart uses the newly compacted files' key->offset sidecars and keeps existing DVs.
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> restarted_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(writer, CreateRealtimeWriter(restarted_context));
ASSERT_OK_AND_ASSIGN(realtime_rows, ReadRows(restarted_context));
ASSERT_EQ(expected, realtime_rows);
ASSERT_OK_AND_ASSIGN(disk_rows, ReadRows());
ASSERT_EQ(expected, disk_rows);
const std::vector<Row> expected_after_delete = {{2, "latest", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> delete_batch,
MakeBatch({{5, "ignored", "p0"}}, /*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/{RecordBatch::RowKind::DELETE}));
ASSERT_OK(writer->Write(std::move(delete_batch)));
// The buffered deletion is visible in realtime, but is not durable yet.
ASSERT_OK_AND_ASSIGN(realtime_rows, ReadRows(restarted_context));
ASSERT_EQ(expected_after_delete, realtime_rows);
ASSERT_OK_AND_ASSIGN(disk_rows, ReadRows());
ASSERT_EQ(expected, disk_rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> delete_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/5));
ASSERT_OK_AND_ASSIGN(realtime_rows, ReadRows(restarted_context));
ASSERT_EQ(expected_after_delete, realtime_rows);
ASSERT_OK_AND_ASSIGN(disk_rows, ReadRows());
ASSERT_EQ(expected, disk_rows);
ASSERT_OK_AND_ASSIGN(int64_t snapshot, Commit(delete_progress, /*commit_identifier=*/5));
ASSERT_EQ(7, snapshot);
// Publication must preserve realtime visibility even before the writer acknowledges it.
ASSERT_OK_AND_ASSIGN(realtime_rows, ReadRows(restarted_context));
ASSERT_EQ(expected_after_delete, realtime_rows);
ASSERT_OK_AND_ASSIGN(disk_rows, ReadRows());
ASSERT_EQ(expected_after_delete, disk_rows);
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot));
ASSERT_OK_AND_ASSIGN(realtime_rows, ReadRows(restarted_context));
ASSERT_EQ(expected_after_delete, realtime_rows);
ASSERT_OK_AND_ASSIGN(disk_rows, ReadRows());
ASSERT_EQ(expected_after_delete, disk_rows);
ASSERT_OK(writer->Close());
}
INSTANTIATE_TEST_SUITE_P(Compaction, RealtimeDeduplicateCompactionInteTest,
::testing::Combine(::testing::Bool(), ::testing::Bool()));
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateCompactionRejectsReplacedInputs) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer, CreateRealtimeWriter());
const std::vector<Row> expected = {{1, "one", "p0"}, {2, "two", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(expected, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t initial_snapshot, Commit(progress, /*commit_identifier=*/0));
ASSERT_EQ(1, initial_snapshot);
ASSERT_OK(writer->Close());
WriteContextBuilder write_builder(table_path_, "stale-compactor");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> write_context,
write_builder.SetOptions(options_).WithStreamingMode(true).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> compactor,
FileStoreWrite::Create(std::move(write_context)));
ASSERT_OK(compactor->Compact(/*partition=*/{}, /*bucket=*/0, /*full_compaction=*/true));
ASSERT_OK_AND_ASSIGN(
std::vector<std::shared_ptr<CommitMessage>> messages,
compactor->PrepareCommit(/*wait_compaction=*/true, /*commit_identifier=*/0));
ASSERT_EQ(1, messages.size());
auto message = std::dynamic_pointer_cast<CommitMessageImpl>(messages[0]);
ASSERT_NE(nullptr, message);
ASSERT_EQ(1, message->GetCompactIncrement().CompactAfter().size());
ASSERT_OK_AND_ASSIGN(Snapshot replacement,
CompactAndCommit(/*partition=*/{}, /*bucket=*/0, /*commit_identifier=*/1));
ASSERT_EQ(2, replacement.Id());
CommitContextBuilder commit_builder(table_path_, "stale-compactor");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> commit_context,
commit_builder.SetOptions(options_).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(commit_context)));
ASSERT_NOK_WITH_MSG(commit->Commit(messages, /*commit_identifier=*/0),
"input is no longer active");
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, ReadRows());
ASSERT_EQ(expected, rows);
// A known rejected commit can be aborted without damaging the winning compaction.
ASSERT_OK(commit->Abort(messages));
for (const auto& file : message->GetCompactIncrement().CompactAfter()) {
const std::string bucket_path = PathUtil::JoinPath(table_path_, "bucket-0");
ASSERT_OK_AND_ASSIGN(bool exists, dir_->GetFileSystem()->Exists(
PathUtil::JoinPath(bucket_path, file->file_name)));
ASSERT_FALSE(exists);
for (const auto& extra : file->extra_files) {
ASSERT_TRUE(extra);
ASSERT_OK_AND_ASSIGN(
exists, dir_->GetFileSystem()->Exists(PathUtil::JoinPath(bucket_path, *extra)));
ASSERT_FALSE(exists);
}
}
ASSERT_OK_AND_ASSIGN(rows, ReadRows());
ASSERT_EQ(expected, rows);
ASSERT_OK(compactor->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateCompactionWithAllRowsDeleted) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
const std::vector<Row> rows = {{1, "one", "p0"}, {2, "two", "p0"}};
for (int64_t identifier = 0; identifier < 2; ++identifier) {
const std::vector<Row> expected_rows = identifier == 0 ? rows : std::vector<Row>{};
const std::vector<RecordBatch::RowKind> kinds =
identifier == 1 ? std::vector<RecordBatch::RowKind>(2, RecordBatch::RowKind::DELETE)
: std::vector<RecordBatch::RowKind>{};
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false, /*bucket=*/0, /*row_kinds=*/kinds));
ASSERT_OK(writer->Write(std::move(batch)));
// Check both the initial rows and their deletion before either is committed.
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, realtime_rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/identifier));
ASSERT_OK_AND_ASSIGN(int64_t snapshot, Commit(progress, /*commit_identifier=*/identifier));
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, disk_rows);
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot));
ASSERT_OK_AND_ASSIGN(realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, realtime_rows);
}
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap offsets_before, ReadCommittedOffsets());
ASSERT_OK_AND_ASSIGN(Snapshot compacted,
CompactAndCommit(/*partition=*/{}, /*bucket=*/0, /*commit_identifier=*/2));
ASSERT_EQ(3, compacted.Id());
ASSERT_EQ(Snapshot::CommitKind::Compact(), compacted.GetCommitKind());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(/*realtime_context=*/nullptr, /*predicate=*/nullptr));
ASSERT_TRUE(plan->Splits().empty());
// Empty split metadata alone is not enough: read through the disk and realtime paths.
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows(plan, nullptr));
ASSERT_TRUE(disk_rows.empty());
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_TRUE(realtime_rows.empty());
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap offsets_after, ReadCommittedOffsets());
ASSERT_EQ(offsets_before, offsets_after);
ASSERT_OK(writer->RefreshCommittedSnapshot(compacted.Id()));
ASSERT_OK_AND_ASSIGN(realtime_rows, ReadRows(realtime_context));
ASSERT_TRUE(realtime_rows.empty());
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestDeletesAllRowsAfterCompactionRewrite) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
const std::vector<Row> rows = {{1, "one", "p0"}, {2, "two", "p0"}, {3, "three", "p0"}};
ASSERT_OK(WriteCommitAndRefresh(rows, /*partitioned=*/false, /*bucket=*/0, /*row_kinds=*/{},
/*commit_identifier=*/0, writer.get()));
std::unique_ptr<FileStoreWrite> compactor;
ASSERT_OK_AND_ASSIGN(
std::vector<std::shared_ptr<CommitMessage>> messages,
CompactAndPrepareCommit({RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)},
/*commit_identifier=*/0, /*full_compaction=*/true, &compactor));
ASSERT_EQ(1, messages.size());
auto message = std::dynamic_pointer_cast<CommitMessageImpl>(messages[0]);
ASSERT_NE(nullptr, message);
ASSERT_EQ(1, message->GetCompactIncrement().CompactAfter().size());
const auto& output = message->GetCompactIncrement().CompactAfter()[0];
ASSERT_EQ(3, output->row_count);
// D0 is empty; D1 deletes every row only after a real output has been written.
ASSERT_OK(WriteCommitAndRefresh(
rows, /*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/std::vector<RecordBatch::RowKind>(3, RecordBatch::RowKind::DELETE),
/*commit_identifier=*/1, writer.get()));
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_TRUE(disk_rows.empty());
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_TRUE(realtime_rows.empty());
}
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap offsets, ReadCommittedOffsets());
ASSERT_EQ(1, offsets.size());
ASSERT_EQ(6, offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_OK_AND_ASSIGN(Snapshot compacted, CommitCompaction(messages, /*commit_identifier=*/0));
ASSERT_EQ(3, compacted.Id());
ASSERT_OK_AND_ASSIGN(std::vector<FileState> files, InspectFiles());
ASSERT_EQ(1, files.size());
ASSERT_EQ(output->file_name, files[0].file->file_name);
ASSERT_EQ(3, files[0].file->row_count);
ASSERT_TRUE(files[0].deletion_file);
ASSERT_EQ((std::set<int64_t>{0, 1, 2}), files[0].deleted_positions);
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_TRUE(disk_rows.empty());
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_TRUE(realtime_rows.empty());
}
ASSERT_OK(writer->RefreshCommittedSnapshot(compacted.Id()));
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_TRUE(disk_rows.empty());
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_TRUE(realtime_rows.empty());
}
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap after, ReadCommittedOffsets());
ASSERT_EQ(offsets, after);
ASSERT_OK(compactor->Close());
// A later rewrite retires the fully deleted output.
ASSERT_OK_AND_ASSIGN(
Snapshot reclaimed,
CompactCommitAndRefresh(/*partition=*/{}, /*bucket=*/0, /*commit_identifier=*/2,
/*full_compaction=*/true, writer.get()));
ASSERT_EQ(4, reclaimed.Id());
ASSERT_OK_AND_ASSIGN(files, InspectFiles());
ASSERT_TRUE(files.empty());
ASSERT_OK_AND_ASSIGN(after, ReadCommittedOffsets());
ASSERT_EQ(offsets, after);
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_TRUE(disk_rows.empty());
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_TRUE(realtime_rows.empty());
}
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestSparseDeletedOffsetsAcrossReaderBatches) {
options_[Options::READ_BATCH_SIZE] = "3";
// Keep holes in the raw batch so NextBatchWithBitmap must honor its sparse selection.
options_["parquet.read.enable-page-index-filter"] = "false";
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK(WriteCommitAndRefresh(MakeRows(/*first_id=*/0, /*count=*/9, /*partition=*/"p0"),
/*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/{}, /*commit_identifier=*/0, writer.get()));
ASSERT_OK(WriteCommitAndRefresh(MakeRows(/*first_id=*/9, /*count=*/9, /*partition=*/"p0"),
/*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/{}, /*commit_identifier=*/1, writer.get()));
std::unique_ptr<FileStoreWrite> compactor;
ASSERT_OK_AND_ASSIGN(
std::vector<std::shared_ptr<CommitMessage>> messages,
CompactAndPrepareCommit({RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)},
/*commit_identifier=*/0, /*full_compaction=*/true, &compactor));
ASSERT_EQ(1, messages.size());
auto message = std::dynamic_pointer_cast<CommitMessageImpl>(messages[0]);
ASSERT_NE(nullptr, message);
ASSERT_EQ(2, message->GetCompactIncrement().CompactBefore().size());
const std::vector<Row> deleted = {{0, "ignored", "p0"}, {2, "ignored", "p0"},
{7, "ignored", "p0"}, {10, "ignored", "p0"},
{14, "ignored", "p0"}, {17, "ignored", "p0"}};
ASSERT_OK(WriteCommitAndRefresh(
deleted, /*partitioned=*/false, /*bucket=*/0, /*row_kinds=*/
std::vector<RecordBatch::RowKind>(deleted.size(), RecordBatch::RowKind::DELETE),
/*commit_identifier=*/2, writer.get()));
std::vector<Row> expected;
const std::set<int64_t> positions = {0, 2, 7, 10, 14, 17};
for (const Row& row : MakeRows(/*first_id=*/0, /*count=*/18, /*partition=*/"p0")) {
if (positions.count(std::get<0>(row)) == 0) {
expected.push_back(row);
}
}
const std::multiset<Row> expected_rows(expected.begin(), expected.end());
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
ASSERT_OK_AND_ASSIGN(Snapshot compacted, CommitCompaction(messages, /*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(std::vector<FileState> files, InspectFiles());
ASSERT_EQ(1, files.size());
ASSERT_EQ(18, files[0].file->row_count);
ASSERT_EQ(positions, files[0].deleted_positions);
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
ASSERT_OK(writer->RefreshCommittedSnapshot(compacted.Id()));
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
ASSERT_OK(writer->Close());
ASSERT_OK(compactor->Close());
}
TEST_F(RealtimeWriteInteTest, TestCompactionRollsAndRemapsMultipleOutputs) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK(WriteCommitAndRefresh(MakeRows(/*first_id=*/0, /*count=*/1200, /*partition=*/"p0"),
/*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/{}, /*commit_identifier=*/0, writer.get()));
ASSERT_OK(WriteCommitAndRefresh(MakeRows(/*first_id=*/1200, /*count=*/1200, /*partition=*/"p0"),
/*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/{}, /*commit_identifier=*/1, writer.get()));
// Parquet checks target size every 1000 records, independently of target-file-row-num.
options_[Options::TARGET_FILE_SIZE] = "1";
options_[Options::READ_BATCH_SIZE] = "100";
options_[Options::COMPACTION_FORCE_REWRITE_ALL_FILES] = "true";
std::unique_ptr<FileStoreWrite> compactor;
ASSERT_OK_AND_ASSIGN(
std::vector<std::shared_ptr<CommitMessage>> messages,
CompactAndPrepareCommit({RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)},
/*commit_identifier=*/0, /*full_compaction=*/true, &compactor));
ASSERT_EQ(1, messages.size());
auto message = std::dynamic_pointer_cast<CommitMessageImpl>(messages[0]);
ASSERT_NE(nullptr, message);
const auto& outputs = message->GetCompactIncrement().CompactAfter();
ASSERT_GT(outputs.size(), 1);
// Pick one row from every physical output, including across the original file boundary.
std::vector<Row> deleted;
std::map<std::string, std::set<int64_t>> expected_dvs;
int64_t first_id = 0;
for (const auto& output : outputs) {
ASSERT_GT(output->row_count, 1);
deleted.emplace_back(first_id, "ignored", "p0");
expected_dvs[output->file_name] = {0};
first_id += output->row_count;
bool has_offset_sidecar = false;
for (const auto& extra : output->extra_files) {
ASSERT_TRUE(extra);
has_offset_sidecar |=
extra->size() >= 7 && extra->compare(extra->size() - 7, 7, ".offset") == 0;
ASSERT_OK_AND_ASSIGN(bool exists,
dir_->GetFileSystem()->Exists(PathUtil::JoinPath(
PathUtil::JoinPath(table_path_, "bucket-0"), *extra)));
ASSERT_TRUE(exists);
}
ASSERT_TRUE(has_offset_sidecar);
}
ASSERT_EQ(2400, first_id);
ASSERT_OK(WriteCommitAndRefresh(
deleted, /*partitioned=*/false, /*bucket=*/0, /*row_kinds=*/
std::vector<RecordBatch::RowKind>(deleted.size(), RecordBatch::RowKind::DELETE),
/*commit_identifier=*/2, writer.get()));
std::vector<Row> expected = MakeRows(/*first_id=*/0, /*count=*/2400, /*partition=*/"p0");
expected.erase(std::remove_if(expected.begin(), expected.end(),
[&](const Row& row) {
return std::any_of(
deleted.begin(), deleted.end(), [&](const Row& deletion) {
return std::get<0>(row) == std::get<0>(deletion);
});
}),
expected.end());
ASSERT_OK_AND_ASSIGN(Snapshot compacted, CommitCompaction(messages, /*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(std::vector<FileState> files, InspectFiles());
ASSERT_EQ(outputs.size(), files.size());
for (const FileState& file : files) {
ASSERT_EQ(1, expected_dvs.count(file.file->file_name));
ASSERT_EQ(expected_dvs.at(file.file->file_name), file.deleted_positions);
}
std::multiset<Row> expected_rows(expected.begin(), expected.end());
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
ASSERT_OK(writer->RefreshCommittedSnapshot(compacted.Id()));
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
ASSERT_OK(writer->Close());
ASSERT_OK(compactor->Close());
// Restore every rolled key->offset sidecar and delete a surviving row from every output.
ASSERT_OK_AND_ASSIGN(realtime_context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(writer, CreateRealtimeWriter(realtime_context));
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
std::vector<Row> restart_deletes;
for (const Row& row : deleted) {
restart_deletes.emplace_back(std::get<0>(row) + 1, "ignored", "p0");
}
ASSERT_OK(WriteCommitAndRefresh(
restart_deletes, /*partitioned=*/false, /*bucket=*/0, /*row_kinds=*/
std::vector<RecordBatch::RowKind>(restart_deletes.size(), RecordBatch::RowKind::DELETE),
/*commit_identifier=*/3, writer.get()));
expected.erase(std::remove_if(expected.begin(), expected.end(),
[&](const Row& row) {
return std::any_of(
restart_deletes.begin(), restart_deletes.end(),
[&](const Row& deletion) {
return std::get<0>(row) == std::get<0>(deletion);
});
}),
expected.end());
expected_rows = std::multiset<Row>(expected.begin(), expected.end());
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
ASSERT_OK_AND_ASSIGN(files, InspectFiles());
ASSERT_EQ(outputs.size(), files.size());
for (const FileState& file : files) {
ASSERT_EQ(1, expected_dvs.count(file.file->file_name));
ASSERT_EQ((std::set<int64_t>{0, 1}), file.deleted_positions);
}
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestCompactionIsolatesPartitionsAndBuckets) {
options_[Options::BUCKET] = "2";
ConfigureRealtimeDeduplicateTable(/*partition_keys=*/{"pt"});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
const std::vector<std::pair<std::string, int32_t>> scopes = {
{"p0", 0}, {"p0", 1}, {"p1", 0}, {"p1", 1}};
for (const auto& [partition, bucket] : scopes) {
const std::vector<Row> rows = {
{0, fmt::format("{}-b{}-zero", partition, bucket), partition},
{1, fmt::format("{}-b{}-one", partition, bucket), partition},
{2, fmt::format("{}-b{}-two", partition, bucket), partition}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/true, /*bucket=*/bucket));
ASSERT_OK(writer->Write(std::move(batch)));
}
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> initial,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(4, initial.size());
ASSERT_OK_AND_ASSIGN(int64_t snapshot, Commit(initial, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot));
// The fourth scope is not compacted. Its existing DV must survive the other replacements.
ASSERT_OK(WriteCommitAndRefresh({{0, "ignored", "p1"}}, /*partitioned=*/true, /*bucket=*/1,
/*row_kinds=*/{RecordBatch::RowKind::DELETE},
/*commit_identifier=*/1, writer.get()));
ASSERT_OK_AND_ASSIGN(std::vector<FileState> baseline, InspectFiles());
std::optional<DeletionFile> untouched_dv;
std::string untouched_name;
for (const FileState& file : baseline) {
if (file.deletion_file) {
ASSERT_FALSE(untouched_dv);
untouched_dv = file.deletion_file;
untouched_name = file.file->file_name;
}
}
ASSERT_TRUE(untouched_dv);
std::vector<RealtimePartitionBucket> compact_scopes;
for (size_t i = 0; i < 3; ++i) {
compact_scopes.emplace_back(std::map<std::string, std::string>{{"pt", scopes[i].first}},
scopes[i].second);
}
std::unique_ptr<FileStoreWrite> compactor;
ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> messages,
CompactAndPrepareCommit(compact_scopes, /*commit_identifier=*/0,
/*full_compaction=*/true, &compactor));
ASSERT_EQ(3, messages.size());
std::map<std::string, std::set<int64_t>> expected_dvs;
for (const auto& raw_message : messages) {
auto message = std::dynamic_pointer_cast<CommitMessageImpl>(raw_message);
ASSERT_NE(nullptr, message);
ASSERT_EQ(1, message->GetCompactIncrement().CompactAfter().size());
const std::string& name = message->GetCompactIncrement().CompactAfter()[0]->file_name;
// Resolve the scope through the bucket path of the corresponding compact-before file.
ASSERT_EQ(1, message->GetCompactIncrement().CompactBefore().size());
const std::string& before = message->GetCompactIncrement().CompactBefore()[0]->file_name;
bool found = false;
for (const FileState& file : baseline) {
if (file.file->file_name != before) {
continue;
}
for (size_t i = 0; i < 3; ++i) {
const std::string suffix =
fmt::format("pt={}/bucket-{}", scopes[i].first, scopes[i].second);
if (file.bucket_path == PathUtil::JoinPath(table_path_, suffix)) {
expected_dvs[name] = {static_cast<int64_t>(i)};
found = true;
}
}
}
ASSERT_TRUE(found);
}
// Every scope starts with offsets {0,1,2}; different deletions expose cross-scope leakage.
for (size_t i = 0; i < 3; ++i) {
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch({{static_cast<int64_t>(i), "ignored", scopes[i].first}},
/*partitioned=*/true, /*bucket=*/scopes[i].second,
/*row_kinds=*/{RecordBatch::RowKind::DELETE}));
ASSERT_OK(writer->Write(std::move(batch)));
}
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> deleted,
writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
ASSERT_EQ(3, deleted.size());
ASSERT_OK_AND_ASSIGN(snapshot, Commit(deleted, /*commit_identifier=*/2));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot));
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap offsets, ReadCommittedOffsets());
ASSERT_EQ(4, offsets.size());
for (const auto& [partition, bucket] : scopes) {
ASSERT_EQ(4, offsets.at(RealtimePartitionBucket(/*partition=*/{{"pt", partition}},
/*bucket=*/bucket)));
}
const std::multiset<Row> expected_rows = {
// p0/b0: id 0 was deleted.
{1, "p0-b0-one", "p0"},
{2, "p0-b0-two", "p0"},
// p0/b1: id 1 was deleted.
{0, "p0-b1-zero", "p0"},
{2, "p0-b1-two", "p0"},
// p1/b0: id 2 was deleted.
{0, "p1-b0-zero", "p1"},
{1, "p1-b0-one", "p1"},
// p1/b1: id 0 was already deleted; this bucket is not compacted.
{1, "p1-b1-one", "p1"},
{2, "p1-b1-two", "p1"},
};
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
ASSERT_OK_AND_ASSIGN(Snapshot compacted, CommitCompaction(messages, /*commit_identifier=*/0));
ASSERT_EQ(4, compacted.Id());
ASSERT_OK_AND_ASSIGN(std::vector<FileState> files, InspectFiles());
ASSERT_EQ(4, files.size());
size_t checked_outputs = 0;
bool checked_untouched = false;
for (const FileState& file : files) {
if (file.file->file_name == untouched_name) {
checked_untouched = true;
ASSERT_TRUE(file.deletion_file);
ASSERT_EQ(untouched_dv->path, file.deletion_file->path);
ASSERT_EQ(untouched_dv->offset, file.deletion_file->offset);
ASSERT_EQ((std::set<int64_t>{0}), file.deleted_positions);
} else {
ASSERT_EQ(1, expected_dvs.count(file.file->file_name));
ASSERT_EQ(expected_dvs.at(file.file->file_name), file.deleted_positions);
++checked_outputs;
}
}
ASSERT_TRUE(checked_untouched);
ASSERT_EQ(3, checked_outputs);
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap after, ReadCommittedOffsets());
ASSERT_EQ(offsets, after);
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
ASSERT_OK(writer->RefreshCommittedSnapshot(compacted.Id()));
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
ASSERT_OK(writer->Close());
ASSERT_OK(compactor->Close());
}
TEST_F(RealtimeWriteInteTest, TestNoHitPreservesBaselineDv) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK(WriteCommitAndRefresh({{0, "zero", "p0"}, {1, "one", "p0"}, {2, "two", "p0"}},
/*partitioned=*/false, /*bucket=*/0, /*row_kinds=*/{},
/*commit_identifier=*/0, writer.get()));
ASSERT_OK(WriteCommitAndRefresh({{0, "ignored", "p0"}}, /*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/{RecordBatch::RowKind::DELETE},
/*commit_identifier=*/1, writer.get()));
ASSERT_OK(CompactCommitAndRefresh(/*partition=*/{}, /*bucket=*/0, /*commit_identifier=*/2,
/*full_compaction=*/true, writer.get()));
// Offset 0 is now physically absent, while a different offset creates a nonempty baseline.
ASSERT_OK(WriteCommitAndRefresh({{1, "new-one", "p0"}}, /*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/{RecordBatch::RowKind::UPDATE_AFTER},
/*commit_identifier=*/3, writer.get()));
ASSERT_OK_AND_ASSIGN(std::vector<FileState> baseline, InspectFiles());
SnapshotManager snapshot_manager(dir_->GetFileSystem(), table_path_);
ASSERT_OK_AND_ASSIGN(std::optional<Snapshot> before, snapshot_manager.LatestSnapshot());
ASSERT_TRUE(before);
ASSERT_TRUE(before->IndexManifest());
std::set<std::string> indexes_before;
{
std::vector<BasicFileStatus> index_files;
ASSERT_OK(
dir_->GetFileSystem()->ListDir(PathUtil::JoinPath(table_path_, "index"), &index_files));
for (const auto& file : index_files) {
if (!file.IsDir()) {
indexes_before.insert(file.GetPath());
}
}
}
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch({{3, "three", "p0"}}, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/4));
ASSERT_EQ(1, progress.size());
// Replay a deletion whose offset has already been removed by compaction.
auto no_hit = std::make_shared<RoaringBitmap64>();
no_hit->Add(0);
progress[0].deleted_offsets = no_hit;
CommitContextBuilder builder(table_path_, commit_user_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> context,
builder.SetOptions(options_).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(context)));
ASSERT_OK_AND_ASSIGN(
int64_t snapshot,
commit->CommitWithProgress(progress, /*commit_identifier=*/4, /*watermark=*/std::nullopt));
ASSERT_EQ(5, snapshot);
ASSERT_OK_AND_ASSIGN(std::optional<Snapshot> after, snapshot_manager.LatestSnapshot());
ASSERT_TRUE(after);
ASSERT_EQ(before->IndexManifest(), after->IndexManifest());
std::set<std::string> indexes_after;
{
std::vector<BasicFileStatus> index_files;
ASSERT_OK(
dir_->GetFileSystem()->ListDir(PathUtil::JoinPath(table_path_, "index"), &index_files));
for (const auto& file : index_files) {
if (!file.IsDir()) {
indexes_after.insert(file.GetPath());
}
}
}
ASSERT_EQ(indexes_before, indexes_after);
ASSERT_OK_AND_ASSIGN(std::vector<FileState> files, InspectFiles());
size_t checked = 0;
for (const FileState& old : baseline) {
if (!old.deletion_file) {
continue;
}
for (const FileState& file : files) {
if (old.file->file_name == file.file->file_name) {
ASSERT_TRUE(file.deletion_file);
ASSERT_EQ(old.deletion_file->path, file.deletion_file->path);
ASSERT_EQ(old.deletion_file->offset, file.deletion_file->offset);
ASSERT_EQ(old.deletion_file->length, file.deletion_file->length);
ASSERT_EQ(old.deleted_positions, file.deleted_positions);
++checked;
}
}
}
ASSERT_EQ(1, checked);
const std::vector<Row> expected = {{2, "two", "p0"}, {1, "new-one", "p0"}, {3, "three", "p0"}};
const std::multiset<Row> expected_rows(expected.begin(), expected.end());
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot));
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestPublicationConflictCleansOnlyAttemptDv) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK(WriteCommitAndRefresh(MakeRows(/*first_id=*/0, /*count=*/5, /*partition=*/"p0"),
/*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/{}, /*commit_identifier=*/0, writer.get()));
ASSERT_OK(WriteCommitAndRefresh({{0, "ignored", "p0"}}, /*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/{RecordBatch::RowKind::DELETE},
/*commit_identifier=*/1, writer.get()));
std::unique_ptr<FileStoreWrite> compactor;
ASSERT_OK_AND_ASSIGN(
std::vector<std::shared_ptr<CommitMessage>> messages,
CompactAndPrepareCommit({RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)},
/*commit_identifier=*/0, /*full_compaction=*/true, &compactor));
ASSERT_EQ(1, messages.size());
auto message = std::dynamic_pointer_cast<CommitMessageImpl>(messages[0]);
ASSERT_NE(nullptr, message);
ASSERT_EQ(1, message->GetCompactIncrement().CompactAfter().size());
const auto& output = message->GetCompactIncrement().CompactAfter()[0];
ASSERT_EQ(4, output->row_count);
ASSERT_OK(WriteCommitAndRefresh({{1, "ignored", "p0"}}, /*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/{RecordBatch::RowKind::DELETE},
/*commit_identifier=*/2, writer.get()));
ASSERT_OK_AND_ASSIGN(std::vector<FileState> baseline, InspectFiles());
ASSERT_EQ(1, baseline.size());
ASSERT_TRUE(baseline[0].deletion_file);
std::set<std::string> indexes_before;
{
std::vector<BasicFileStatus> index_files;
ASSERT_OK(
dir_->GetFileSystem()->ListDir(PathUtil::JoinPath(table_path_, "index"), &index_files));
for (const auto& file : index_files) {
if (!file.IsDir()) {
indexes_before.insert(file.GetPath());
}
}
}
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch({{2, "ignored", "p0"}}, /*partitioned=*/false, /*bucket=*/0,
/*row_kinds=*/{RecordBatch::RowKind::DELETE}));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> concurrent,
writer->PrepareCommitWithProgress(/*commit_identifier=*/3));
std::set<std::string> failed_attempt_paths;
auto fs = std::make_shared<InterleavingSnapshotFileSystem>(
PathUtil::JoinPath(table_path_, "snapshot/snapshot-4"), [&]() -> Status {
// Capture candidates before the competing build writes any new DV of its own.
std::vector<BasicFileStatus> index_files;
PAIMON_RETURN_NOT_OK(dir_->GetFileSystem()->ListDir(
PathUtil::JoinPath(table_path_, "index"), &index_files));
std::set<std::string> during;
for (const auto& file : index_files) {
if (!file.IsDir()) {
during.insert(file.GetPath());
}
}
std::set_difference(during.begin(), during.end(), indexes_before.begin(),
indexes_before.end(),
std::inserter(failed_attempt_paths, failed_attempt_paths.end()));
PAIMON_ASSIGN_OR_RAISE(int64_t snapshot, Commit(concurrent, /*commit_identifier=*/3));
if (snapshot != 4) {
return Status::Invalid("expected the competing snapshot to occupy snapshot 4");
}
return Status::OK();
});
CommitContextBuilder builder(table_path_, "regression-compactor");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> context,
builder.SetOptions(options_).WithFileSystem(fs).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(context)));
ASSERT_OK(commit->Commit(messages, /*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(uint64_t attempts, commit->GetCommitMetrics()->GetCounter(
CommitMetrics::LAST_COMMIT_ATTEMPTS));
ASSERT_EQ(2, attempts);
ASSERT_EQ(1, failed_attempt_paths.size());
for (const std::string& path : failed_attempt_paths) {
ASSERT_OK_AND_ASSIGN(bool exists, dir_->GetFileSystem()->Exists(path));
ASSERT_FALSE(exists) << path;
}
// Cleanup owns only unpublished DVs, not baseline DVs, source data, or compact outputs.
for (const std::string& path : indexes_before) {
ASSERT_OK_AND_ASSIGN(bool exists, dir_->GetFileSystem()->Exists(path));
ASSERT_TRUE(exists) << path;
}
std::vector<std::shared_ptr<DataFileMeta>> retained = {baseline[0].file, output};
for (const auto& file : retained) {
const std::string bucket_path = PathUtil::JoinPath(table_path_, "bucket-0");
ASSERT_OK_AND_ASSIGN(bool exists, dir_->GetFileSystem()->Exists(
PathUtil::JoinPath(bucket_path, file->file_name)));
ASSERT_TRUE(exists);
for (const auto& extra : file->extra_files) {
ASSERT_TRUE(extra);
ASSERT_OK_AND_ASSIGN(
exists, dir_->GetFileSystem()->Exists(PathUtil::JoinPath(bucket_path, *extra)));
ASSERT_TRUE(exists);
}
}
ASSERT_OK_AND_ASSIGN(std::vector<FileState> files, InspectFiles());
ASSERT_EQ(1, files.size());
ASSERT_EQ(output->file_name, files[0].file->file_name);
ASSERT_TRUE(files[0].deletion_file);
ASSERT_EQ(0, failed_attempt_paths.count(files[0].deletion_file->path));
ASSERT_EQ((std::set<int64_t>{0, 1}), files[0].deleted_positions);
ASSERT_OK_AND_ASSIGN(bool exists, dir_->GetFileSystem()->Exists(files[0].deletion_file->path));
ASSERT_TRUE(exists);
const std::vector<Row> expected = MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0");
const std::multiset<Row> expected_rows(expected.begin(), expected.end());
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
SnapshotManager snapshot_manager(dir_->GetFileSystem(), table_path_);
ASSERT_OK_AND_ASSIGN(std::optional<Snapshot> published, snapshot_manager.LatestSnapshot());
ASSERT_TRUE(published);
ASSERT_EQ(5, published->Id());
ASSERT_OK(writer->RefreshCommittedSnapshot(published->Id()));
{
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, std::multiset<Row>(disk_rows.begin(), disk_rows.end()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, std::multiset<Row>(realtime_rows.begin(), realtime_rows.end()));
}
ASSERT_OK(writer->Close());
ASSERT_OK(compactor->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateWriterHandoffRestoresCommittedFiles) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> first_writer,
CreateRealtimeWriter(realtime_context));
const std::vector<Row> initial_rows = {{1, "old-one", "p0"}, {2, "two", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> initial_batch,
MakeBatch(initial_rows, /*partitioned=*/false));
ASSERT_OK(first_writer->Write(std::move(initial_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> initial_progress,
first_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(initial_progress, /*commit_identifier=*/0));
ASSERT_EQ(1, snapshot_id);
ASSERT_OK(first_writer->Close());
first_writer.reset();
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> second_writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::vector<Row> restored_rows, ReadRows(realtime_context));
ASSERT_EQ(initial_rows, restored_rows);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> update_batch,
MakeBatch({{1, "new-one", "p0"}}, /*partitioned=*/false));
ASSERT_OK(second_writer->Write(std::move(update_batch)));
const std::vector<Row> expected_rows = {{2, "two", "p0"}, {1, "new-one", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> updated_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, updated_rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> update_progress,
second_writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, update_progress.size());
std::shared_ptr<CommitMessageImpl> update_message =
std::dynamic_pointer_cast<CommitMessageImpl>(update_progress[0].commit_message);
ASSERT_NE(nullptr, update_message);
ASSERT_TRUE(update_message->GetCompactIncrement().NewIndexFiles().empty());
ASSERT_NE(nullptr, update_progress[0].deleted_offsets);
ASSERT_FALSE(update_progress[0].deleted_offsets->IsEmpty());
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_prepare, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_after_prepare);
ASSERT_OK(second_writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateWriterHandoffRetainsPreparedFiles) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> first_writer,
CreateRealtimeWriter(realtime_context));
const std::vector<Row> initial_rows = {
{1, "old-one", "p0"}, {2, "keep-two", "p0"}, {3, "delete-three", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> initial_batch,
MakeBatch(initial_rows, /*partitioned=*/false));
ASSERT_OK(first_writer->Write(std::move(initial_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
first_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, progress.size());
ASSERT_EQ(OffsetRange(0, 3), progress[0].offset_range);
std::shared_ptr<CommitMessageImpl> initial_message =
std::dynamic_pointer_cast<CommitMessageImpl>(progress[0].commit_message);
ASSERT_NE(nullptr, initial_message);
ASSERT_EQ(1, initial_message->GetNewFilesIncrement().NewFiles().size());
ASSERT_TRUE(initial_message->GetCompactIncrement().NewIndexFiles().empty());
ASSERT_OK(first_writer->Close());
first_writer.reset();
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> second_writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::vector<Row> restored_rows, ReadRows(realtime_context));
ASSERT_EQ(initial_rows, restored_rows);
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> update_batch,
MakeBatch({Row{1, "new-one", "p0"}, Row{3, "ignored", "p0"}, Row{4, "four", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER, RecordBatch::RowKind::DELETE,
RecordBatch::RowKind::INSERT}));
ASSERT_OK(second_writer->Write(std::move(update_batch)));
const std::vector<Row> expected_rows = {
{2, "keep-two", "p0"}, {1, "new-one", "p0"}, {4, "four", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_prepare, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_before_prepare);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> update_progress,
second_writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, update_progress.size());
ASSERT_EQ(OffsetRange(3, 6), update_progress[0].offset_range);
std::shared_ptr<CommitMessageImpl> update_message =
std::dynamic_pointer_cast<CommitMessageImpl>(update_progress[0].commit_message);
ASSERT_NE(nullptr, update_message);
ASSERT_EQ(1, update_message->GetNewFilesIncrement().NewFiles().size());
ASSERT_EQ(2, update_message->GetNewFilesIncrement().NewFiles()[0]->row_count);
ASSERT_TRUE(update_message->GetCompactIncrement().NewIndexFiles().empty());
ASSERT_NE(nullptr, update_progress[0].deleted_offsets);
ASSERT_FALSE(update_progress[0].deleted_offsets->IsEmpty());
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_prepare, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_after_prepare);
progress.push_back(std::move(update_progress[0]));
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, /*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_before_refresh);
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, disk_rows);
ASSERT_OK(second_writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_after_refresh);
ASSERT_OK(second_writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateWriterHandoffRetainsDvAcrossRefresh) {
for (bool prepare_before_refresh : {false, true}) {
SCOPED_TRACE(prepare_before_refresh);
dir_ = UniqueTestDirectory::Create("local");
ASSERT_NE(nullptr, dir_);
table_path_ = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
ResetExternalOffset(/*partition=*/{}, /*bucket=*/0, /*next_offset=*/0);
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> first_writer,
CreateRealtimeWriter(realtime_context));
const std::vector<Row> initial_rows = {
{1, "one", "p0"}, {2, "two", "p0"}, {3, "three", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> initial_batch,
MakeBatch(initial_rows, /*partitioned=*/false));
ASSERT_OK(first_writer->Write(std::move(initial_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> initial_progress,
first_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, initial_progress.size());
auto initial_message =
std::dynamic_pointer_cast<CommitMessageImpl>(initial_progress[0].commit_message);
ASSERT_NE(nullptr, initial_message);
ASSERT_EQ(1, initial_message->GetNewFilesIncrement().NewFiles().size());
ASSERT_OK_AND_ASSIGN(int64_t initial_snapshot_id,
Commit(initial_progress, /*commit_identifier=*/0));
ASSERT_OK(first_writer->RefreshCommittedSnapshot(initial_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_delete,
MakeBatch({initial_rows[0]}, /*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::DELETE}));
ASSERT_OK(first_writer->Write(std::move(first_delete)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_progress,
first_writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_OK(first_writer->Close());
first_writer.reset();
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> second_writer,
CreateRealtimeWriter(realtime_context));
const Row fourth_row = {4, "four", "p0"};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> insert_batch,
MakeBatch({fourth_row}, /*partitioned=*/false));
// Create the bucket writer before the first writer's deletion reaches the snapshot.
ASSERT_OK(second_writer->Write(std::move(insert_batch)));
std::vector<RealtimeCommitProgress> second_progress;
if (prepare_before_refresh) {
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_delete,
MakeBatch({initial_rows[1]}, /*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::DELETE}));
ASSERT_OK(second_writer->Write(std::move(second_delete)));
ASSERT_OK_AND_ASSIGN(second_progress,
second_writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
}
ASSERT_OK_AND_ASSIGN(int64_t first_snapshot_id,
Commit(first_progress, /*commit_identifier=*/1));
ASSERT_OK(second_writer->RefreshCommittedSnapshot(first_snapshot_id));
const std::vector<Row> rows_after_refresh =
prepare_before_refresh ? std::vector<Row>{initial_rows[2], fourth_row}
: std::vector<Row>{initial_rows[1], initial_rows[2], fourth_row};
ASSERT_OK_AND_ASSIGN(std::vector<Row> refreshed_rows, ReadRows(realtime_context));
ASSERT_EQ(rows_after_refresh, refreshed_rows);
if (!prepare_before_refresh) {
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_delete,
MakeBatch({initial_rows[1]}, /*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::DELETE}));
ASSERT_OK(second_writer->Write(std::move(second_delete)));
ASSERT_OK_AND_ASSIGN(second_progress,
second_writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
}
const std::vector<Row> expected_rows = {initial_rows[2], fourth_row};
ASSERT_OK_AND_ASSIGN(std::vector<Row> prepared_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, prepared_rows);
ASSERT_OK(Commit(second_progress, /*commit_identifier=*/2));
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, disk_rows);
// Another prepare must preserve deletions from the message prepared before refresh,
// even though its commit has not yet been refreshed into this context.
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> third_delete,
MakeBatch({initial_rows[2]}, /*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::DELETE}));
ASSERT_OK(second_writer->Write(std::move(third_delete)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> third_progress,
second_writer->PrepareCommitWithProgress(/*commit_identifier=*/3));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_commit, ReadRows(realtime_context));
ASSERT_EQ(std::vector<Row>{fourth_row}, rows_before_commit);
ASSERT_OK_AND_ASSIGN(int64_t final_snapshot_id,
Commit(third_progress, /*commit_identifier=*/3));
ASSERT_OK_AND_ASSIGN(disk_rows, ReadRows());
ASSERT_EQ(std::vector<Row>{fourth_row}, disk_rows);
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_refresh, ReadRows(realtime_context));
ASSERT_EQ(std::vector<Row>{fourth_row}, rows_before_refresh);
ASSERT_OK(second_writer->RefreshCommittedSnapshot(final_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> final_rows, ReadRows(realtime_context));
ASSERT_EQ(std::vector<Row>{fourth_row}, final_rows);
ASSERT_OK(second_writer->Close());
}
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateWithinOneBatch) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> batch,
MakeBatch({Row{1, "old-one", "p0"}, Row{2, "two", "p0"}, Row{1, "new-one", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
const std::vector<Row> expected_rows = {{2, "two", "p0"}, {1, "new-one", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, realtime_rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, progress.size());
ASSERT_EQ(OffsetRange(0, 3), progress[0].offset_range);
std::shared_ptr<CommitMessageImpl> message =
std::dynamic_pointer_cast<CommitMessageImpl>(progress[0].commit_message);
ASSERT_NE(nullptr, message);
ASSERT_EQ(1, message->GetNewFilesIncrement().NewFiles().size());
ASSERT_EQ(2, message->GetNewFilesIncrement().NewFiles()[0]->row_count);
ASSERT_TRUE(message->GetCompactIncrement().NewIndexFiles().empty());
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK(writer->Close());
writer.reset();
realtime_context.reset();
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, disk_rows);
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateAcrossSealedSegments) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch({Row{1, "old-one", "p0"}, Row{2, "two", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(first_batch)));
ASSERT_OK(writer->Seal());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch({Row{1, "new-one", "p0"}, Row{3, "three", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(second_batch)));
const std::vector<Row> expected_rows = {
{2, "two", "p0"}, {1, "new-one", "p0"}, {3, "three", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_prepare, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_before_prepare);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, progress.size());
ASSERT_EQ(OffsetRange(0, 4), progress[0].offset_range);
std::shared_ptr<CommitMessageImpl> message =
std::dynamic_pointer_cast<CommitMessageImpl>(progress[0].commit_message);
ASSERT_NE(nullptr, message);
ASSERT_EQ(2, message->GetNewFilesIncrement().NewFiles().size());
ASSERT_TRUE(message->GetCompactIncrement().NewIndexFiles().empty());
ASSERT_NE(nullptr, progress[0].deleted_offsets);
ASSERT_FALSE(progress[0].deleted_offsets->IsEmpty());
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_prepare, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_after_prepare);
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_after_refresh);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateAcrossPreparedSegments) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch({Row{1, "old-one", "p0"}, Row{2, "two", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, progress.size());
ASSERT_EQ(OffsetRange(0, 2), progress[0].offset_range);
ASSERT_NE(nullptr, progress[0].deleted_offsets);
ASSERT_TRUE(progress[0].deleted_offsets->IsEmpty());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch({Row{1, "new-one", "p0"}, Row{3, "three", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(second_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, second_progress.size());
ASSERT_EQ(OffsetRange(2, 4), second_progress[0].offset_range);
ASSERT_NE(nullptr, second_progress[0].deleted_offsets);
ASSERT_EQ("{0}", second_progress[0].deleted_offsets->ToString());
ASSERT_TRUE(progress[0].deleted_offsets->IsEmpty());
std::shared_ptr<CommitMessageImpl> second_message =
std::dynamic_pointer_cast<CommitMessageImpl>(second_progress[0].commit_message);
ASSERT_NE(nullptr, second_message);
ASSERT_EQ(1, second_message->GetNewFilesIncrement().NewFiles().size());
ASSERT_TRUE(second_message->GetCompactIncrement().NewIndexFiles().empty());
progress.push_back(std::move(second_progress[0]));
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, /*commit_identifier=*/1));
const std::vector<Row> expected_rows = {
{2, "two", "p0"}, {1, "new-one", "p0"}, {3, "three", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_before_refresh);
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_after_refresh);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateDeletesCommittedKey) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> insert_batch,
MakeBatch({Row{1, "one", "p0"}, Row{2, "two", "p0"}}, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(insert_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> insert_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t insert_snapshot_id,
Commit(insert_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(insert_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> delete_batch,
MakeBatch({Row{1, "ignored", "p0"}}, /*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::DELETE}));
ASSERT_OK(writer->Write(std::move(delete_batch)));
const std::vector<Row> expected_rows = {{2, "two", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, realtime_rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> delete_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, delete_progress.size());
ASSERT_EQ(OffsetRange(2, 3), delete_progress[0].offset_range);
std::shared_ptr<CommitMessageImpl> delete_message =
std::dynamic_pointer_cast<CommitMessageImpl>(delete_progress[0].commit_message);
ASSERT_NE(nullptr, delete_message);
ASSERT_TRUE(delete_message->GetNewFilesIncrement().NewFiles().empty());
ASSERT_TRUE(delete_message->GetCompactIncrement().NewIndexFiles().empty());
ASSERT_NE(nullptr, delete_progress[0].deleted_offsets);
ASSERT_FALSE(delete_progress[0].deleted_offsets->IsEmpty());
ASSERT_OK_AND_ASSIGN(int64_t delete_snapshot_id,
Commit(delete_progress, /*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(delete_snapshot_id));
ASSERT_OK(writer->Close());
writer.reset();
realtime_context.reset();
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, disk_rows);
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateInsertThenDeleteInOneSegment) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch({Row{1, "one", "p0"}, Row{1, "ignored", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::INSERT, RecordBatch::RowKind::DELETE}));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_TRUE(realtime_rows.empty());
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, progress.size());
ASSERT_EQ(OffsetRange(0, 2), progress[0].offset_range);
std::shared_ptr<CommitMessageImpl> message =
std::dynamic_pointer_cast<CommitMessageImpl>(progress[0].commit_message);
ASSERT_NE(nullptr, message);
ASSERT_TRUE(message->IsEmpty());
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap committed_offsets, ReadCommittedOffsets());
ASSERT_EQ(2, committed_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_refresh, ReadRows(realtime_context));
ASSERT_TRUE(rows_after_refresh.empty());
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateUpdateBeforeAndAfter) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> insert_batch,
MakeBatch({Row{1, "old-one", "p0"}, Row{2, "two", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(insert_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> insert_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t insert_snapshot_id,
Commit(insert_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(insert_snapshot_id));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> update_batch,
MakeBatch({Row{1, "ignored-before", "p0"}, Row{1, "new-one", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_BEFORE, RecordBatch::RowKind::UPDATE_AFTER}));
ASSERT_OK(writer->Write(std::move(update_batch)));
const std::vector<Row> expected_rows = {{2, "two", "p0"}, {1, "new-one", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, realtime_rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> update_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, update_progress.size());
ASSERT_EQ(OffsetRange(2, 4), update_progress[0].offset_range);
std::shared_ptr<CommitMessageImpl> update_message =
std::dynamic_pointer_cast<CommitMessageImpl>(update_progress[0].commit_message);
ASSERT_NE(nullptr, update_message);
ASSERT_EQ(1, update_message->GetNewFilesIncrement().NewFiles().size());
ASSERT_TRUE(update_message->GetCompactIncrement().NewIndexFiles().empty());
ASSERT_NE(nullptr, update_progress[0].deleted_offsets);
ASSERT_FALSE(update_progress[0].deleted_offsets->IsEmpty());
ASSERT_OK_AND_ASSIGN(int64_t update_snapshot_id,
Commit(update_progress, /*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(update_snapshot_id));
ASSERT_OK(writer->Close());
writer.reset();
realtime_context.reset();
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, disk_rows);
}
class RealtimeDeduplicateExactIndexInteTest : public RealtimeWriteInteTest,
public ::testing::WithParamInterface<std::string> {};
TEST_P(RealtimeDeduplicateExactIndexInteTest,
TestRealtimeDeduplicateAcrossMultipleFileIndexStorageForms) {
options_[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1MB";
ConfigureRealtimeDeduplicateTable({}, GetParam());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> embedded_writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> embedded_batch,
MakeBatch({Row{1, "old-one", "p0"}, Row{2, "two", "p0"}},
/*partitioned=*/false));
ASSERT_OK(embedded_writer->Write(std::move(embedded_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> embedded_progress,
embedded_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
std::vector<std::shared_ptr<DataFileMeta>> embedded_files = NewFiles(embedded_progress);
ASSERT_EQ(1, embedded_files.size());
ASSERT_NE(nullptr, embedded_files[0]->embedded_index);
// The only extra file is the deduplicate key-offset sidecar.
ASSERT_EQ(1, embedded_files[0]->extra_files.size());
ASSERT_OK_AND_ASSIGN(int64_t embedded_snapshot_id,
Commit(embedded_progress, /*commit_identifier=*/0));
ASSERT_OK(embedded_writer->RefreshCommittedSnapshot(embedded_snapshot_id));
ASSERT_OK(embedded_writer->Close());
options_[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1B";
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> external_writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> external_batch,
MakeBatch({Row{3, "old-three", "p0"}, Row{4, "four", "p0"}},
/*partitioned=*/false));
ASSERT_OK(external_writer->Write(std::move(external_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> external_progress,
external_writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
std::vector<std::shared_ptr<DataFileMeta>> external_files = NewFiles(external_progress);
ASSERT_EQ(1, external_files.size());
ASSERT_EQ(nullptr, external_files[0]->embedded_index);
// The external file index precedes the deduplicate key-offset sidecar.
ASSERT_EQ(2, external_files[0]->extra_files.size());
ASSERT_OK_AND_ASSIGN(int64_t external_snapshot_id,
Commit(external_progress, /*commit_identifier=*/1));
ASSERT_OK(external_writer->RefreshCommittedSnapshot(external_snapshot_id));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> update_batch,
MakeBatch({Row{1, "new-one", "p0"}, Row{3, "new-three", "p0"}, Row{5, "five", "p0"}},
/*partitioned=*/false));
ASSERT_OK(external_writer->Write(std::move(update_batch)));
const std::vector<Row> expected_rows = {{2, "two", "p0"},
{4, "four", "p0"},
{1, "new-one", "p0"},
{3, "new-three", "p0"},
{5, "five", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_prepare, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_before_prepare);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> update_progress,
external_writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
ASSERT_EQ(1, update_progress.size());
std::shared_ptr<CommitMessageImpl> update_message =
std::dynamic_pointer_cast<CommitMessageImpl>(update_progress[0].commit_message);
ASSERT_NE(nullptr, update_message);
ASSERT_EQ(1, update_message->GetNewFilesIncrement().NewFiles().size());
ASSERT_TRUE(update_message->GetCompactIncrement().NewIndexFiles().empty());
ASSERT_NE(nullptr, update_progress[0].deleted_offsets);
ASSERT_FALSE(update_progress[0].deleted_offsets->IsEmpty());
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_prepare, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_after_prepare);
ASSERT_OK_AND_ASSIGN(int64_t update_snapshot_id,
Commit(update_progress, /*commit_identifier=*/2));
ASSERT_OK(external_writer->RefreshCommittedSnapshot(update_snapshot_id));
ASSERT_OK(external_writer->Close());
external_writer.reset();
realtime_context.reset();
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(expected_rows, disk_rows);
}
INSTANTIATE_TEST_SUITE_P(ExactIndex, RealtimeDeduplicateExactIndexInteTest,
::testing::Values("bitmap", "bsi", "range-bitmap"));
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateReadViewIsStableAcrossWriteAndRefresh) {
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> base_batch,
MakeBatch({Row{1, "old-one", "p0"}, Row{2, "old-two", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(base_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> base_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t base_snapshot_id, Commit(base_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(base_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_update,
MakeBatch({Row{1, "new-one", "p0"}, Row{3, "three", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(first_update)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> old_plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> old_plan_across_refresh,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_update,
MakeBatch({Row{2, "new-two", "p0"}, Row{4, "four", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(second_update)));
const std::vector<Row> old_expected_rows = {
{2, "old-two", "p0"}, {1, "new-one", "p0"}, {3, "three", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> old_plan_rows, ReadRows(old_plan, realtime_context));
ASSERT_EQ(old_expected_rows, old_plan_rows);
const std::vector<Row> latest_expected_rows = {
{1, "new-one", "p0"}, {3, "three", "p0"}, {2, "new-two", "p0"}, {4, "four", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> latest_rows_before_prepare, ReadRows(realtime_context));
ASSERT_EQ(latest_expected_rows, latest_rows_before_prepare);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, /*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> pinned_rows_after_refresh,
ReadRows(old_plan_across_refresh, realtime_context));
ASSERT_EQ(old_expected_rows, pinned_rows_after_refresh);
ASSERT_OK_AND_ASSIGN(std::vector<Row> latest_rows_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(latest_expected_rows, latest_rows_after_refresh);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeDeduplicateIsolatesPartitionsAndBuckets) {
options_[Options::BUCKET] = "2";
ConfigureRealtimeDeduplicateTable(/*partition_keys=*/{"pt"});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> p0b0_batch,
MakeBatch({Row{1, "p0-b0-old", "p0"}, Row{2, "p0-b0-keep", "p0"}},
/*partitioned=*/true, /*bucket=*/0));
ASSERT_OK(writer->Write(std::move(p0b0_batch)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> p0b1_batch,
MakeBatch({Row{1, "p0-b1-old", "p0"}, Row{3, "p0-b1-keep", "p0"}},
/*partitioned=*/true, /*bucket=*/1));
ASSERT_OK(writer->Write(std::move(p0b1_batch)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> p1b0_batch,
MakeBatch({Row{1, "p1-b0-old", "p1"}, Row{4, "p1-b0-keep", "p1"}},
/*partitioned=*/true, /*bucket=*/0));
ASSERT_OK(writer->Write(std::move(p1b0_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> base_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(3, base_progress.size());
ASSERT_OK_AND_ASSIGN(int64_t base_snapshot_id, Commit(base_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(base_snapshot_id));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> update_batch,
MakeBatch({Row{1, "p0-b0-new", "p0"}}, /*partitioned=*/true, /*bucket=*/0));
ASSERT_OK(writer->Write(std::move(update_batch)));
const std::vector<Row> expected_rows = {{2, "p0-b0-keep", "p0"}, {1, "p0-b0-new", "p0"},
{1, "p0-b1-old", "p0"}, {3, "p0-b1-keep", "p0"},
{1, "p1-b0-old", "p1"}, {4, "p1-b0-keep", "p1"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_prepare, ReadRows(realtime_context));
ASSERT_EQ(std::multiset<Row>(expected_rows.begin(), expected_rows.end()),
std::multiset<Row>(rows_before_prepare.begin(), rows_before_prepare.end()));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> update_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, update_progress.size());
ASSERT_EQ(RealtimePartitionBucket({{"pt", "p0"}}, /*bucket=*/0),
update_progress[0].partition_bucket);
ASSERT_EQ(OffsetRange(2, 3), update_progress[0].offset_range);
std::shared_ptr<CommitMessageImpl> update_message =
std::dynamic_pointer_cast<CommitMessageImpl>(update_progress[0].commit_message);
ASSERT_NE(nullptr, update_message);
ASSERT_TRUE(update_message->GetCompactIncrement().NewIndexFiles().empty());
ASSERT_NE(nullptr, update_progress[0].deleted_offsets);
ASSERT_FALSE(update_progress[0].deleted_offsets->IsEmpty());
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_prepare, ReadRows(realtime_context));
ASSERT_EQ(std::multiset<Row>(expected_rows.begin(), expected_rows.end()),
std::multiset<Row>(rows_after_prepare.begin(), rows_after_prepare.end()));
ASSERT_OK_AND_ASSIGN(int64_t update_snapshot_id,
Commit(update_progress, /*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_refresh, ReadRows(realtime_context));
ASSERT_EQ(std::multiset<Row>(expected_rows.begin(), expected_rows.end()),
std::multiset<Row>(rows_before_refresh.begin(), rows_before_refresh.end()));
ASSERT_OK(writer->RefreshCommittedSnapshot(update_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(std::multiset<Row>(expected_rows.begin(), expected_rows.end()),
std::multiset<Row>(rows_after_refresh.begin(), rows_after_refresh.end()));
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap committed_offsets, ReadCommittedOffsets());
ASSERT_EQ(3, committed_offsets.size());
ASSERT_EQ(3, committed_offsets.at(RealtimePartitionBucket({{"pt", "p0"}}, /*bucket=*/0)));
ASSERT_EQ(2, committed_offsets.at(RealtimePartitionBucket({{"pt", "p0"}}, /*bucket=*/1)));
ASSERT_EQ(2, committed_offsets.at(RealtimePartitionBucket({{"pt", "p1"}}, /*bucket=*/0)));
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest,
TestRealtimeDeduplicatePredicatePushdownAcrossDataFileSealedAndBuilding) {
options_[Options::WRITE_BATCH_SIZE] = "1";
options_[Options::REALTIME_STORE_STATS_MODE] = "full";
options_["parquet.page.size"] = "1";
options_["parquet.enable.dictionary"] = "false";
options_["parquet.write.enable-page-index"] = "true";
options_["parquet.write.max-row-group-length"] = "1";
options_["parquet.read.enable-page-index-filter"] = "true";
ConfigureRealtimeDeduplicateTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
auto write_one = [&](const Row& row) -> Status {
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<RecordBatch> batch,
MakeBatch({row}, /*partitioned=*/false));
return writer->Write(std::move(batch));
};
ASSERT_OK(write_one({1, "match", "p0"}));
ASSERT_OK(write_one({2, "data-file-other", "p0"}));
ASSERT_OK(write_one({7, "match", "p0"}));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> data_file_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t data_file_snapshot_id,
Commit(data_file_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(data_file_snapshot_id));
ASSERT_OK(write_one({3, "match", "p0"}));
ASSERT_OK(write_one({4, "sealed-other", "p0"}));
ASSERT_OK(write_one({7, "replacement-other", "p0"}));
ASSERT_OK(writer->Seal());
ASSERT_OK(write_one({5, "match", "p0"}));
ASSERT_OK(write_one({6, "building-other", "p0"}));
const std::string matching_payload = "match";
std::shared_ptr<Predicate> scan_predicate = PredicateBuilder::Equal(
/*field_index=*/2, /*field_name=*/"payload", FieldType::STRING,
Literal(FieldType::STRING, matching_payload.data(), matching_payload.size()));
std::shared_ptr<Predicate> read_predicate = PredicateBuilder::Equal(
/*field_index=*/1, /*field_name=*/"payload", FieldType::STRING,
Literal(FieldType::STRING, matching_payload.data(), matching_payload.size()));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan, CreatePlan(realtime_context, scan_predicate));
// Disable residual filtering so the result proves that the predicate reached the data file
// reader and both the sealed and building in-memory segments.
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, ReadRows(plan, realtime_context, read_predicate,
/*enable_predicate_filter=*/false));
ASSERT_EQ((std::vector<Row>{{1, "match", "p0"}, {3, "match", "p0"}, {5, "match", "p0"}}), rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> final_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, final_progress.size());
std::shared_ptr<CommitMessageImpl> final_message =
std::dynamic_pointer_cast<CommitMessageImpl>(final_progress[0].commit_message);
ASSERT_NE(nullptr, final_message);
ASSERT_TRUE(final_message->GetCompactIncrement().NewIndexFiles().empty());
ASSERT_NE(nullptr, final_progress[0].deleted_offsets);
ASSERT_FALSE(final_progress[0].deleted_offsets->IsEmpty());
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeReadBatchSizeSplitsLargeAppendBatch) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
constexpr int64_t kRowCount = 1000;
const std::vector<Row> expected_rows = MakeRows(/*first_id=*/0, kRowCount, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(expected_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
for (int32_t read_batch_size : {1, 17, 128, 2048}) {
options_[Options::READ_BATCH_SIZE] = std::to_string(read_batch_size);
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, actual_rows);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<arrow::ChunkedArray> result,
ReadPlan(plan, realtime_context, {"id", "payload", "pt"}, /*predicate=*/nullptr,
/*enable_predicate_filter=*/false));
ASSERT_NE(nullptr, result);
ASSERT_EQ(kRowCount, result->length());
ASSERT_EQ((kRowCount + read_batch_size - 1) / read_batch_size, result->num_chunks());
for (const std::shared_ptr<arrow::Array>& chunk : result->chunks()) {
ASSERT_GT(chunk->length(), 0);
ASSERT_LE(chunk->length(), read_batch_size);
}
}
FinalizeCommitAndCheck(writer.get(), /*realtime_commits=*/{}, /*prepare_identifier=*/0,
expected_rows);
}
TEST_F(RealtimeWriteInteTest, TestRealtimeMetricsTrackBuildingSealedAndCommittedData) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch(MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0"),
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues building_metrics,
ReadRealtimeMetrics(writer->GetMetrics()));
ASSERT_GT(building_metrics.building_memory, 0);
ASSERT_EQ(0, building_metrics.sealed_memory);
ASSERT_EQ(building_metrics.building_memory, building_metrics.total_memory);
ASSERT_EQ(3, building_metrics.building_rows);
ASSERT_EQ(0, building_metrics.sealed_rows);
ASSERT_EQ(3, building_metrics.total_rows);
ASSERT_OK(writer->Seal());
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues sealed_metrics,
ReadRealtimeMetrics(realtime_context->GetMetrics()));
ASSERT_EQ(0, sealed_metrics.building_memory);
ASSERT_EQ(building_metrics.total_memory, sealed_metrics.sealed_memory);
ASSERT_EQ(sealed_metrics.sealed_memory, sealed_metrics.total_memory);
ASSERT_EQ(0, sealed_metrics.building_rows);
ASSERT_EQ(3, sealed_metrics.sealed_rows);
ASSERT_EQ(3, sealed_metrics.total_rows);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0"),
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(second_batch)));
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues mixed_metrics,
ReadRealtimeMetrics(realtime_context->GetMetrics()));
ASSERT_GT(mixed_metrics.building_memory, 0);
ASSERT_EQ(sealed_metrics.sealed_memory, mixed_metrics.sealed_memory);
ASSERT_EQ(mixed_metrics.building_memory + mixed_metrics.sealed_memory,
mixed_metrics.total_memory);
ASSERT_EQ(2, mixed_metrics.building_rows);
ASSERT_EQ(3, mixed_metrics.sealed_rows);
ASSERT_EQ(5, mixed_metrics.total_rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, commits.size());
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues prepared_metrics,
ReadRealtimeMetrics(writer->GetMetrics()));
ASSERT_EQ(0, prepared_metrics.building_memory);
ASSERT_EQ(mixed_metrics.total_memory, prepared_metrics.sealed_memory);
ASSERT_EQ(prepared_metrics.sealed_memory, prepared_metrics.total_memory);
ASSERT_EQ(0, prepared_metrics.building_rows);
ASSERT_EQ(5, prepared_metrics.sealed_rows);
ASSERT_EQ(5, prepared_metrics.total_rows);
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(commits, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues reclaimed_metrics,
ReadRealtimeMetrics(realtime_context->GetMetrics()));
ASSERT_EQ(0, reclaimed_metrics.building_memory);
ASSERT_EQ(0, reclaimed_metrics.sealed_memory);
ASSERT_EQ(0, reclaimed_metrics.total_memory);
ASSERT_EQ(0, reclaimed_metrics.building_rows);
ASSERT_EQ(0, reclaimed_metrics.sealed_rows);
ASSERT_EQ(0, reclaimed_metrics.total_rows);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeCommitRetriesSnapshotConflict) {
options_[Options::BUCKET] = "2";
options_[Options::COMMIT_MAX_RETRIES] = "1";
options_[Options::COMMIT_MIN_RETRY_WAIT] = "1ms";
options_[Options::COMMIT_MAX_RETRY_WAIT] = "1ms";
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> bucket0_writer, CreateRealtimeWriter());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> bucket0_batch,
MakeBatch(MakeRows(/*first_id=*/0, /*count=*/2, /*partition=*/"p0"),
/*partitioned=*/false, /*bucket=*/0));
ASSERT_OK(bucket0_writer->Write(std::move(bucket0_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> bucket0_commits,
bucket0_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, bucket0_commits.size());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> bucket1_writer, CreateRealtimeWriter());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> bucket1_batch,
MakeBatch(MakeRows(/*first_id=*/10, /*count=*/3, /*partition=*/"p0"),
/*partitioned=*/false, /*bucket=*/1));
ASSERT_OK(bucket1_writer->Write(std::move(bucket1_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> bucket1_commits,
bucket1_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, bucket1_commits.size());
const std::string first_snapshot_path = PathUtil::JoinPath(table_path_, "snapshot/snapshot-1");
auto conflict_file_system = std::make_shared<SnapshotConflictFileSystem>(first_snapshot_path);
CommitContextBuilder bucket0_builder(table_path_, "bucket0_realtime_commit_user");
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<CommitContext> bucket0_context,
bucket0_builder.SetOptions(options_).WithFileSystem(conflict_file_system).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> bucket0_commit,
FileStoreCommit::Create(std::move(bucket0_context)));
CommitContextBuilder bucket1_builder(table_path_, "bucket1_realtime_commit_user");
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<CommitContext> bucket1_context,
bucket1_builder.SetOptions(options_).WithFileSystem(conflict_file_system).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> bucket1_commit,
FileStoreCommit::Create(std::move(bucket1_context)));
std::optional<Result<int64_t>> bucket0_result;
std::optional<Result<int64_t>> bucket1_result;
std::thread bucket0_thread([&]() {
bucket0_result.emplace(bucket0_commit->CommitWithProgress(
bucket0_commits, /*commit_identifier=*/0, /*watermark=*/std::nullopt));
});
std::thread bucket1_thread([&]() {
bucket1_result.emplace(bucket1_commit->CommitWithProgress(
bucket1_commits, /*commit_identifier=*/0, /*watermark=*/std::nullopt));
});
bucket0_thread.join();
bucket1_thread.join();
ASSERT_TRUE(bucket0_result.has_value());
ASSERT_TRUE(bucket1_result.has_value());
ASSERT_OK_AND_ASSIGN(int64_t bucket0_snapshot_id, std::move(bucket0_result.value()));
ASSERT_OK_AND_ASSIGN(int64_t bucket1_snapshot_id, std::move(bucket1_result.value()));
std::vector<int64_t> snapshot_ids = {bucket0_snapshot_id, bucket1_snapshot_id};
std::sort(snapshot_ids.begin(), snapshot_ids.end());
ASSERT_EQ((std::vector<int64_t>{1, 2}), snapshot_ids);
ASSERT_EQ(2, conflict_file_system->ConflictSnapshotAttempts());
ASSERT_OK_AND_ASSIGN(uint64_t bucket0_attempts, bucket0_commit->GetCommitMetrics()->GetCounter(
CommitMetrics::LAST_COMMIT_ATTEMPTS));
ASSERT_OK_AND_ASSIGN(uint64_t bucket1_attempts, bucket1_commit->GetCommitMetrics()->GetCounter(
CommitMetrics::LAST_COMMIT_ATTEMPTS));
std::vector<uint64_t> attempts = {bucket0_attempts, bucket1_attempts};
std::sort(attempts.begin(), attempts.end());
ASSERT_EQ((std::vector<uint64_t>{1, 2}), attempts);
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap committed_offsets, ReadCommittedOffsets());
ASSERT_EQ(2, committed_offsets.size());
ASSERT_EQ(2, committed_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_EQ(3, committed_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/1)));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, ReadRows());
std::sort(rows.begin(), rows.end());
std::vector<Row> expected_rows = MakeRows(/*first_id=*/0, /*count=*/2, /*partition=*/"p0");
std::vector<Row> bucket1_rows = MakeRows(/*first_id=*/10, /*count=*/3, /*partition=*/"p0");
expected_rows.insert(expected_rows.end(), bucket1_rows.begin(), bucket1_rows.end());
std::sort(expected_rows.begin(), expected_rows.end());
ASSERT_EQ(expected_rows, rows);
ASSERT_OK(bucket0_writer->Close());
ASSERT_OK(bucket1_writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeCommitRetriesConflictWithCompaction) {
options_[Options::COMMIT_MAX_RETRIES] = "1";
options_[Options::COMMIT_MIN_RETRY_WAIT] = "1ms";
options_[Options::COMMIT_MAX_RETRY_WAIT] = "1ms";
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
for (int64_t id = 0; id < 2; ++id) {
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(MakeRows(id, /*count=*/1, /*partition=*/"p0"),
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/id));
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, /*commit_identifier=*/id));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
}
WriteContextBuilder compact_write_builder(table_path_, "compaction_write_user");
compact_write_builder.SetOptions(options_).WithStreamingMode(true).WithTempDirectory(
PathUtil::JoinPath(dir_->Str(), "conflict-compact-tmp"));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> compact_write_context,
compact_write_builder.Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> compact_writer,
FileStoreWrite::Create(std::move(compact_write_context)));
ASSERT_OK(compact_writer->Compact(/*partition=*/{}, /*bucket=*/0,
/*full_compaction=*/true));
ASSERT_OK_AND_ASSIGN(
std::vector<std::shared_ptr<CommitMessage>> compact_messages,
compact_writer->PrepareCommit(/*wait_compaction=*/true, /*commit_identifier=*/0));
ASSERT_EQ(1, compact_messages.size());
std::shared_ptr<CommitMessageImpl> compact_message =
std::dynamic_pointer_cast<CommitMessageImpl>(compact_messages[0]);
ASSERT_NE(nullptr, compact_message);
ASSERT_GE(compact_message->GetCompactIncrement().CompactBefore().size(), 2);
ASSERT_FALSE(compact_message->GetCompactIncrement().CompactAfter().empty());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> append_batch,
MakeBatch(MakeRows(/*first_id=*/2, /*count=*/1, /*partition=*/"p0"),
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(append_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> append_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
const std::string conflict_snapshot_path =
PathUtil::JoinPath(table_path_, "snapshot/snapshot-3");
auto conflict_file_system =
std::make_shared<SnapshotConflictFileSystem>(conflict_snapshot_path);
CommitContextBuilder append_commit_builder(table_path_, commit_user_);
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<CommitContext> append_commit_context,
append_commit_builder.SetOptions(options_).WithFileSystem(conflict_file_system).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> append_commit,
FileStoreCommit::Create(std::move(append_commit_context)));
CommitContextBuilder compact_commit_builder(table_path_, "compaction_commit_user");
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<CommitContext> compact_commit_context,
compact_commit_builder.SetOptions(options_).WithFileSystem(conflict_file_system).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> compact_commit,
FileStoreCommit::Create(std::move(compact_commit_context)));
std::optional<Result<int64_t>> append_result;
std::optional<Status> compact_status;
std::thread append_thread([&]() {
// CommitWithProgress probes the latest snapshot once for idempotency and once while
// starting TryCommit. Synchronize only its destination check.
conflict_file_system->SetPassthroughChecksForCurrentThread(/*checks=*/2);
append_result.emplace(append_commit->CommitWithProgress(
append_progress, /*commit_identifier=*/2, /*watermark=*/std::nullopt));
});
std::thread compact_thread([&]() {
// A regular commit probes the latest snapshot once before its destination check.
conflict_file_system->SetPassthroughChecksForCurrentThread(/*checks=*/1);
compact_status.emplace(compact_commit->Commit(compact_messages, /*commit_identifier=*/0,
/*watermark=*/std::nullopt));
});
append_thread.join();
compact_thread.join();
ASSERT_TRUE(append_result.has_value());
ASSERT_TRUE(compact_status.has_value());
ASSERT_OK_AND_ASSIGN(int64_t append_snapshot_id, std::move(append_result.value()));
ASSERT_OK(compact_status.value());
ASSERT_TRUE(append_snapshot_id == 3 || append_snapshot_id == 4);
ASSERT_EQ(2, conflict_file_system->ConflictSnapshotAttempts());
ASSERT_OK_AND_ASSIGN(uint64_t append_attempts, append_commit->GetCommitMetrics()->GetCounter(
CommitMetrics::LAST_COMMIT_ATTEMPTS));
ASSERT_OK_AND_ASSIGN(uint64_t compact_attempts, compact_commit->GetCommitMetrics()->GetCounter(
CommitMetrics::LAST_COMMIT_ATTEMPTS));
std::vector<uint64_t> attempts = {append_attempts, compact_attempts};
std::sort(attempts.begin(), attempts.end());
ASSERT_EQ((std::vector<uint64_t>{1, 2}), attempts);
ASSERT_OK_AND_ASSIGN(CoreOptions core_options, CoreOptions::FromMap(options_));
SnapshotManager snapshot_manager(core_options.GetFileSystem(), table_path_);
ASSERT_OK_AND_ASSIGN(Snapshot snapshot3, snapshot_manager.LoadSnapshot(/*snapshot_id=*/3));
ASSERT_OK_AND_ASSIGN(Snapshot snapshot4, snapshot_manager.LoadSnapshot(/*snapshot_id=*/4));
ASSERT_TRUE((snapshot3.GetCommitKind() == Snapshot::CommitKind::Append() &&
snapshot4.GetCommitKind() == Snapshot::CommitKind::Compact()) ||
(snapshot3.GetCommitKind() == Snapshot::CommitKind::Compact() &&
snapshot4.GetCommitKind() == Snapshot::CommitKind::Append()));
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap committed_offsets, ReadCommittedOffsets());
ASSERT_EQ(1, committed_offsets.size());
ASSERT_EQ(3, committed_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, ReadRows());
ASSERT_EQ(MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0"), rows);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(/*realtime_context=*/nullptr, /*predicate=*/nullptr));
ASSERT_OK_AND_ASSIGN(DiskFileLayout layout, InspectDiskFiles(plan->Splits()));
ASSERT_TRUE(layout.has_level0_file);
ASSERT_TRUE(layout.has_high_level_file);
ASSERT_OK(writer->Close());
ASSERT_OK(compact_writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestSealOnlySealsRealtimeSegment) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> first_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch(first_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(uint64_t memory_before_seal, GetRealtimeMemoryUsage(realtime_context));
ASSERT_GT(memory_before_seal, 0);
ASSERT_OK(writer->Seal());
ASSERT_OK_AND_ASSIGN(uint64_t memory_after_seal, GetRealtimeMemoryUsage(realtime_context));
ASSERT_EQ(memory_before_seal, memory_after_seal);
ASSERT_OK_AND_ASSIGN(std::vector<Row> sealed_rows, ReadRows(realtime_context));
ASSERT_EQ(first_rows, sealed_rows);
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows_before_prepare, ReadRows());
ASSERT_TRUE(disk_rows_before_prepare.empty());
std::vector<Row> second_rows = MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(second_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(second_batch)));
std::vector<Row> expected_rows = first_rows;
expected_rows.insert(expected_rows.end(), second_rows.begin(), second_rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_prepare, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_before_prepare);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, progress.size());
ASSERT_EQ(OffsetRange(0, 5), progress[0].offset_range);
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_prepare, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_after_prepare);
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_after_refresh);
ASSERT_OK(writer->Close());
realtime_context.reset();
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows());
ASSERT_EQ(expected_rows, actual_rows);
}
TEST_F(RealtimeWriteInteTest, TestSparseExternalOffsetsCommitAndRecover) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
const std::vector<int64_t> offsets = {10, 20, 30};
std::vector<Row> rows;
for (int64_t offset : offsets) {
ResetExternalOffset(/*partition=*/{}, /*bucket=*/0, offset);
std::vector<Row> one_row = MakeRows(static_cast<int64_t>(rows.size()), /*count=*/1, "p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(one_row, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
rows.push_back(one_row.front());
}
ASSERT_OK_AND_ASSIGN(std::vector<Row> realtime_rows, ReadRows(realtime_context));
ASSERT_EQ(rows, realtime_rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, commits.size());
ASSERT_EQ(OffsetRange(10, 31), commits[0].offset_range);
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(commits, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap committed_offsets, ReadCommittedOffsets());
ASSERT_EQ(31, committed_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(realtime_context));
ASSERT_EQ(rows, actual_rows);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestExplicitSnapshotIdRequiresExistingSnapshot) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch({{1, "memory", "p0"}}, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
options_[Options::SCAN_SNAPSHOT_ID] = "1";
ASSERT_NOK_WITH_MSG(CreatePlan(realtime_context, /*predicate=*/nullptr),
"There is currently no snapshot.");
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestExplicitSnapshotIdWithSparseTailAndReclaimBoundary) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ResetExternalOffset(/*partition=*/{}, /*bucket=*/0, /*next_offset=*/10);
const std::vector<Row> disk_rows = {{1, "disk", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeBatch(disk_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t disk_snapshot_id, Commit(disk_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(disk_snapshot_id));
// Offset gaps are valid. The snapshot high-watermark is 11 while the next memory row starts at
// 30, so a real-time scan must not require the two numeric offsets to be adjacent.
ResetExternalOffset(/*partition=*/{}, /*bucket=*/0, /*next_offset=*/30);
const std::vector<Row> memory_rows = {{2, "memory", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch(memory_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(memory_batch)));
options_[Options::SCAN_SNAPSHOT_ID] = std::to_string(disk_snapshot_id);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_EQ(disk_snapshot_id, plan->SnapshotId());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan, realtime_context));
ASSERT_EQ((std::vector<Row>{{1, "disk", "p0"}, {2, "memory", "p0"}}), actual_rows);
options_.erase(Options::SCAN_SNAPSHOT_ID);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> memory_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(int64_t memory_snapshot_id,
Commit(memory_progress, /*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(memory_snapshot_id));
options_[Options::SCAN_SNAPSHOT_ID] = std::to_string(disk_snapshot_id);
ASSERT_NOK_WITH_MSG(CreatePlan(realtime_context, /*predicate=*/nullptr),
"behind context committed offset");
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestPkRead) {
options_[Options::REALTIME_STORE_STATS_MODE] = "full";
CreatePkTable();
auto saw_query_predicate = std::make_shared<std::atomic<bool>>(false);
auto query_view = std::make_shared<std::weak_ptr<RealtimeReadView>>();
auto factory =
MakeDecoratingFactory<QueryTrackingRealtimeStore>(saw_query_predicate, query_view);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create(factory));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> first_rows = {{1, "old", "p0"}, {2, "two", "p0"}, {1, "new-in-run", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch(first_rows, /*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::INSERT, RecordBatch::RowKind::INSERT,
RecordBatch::RowKind::UPDATE_AFTER}));
ASSERT_OK(writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> update_batch,
MakeBatch({Row{1, "new", "p0"}}, /*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER}));
ASSERT_OK(writer->Write(std::move(update_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<Row> memory_rows, ReadRows(realtime_context));
ASSERT_EQ((std::vector<Row>{{1, "new", "p0"}, {2, "two", "p0"}}), memory_rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, progress.size());
ASSERT_OK(Commit(progress, /*commit_identifier=*/0));
std::vector<Row> second_rows = {{1, "latest", "p0"}, {2, "gone", "p0"}, {3, "three", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(second_rows, /*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER,
RecordBatch::RowKind::DELETE, RecordBatch::RowKind::INSERT}));
ASSERT_OK(writer->Write(std::move(second_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<Row> union_rows, ReadRows(realtime_context));
ASSERT_EQ((std::vector<Row>{{1, "latest", "p0"}, {3, "three", "p0"}}), union_rows);
const std::string expected_payload = "new";
std::shared_ptr<Predicate> predicate = PredicateBuilder::Equal(
/*field_index=*/1, /*field_name=*/"payload", FieldType::STRING,
Literal(FieldType::STRING, expected_payload.data(), expected_payload.size()));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> filtered_plan,
CreatePlan(realtime_context, predicate));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> filtered_result,
ReadPlan(filtered_plan, realtime_context, {"id", "payload", "pt"},
predicate, /*enable_predicate_filter=*/true));
ASSERT_EQ(nullptr, filtered_result);
ASSERT_FALSE(saw_query_predicate->load(std::memory_order_acquire));
std::shared_ptr<Predicate> primary_key_predicate = PredicateBuilder::Equal(
/*field_index=*/0, /*field_name=*/"id", FieldType::BIGINT, Literal(int64_t{3}));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> primary_key_plan,
CreatePlan(realtime_context, primary_key_predicate));
ASSERT_OK_AND_ASSIGN(std::vector<Row> primary_key_rows,
ReadRows(primary_key_plan, realtime_context, primary_key_predicate,
/*enable_predicate_filter=*/true));
ASSERT_EQ((std::vector<Row>{{3, "three", "p0"}}), primary_key_rows);
ASSERT_TRUE(saw_query_predicate->load(std::memory_order_acquire));
ASSERT_OK(PrepareAndClose(writer.get()));
writer.reset();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> lifetime_plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ReadContextBuilder read_builder(table_path_);
read_builder.SetOptions(options_)
.SetReadFieldNames({"id", "payload", "pt"})
.WithRealtimeContext(realtime_context)
.WithMemoryPool(pool_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> read_context, read_builder.Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableRead> table_read,
TableRead::Create(std::move(read_context)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<BatchReader> reader,
table_read->CreateReader(lifetime_plan->Splits()));
ASSERT_FALSE(query_view->expired());
std::weak_ptr<RealtimeContext> weak_context = realtime_context;
table_read.reset();
lifetime_plan.reset();
realtime_context.reset();
ASSERT_TRUE(weak_context.expired());
ASSERT_FALSE(query_view->expired());
ASSERT_OK_AND_ASSIGN(BatchReader::ReadBatch read_batch, reader->NextBatch());
ASSERT_FALSE(BatchReader::IsEofBatch(read_batch));
reader->Close();
reader.reset();
ASSERT_TRUE(query_view->expired());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::Array> read_array,
ReadResultCollector::GetArray(std::move(read_batch)));
ASSERT_NE(nullptr, read_array);
read_array.reset();
}
TEST_F(RealtimeWriteInteTest, TestPkVector) {
options_[Options::FILE_FORMAT] = "parquet";
std::shared_ptr<arrow::DataType> vector_type =
arrow::fixed_size_list(arrow::field("item", arrow::float32(), /*nullable=*/false), 3);
fields_ = {arrow::field("id", arrow::int64()), arrow::field("embedding", vector_type)};
schema_ = arrow::schema(fields_);
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch, MakeUnpartitionedBatchFromJson(R"([
[0, 2, [2.0, 2.0, 2.0]],
[1, 3, null],
[2, 1, [1.0, 1.0, 1.0]],
[3, 2, [3.0, 3.0, 3.0]]
])"));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ReadPlanWithSchemaAndCheck(plan, realtime_context, schema_,
R"([[0, 1, [1.0, 1.0, 1.0]],
[0, 2, [3.0, 3.0, 3.0]],
[0, 3, null]])");
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestPkRealtimeReadOptimizedScanUnsupported) {
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
const std::vector<Row> rows = {{1, "one", "p0"}, {2, "two", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> disk_rows, ReadRows());
ASSERT_EQ(rows, disk_rows);
ScanContextBuilder scan_builder(table_path_ + "$ro");
scan_builder.SetOptions(options_).WithRealtimeContext(realtime_context).WithMemoryPool(pool_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ScanContext> scan_context, scan_builder.Finish());
Result<std::unique_ptr<TableScan>> scan = TableScan::Create(std::move(scan_context));
ASSERT_TRUE(scan.status().IsNotImplemented()) << scan.status().ToString();
ASSERT_NE(std::string::npos, scan.status().ToString().find(
"PK real-time union read does not support read-optimized"));
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestPkDeleteInsertAndPinnedReadsAcrossRefresh) {
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> delete_batch,
MakeBatch({Row{1, "deleted", "p0"}}, /*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::DELETE}));
ASSERT_OK(writer->Write(std::move(delete_batch)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> insert_batch,
MakeBatch({Row{1, "inserted", "p0"}}, /*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::INSERT}));
ASSERT_OK(writer->Write(std::move(insert_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, progress.size());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> pinned_plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> reader_plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ReadContextBuilder read_builder(table_path_);
read_builder.SetOptions(options_)
.SetReadFieldNames({"id", "payload", "pt"})
.WithRealtimeContext(realtime_context)
.WithMemoryPool(pool_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> read_context, read_builder.Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableRead> table_read,
TableRead::Create(std::move(read_context)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<BatchReader> pinned_reader,
table_read->CreateReader(reader_plan->Splits()));
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> plan_rows, ReadRows(pinned_plan, realtime_context));
ASSERT_EQ((std::vector<Row>{{1, "inserted", "p0"}}), plan_rows);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> reader_rows,
ReadResultCollector::CollectResult(std::move(pinned_reader)));
ASSERT_EQ(1, reader_rows->length());
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestPkMergeDiskSealedAndActive) {
options_[Options::READ_BATCH_SIZE] = "2";
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
const std::vector<std::vector<Row>> disk_batches = {
{{1, "disk-1", "p0"}, {2, "disk-2", "p0"}, {3, "disk-3", "p0"}},
{{10, "disk-10", "p0"}, {11, "disk-11", "p0"}},
};
int64_t commit_identifier = 0;
for (const std::vector<Row>& disk_rows : disk_batches) {
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(disk_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(commit_identifier));
ASSERT_EQ(1, progress.size());
ASSERT_EQ(1, NewFiles(progress).size());
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, commit_identifier));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
++commit_identifier;
}
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> sealed_batch,
MakeBatch({Row{1, "sealed-1", "p0"}, Row{2, "deleted-2", "p0"}, Row{4, "sealed-4", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER, RecordBatch::RowKind::DELETE,
RecordBatch::RowKind::INSERT}));
ASSERT_OK(writer->Write(std::move(sealed_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> sealed_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
ASSERT_EQ(1, sealed_progress.size());
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> active_batch,
MakeBatch({Row{1, "active-1", "p0"}, Row{4, "deleted-4", "p0"}, Row{5, "active-5", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER, RecordBatch::RowKind::DELETE,
RecordBatch::RowKind::INSERT}));
ASSERT_OK(writer->Write(std::move(active_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result,
ReadPlan(plan, realtime_context, {"payload", "id"}, /*predicate=*/nullptr,
/*enable_predicate_filter=*/false));
ASSERT_NE(nullptr, result);
ASSERT_GT(result->num_chunks(), 1);
for (const std::shared_ptr<arrow::Array>& chunk : result->chunks()) {
ASSERT_LE(chunk->length(), 2);
}
std::shared_ptr<arrow::DataType> result_type = arrow::struct_(
{arrow::field("_VALUE_KIND", arrow::int8()), arrow::field("payload", arrow::utf8()),
arrow::field("id", arrow::int64())});
std::shared_ptr<arrow::Array> expected =
arrow::ipc::internal::json::ArrayFromJSON(result_type, R"([
[0, "active-1", 1],
[0, "disk-3", 3],
[0, "active-5", 5],
[0, "disk-10", 10],
[0, "disk-11", 11]
])")
.ValueOrDie();
ASSERT_TRUE(std::make_shared<arrow::ChunkedArray>(expected)->Equals(*result))
<< result->ToString();
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestPkMergeAllDiskSplitsWithMemory) {
options_[Options::SOURCE_SPLIT_OPEN_FILE_COST] = "1";
options_[Options::SOURCE_SPLIT_TARGET_SIZE] = "1";
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
const std::vector<std::vector<Row>> disk_batches = {
{{1, "disk-1", "p0"}, {2, "disk-2", "p0"}},
{{10, "disk-10", "p0"}, {11, "disk-11", "p0"}},
{{20, "disk-20", "p0"}, {21, "disk-21", "p0"}},
};
for (int64_t commit_identifier = 0;
commit_identifier < static_cast<int64_t>(disk_batches.size()); ++commit_identifier) {
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(disk_batches[commit_identifier], /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(commit_identifier));
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(progress, commit_identifier));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
}
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> memory_batch,
MakeBatch({Row{1, "memory-1", "p0"}, Row{10, "deleted-10", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER, RecordBatch::RowKind::DELETE}));
ASSERT_OK(writer->Write(std::move(memory_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_EQ(1, plan->Splits().size());
std::shared_ptr<RealtimeSplit> realtime_split =
std::dynamic_pointer_cast<RealtimeSplit>(plan->Splits()[0]);
ASSERT_NE(nullptr, realtime_split);
ASSERT_EQ(3, realtime_split->DiskSplits().size());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan, realtime_context));
ASSERT_EQ((std::vector<Row>{{1, "memory-1", "p0"},
{2, "disk-2", "p0"},
{11, "disk-11", "p0"},
{20, "disk-20", "p0"},
{21, "disk-21", "p0"}}),
actual_rows);
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestPkNestedProjectionAcrossDiskAndMemory) {
const std::shared_ptr<arrow::Field> projected_b = arrow::field("b", arrow::int64());
fields_ = {
arrow::field("id", arrow::int64()),
arrow::field("payload", arrow::struct_({arrow::field("a", arrow::int64()), projected_b})),
arrow::field("pt", arrow::utf8()),
};
schema_ = arrow::schema(fields_);
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RealtimeSchemaLayout> schema_layout,
RealtimeSchemaLayout::Create(RealtimeStoreMode::PRIMARY_KEY, schema_));
const std::shared_ptr<arrow::Schema>& realtime_schema = schema_layout->InputSchema();
auto make_batch = [&](const std::string& json) -> Result<std::unique_ptr<RecordBatch>> {
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
arrow::ipc::internal::json::ArrayFromJSON(
arrow::struct_(realtime_schema->fields()), json));
ArrowArray c_array;
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*array, &c_array));
return RecordBatchBuilder(&c_array).SetBucket(0).Finish();
};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
make_batch(R"([[0, 1, [101, 1001], "p0"],
[1, 2, [102, 1002], "p0"]])"));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(disk_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> sealed_batch,
make_batch(R"([[2, 1, [201, 2001], "p0"],
[3, 3, [203, 2003], "p0"]])"));
ASSERT_OK(writer->Write(std::move(sealed_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> sealed_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, sealed_progress.size());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> active_batch,
make_batch(R"([[4, 1, [301, 3001], "p0"],
[5, 4, [304, null], "p0"]])"));
ASSERT_OK(writer->Write(std::move(active_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
auto projected_schema = arrow::schema({
arrow::field("payload", arrow::struct_({projected_b})),
arrow::field("id", arrow::int64()),
});
auto c_schema = std::make_unique<ArrowSchema>();
ASSERT_TRUE(arrow::ExportSchema(*projected_schema, c_schema.get()).ok());
ReadContextBuilder read_builder(table_path_);
read_builder.SetOptions(options_)
.SetReadSchema(std::move(c_schema))
.WithRealtimeContext(realtime_context)
.WithMemoryPool(pool_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> read_context, read_builder.Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableRead> table_read,
TableRead::Create(std::move(read_context)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<BatchReader> reader,
table_read->CreateReader(plan->Splits()));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> actual,
ReadResultCollector::CollectResult(std::move(reader)));
const std::shared_ptr<arrow::DataType> result_type = arrow::struct_({
arrow::field("_VALUE_KIND", arrow::int8()),
arrow::field("payload", arrow::struct_({projected_b})),
arrow::field("id", arrow::int64()),
});
const std::shared_ptr<arrow::Array> expected =
arrow::ipc::internal::json::ArrayFromJSON(result_type, R"([
[0, [3001], 1],
[0, [1002], 2],
[0, [2003], 3],
[0, [null], 4]
])")
.ValueOrDie();
ASSERT_TRUE(std::make_shared<arrow::ChunkedArray>(expected)->Equals(*actual))
<< actual->ToString();
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestPkKeylessProjection) {
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeBatch({Row{1, "disk", "p0"}}, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(disk_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch({Row{1, "memory", "p0"}}, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(memory_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ReadPlanWithSchemaAndCheck(plan, realtime_context,
arrow::schema({arrow::field("payload", arrow::utf8())}), R"([
[0, "memory"]
])");
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestCompositePkKeylessProjection) {
CreatePkTable(/*partition_keys=*/{}, /*primary_keys=*/{"id", "payload"});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeBatch({Row{1, "key", "disk"}}, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(disk_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch({Row{1, "key", "memory"}}, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(memory_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ReadPlanWithSchemaAndCheck(plan, realtime_context,
arrow::schema({arrow::field("pt", arrow::utf8())}), R"([
[0, "memory"]
])");
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestPkCompositeMerge) {
CreatePkTable(/*partition_keys=*/{}, /*primary_keys=*/{"id", "payload"});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeBatch({Row{1, "a", "disk-1a"}, Row{1, "b", "disk-1b"},
Row{2, "a", "disk-2a"}, Row{3, "c", "disk-3c"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, disk_progress.size());
ASSERT_EQ(OffsetRange(0, 4), disk_progress[0].offset_range);
ASSERT_EQ(1, NewFiles(disk_progress).size());
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(disk_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> sealed_batch,
MakeBatch({Row{1, "a", "sealed-1a"}, Row{1, "b", "deleted-1b"}, Row{2, "b", "sealed-2b"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER, RecordBatch::RowKind::DELETE,
RecordBatch::RowKind::INSERT}));
ASSERT_OK(writer->Write(std::move(sealed_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> sealed_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, sealed_progress.size());
ASSERT_EQ(OffsetRange(4, 7), sealed_progress[0].offset_range);
ASSERT_EQ(1, NewFiles(sealed_progress).size());
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> active_batch,
MakeBatch({Row{1, "a", "active-1a"}, Row{1, "c", "active-1c"}, Row{2, "a", "active-2a"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER, RecordBatch::RowKind::INSERT,
RecordBatch::RowKind::UPDATE_AFTER}));
ASSERT_OK(writer->Write(std::move(active_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_EQ(1, plan->Splits().size());
std::shared_ptr<RealtimeSplit> split =
std::dynamic_pointer_cast<RealtimeSplit>(plan->Splits()[0]);
ASSERT_NE(nullptr, split);
ASSERT_FALSE(split->DiskSplits().empty());
ASSERT_EQ(4, split->CommittedEndOffset());
ASSERT_EQ(10, split->MemoryEndOffset());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan, realtime_context));
ASSERT_EQ((std::vector<Row>{{1, "a", "active-1a"},
{1, "c", "active-1c"},
{2, "a", "active-2a"},
{2, "b", "sealed-2b"},
{3, "c", "disk-3c"}}),
actual_rows);
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestPkWriterHandoff) {
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> first_writer,
CreateRealtimeWriter(realtime_context));
const std::vector<Row> first_rows = {
{0, "value-0", "p0"}, {1, "value-1", "p0"}, {2, "value-2", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch(first_rows, /*partitioned=*/false));
ASSERT_OK(first_writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_progress,
first_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, first_progress.size());
ASSERT_EQ(OffsetRange(0, 3), first_progress[0].offset_range);
ASSERT_EQ(1, NewFiles(first_progress).size());
ASSERT_EQ(0, NewFiles(first_progress)[0]->min_sequence_number);
ASSERT_EQ(2, NewFiles(first_progress)[0]->max_sequence_number);
ASSERT_OK(first_writer->Close());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> second_writer,
CreateRealtimeWriter(realtime_context));
const std::vector<Row> second_rows = {{0, "updated-0", "p0"}, {3, "value-3", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(second_rows, /*partitioned=*/false));
ASSERT_OK(second_writer->Write(std::move(second_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_progress,
second_writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, second_progress.size());
ASSERT_EQ(OffsetRange(3, 5), second_progress[0].offset_range);
ASSERT_EQ(1, NewFiles(second_progress).size());
ASSERT_EQ(3, NewFiles(second_progress)[0]->min_sequence_number);
ASSERT_EQ(4, NewFiles(second_progress)[0]->max_sequence_number);
first_progress.push_back(std::move(second_progress[0]));
ASSERT_OK(Commit(first_progress, /*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(realtime_context));
ASSERT_EQ((std::vector<Row>{{0, "updated-0", "p0"},
{1, "value-1", "p0"},
{2, "value-2", "p0"},
{3, "value-3", "p0"}}),
actual_rows);
ASSERT_OK(second_writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestPkPartitionBucketRecovery) {
options_[Options::BUCKET] = "2";
CreatePkTable(/*partition_keys=*/{"pt"});
const RealtimePartitionBucket p0b0({{"pt", "p0"}}, /*bucket=*/0);
const RealtimePartitionBucket p1b1({{"pt", "p1"}}, /*bucket=*/1);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> first_context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> first_writer,
CreateRealtimeWriter(first_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> p0_first_batch,
MakeBatch({Row{0, "p0-zero", "p0"}, Row{1, "p0-one", "p0"}},
/*partitioned=*/true, /*bucket=*/0));
ASSERT_OK(first_writer->Write(std::move(p0_first_batch)));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> p1_first_batch,
MakeBatch({Row{10, "p1-ten", "p1"}, Row{11, "p1-eleven", "p1"}, Row{12, "p1-twelve", "p1"}},
/*partitioned=*/true, /*bucket=*/1));
ASSERT_OK(first_writer->Write(std::move(p1_first_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_progress,
first_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(2, first_progress.size());
std::map<RealtimePartitionBucket, OffsetRange> first_ranges;
std::map<RealtimePartitionBucket, std::pair<int64_t, int64_t>> first_sequences;
for (const RealtimeCommitProgress& progress : first_progress) {
first_ranges.emplace(progress.partition_bucket, progress.offset_range);
std::shared_ptr<CommitMessageImpl> message =
std::dynamic_pointer_cast<CommitMessageImpl>(progress.commit_message);
ASSERT_NE(nullptr, message);
const std::vector<std::shared_ptr<DataFileMeta>>& files =
message->GetNewFilesIncrement().NewFiles();
ASSERT_EQ(1, files.size());
first_sequences.emplace(
progress.partition_bucket,
std::make_pair(files[0]->min_sequence_number, files[0]->max_sequence_number));
}
ASSERT_EQ(OffsetRange(0, 2), first_ranges.at(p0b0));
ASSERT_EQ(OffsetRange(0, 3), first_ranges.at(p1b1));
ASSERT_EQ((std::pair<int64_t, int64_t>(0, 1)), first_sequences.at(p0b0));
ASSERT_EQ((std::pair<int64_t, int64_t>(0, 2)), first_sequences.at(p1b1));
ASSERT_OK_AND_ASSIGN(int64_t first_snapshot_id,
Commit(first_progress, /*commit_identifier=*/0));
ASSERT_OK(first_writer->RefreshCommittedSnapshot(first_snapshot_id));
ASSERT_OK(first_writer->Close());
first_writer.reset();
first_context.reset();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> second_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> second_writer,
CreateRealtimeWriter(second_context));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> p0_second_batch,
MakeBatch({Row{0, "p0-zero-new", "p0"}, Row{2, "p0-two", "p0"}},
/*partitioned=*/true, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER, RecordBatch::RowKind::INSERT}));
ASSERT_OK(second_writer->Write(std::move(p0_second_batch)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> p1_second_batch,
MakeBatch({Row{10, "p1-ten-deleted", "p1"}, Row{13, "p1-thirteen", "p1"}},
/*partitioned=*/true, /*bucket=*/1,
{RecordBatch::RowKind::DELETE, RecordBatch::RowKind::INSERT}));
ASSERT_OK(second_writer->Write(std::move(p1_second_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_progress,
second_writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(2, second_progress.size());
std::map<RealtimePartitionBucket, OffsetRange> second_ranges;
std::map<RealtimePartitionBucket, std::pair<int64_t, int64_t>> second_sequences;
for (const RealtimeCommitProgress& progress : second_progress) {
second_ranges.emplace(progress.partition_bucket, progress.offset_range);
std::shared_ptr<CommitMessageImpl> message =
std::dynamic_pointer_cast<CommitMessageImpl>(progress.commit_message);
ASSERT_NE(nullptr, message);
const std::vector<std::shared_ptr<DataFileMeta>>& files =
message->GetNewFilesIncrement().NewFiles();
ASSERT_EQ(1, files.size());
second_sequences.emplace(
progress.partition_bucket,
std::make_pair(files[0]->min_sequence_number, files[0]->max_sequence_number));
}
ASSERT_EQ(OffsetRange(2, 4), second_ranges.at(p0b0));
ASSERT_EQ(OffsetRange(3, 5), second_ranges.at(p1b1));
ASSERT_EQ((std::pair<int64_t, int64_t>(2, 3)), second_sequences.at(p0b0));
ASSERT_EQ((std::pair<int64_t, int64_t>(3, 4)), second_sequences.at(p1b1));
ASSERT_OK_AND_ASSIGN(int64_t second_snapshot_id,
Commit(second_progress, /*commit_identifier=*/1));
ASSERT_OK(second_writer->RefreshCommittedSnapshot(second_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(second_context));
std::sort(actual_rows.begin(), actual_rows.end());
ASSERT_EQ((std::vector<Row>{{0, "p0-zero-new", "p0"},
{1, "p0-one", "p0"},
{2, "p0-two", "p0"},
{11, "p1-eleven", "p1"},
{12, "p1-twelve", "p1"},
{13, "p1-thirteen", "p1"}}),
actual_rows);
ASSERT_OK(second_writer->Close());
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap offsets, ReadCommittedOffsets());
ASSERT_EQ(2, offsets.size());
ASSERT_EQ(4, offsets.at(p0b0));
ASSERT_EQ(5, offsets.at(p1b1));
}
TEST_F(RealtimeWriteInteTest, TestPkRecovery) {
CreatePkTable();
WriteContextBuilder seed_builder(table_path_, commit_user_);
seed_builder.SetOptions(options_).WithStreamingMode(true);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> seed_context, seed_builder.Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> seed_writer,
FileStoreWrite::Create(std::move(seed_context)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> seed_batch,
MakeLogicalBatch({Row{99, "seed", "p0"}}, /*bucket=*/0));
ASSERT_OK(seed_writer->Write(std::move(seed_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<std::shared_ptr<CommitMessage>> seed_messages,
seed_writer->PrepareCommit(/*wait_compaction=*/false,
/*commit_identifier=*/0));
CommitContextBuilder seed_commit_builder(table_path_, commit_user_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> seed_commit_context,
seed_commit_builder.SetOptions(options_).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> seed_commit,
FileStoreCommit::Create(std::move(seed_commit_context)));
ASSERT_OK(seed_commit->Commit(seed_messages, /*commit_identifier=*/0));
ASSERT_OK(seed_writer->Close());
const std::vector<Row> mutations = {
{1, "one", "p0"}, {1, "one-new", "p0"}, {2, "deleted", "p0"}, {3, "three", "p0"}};
const std::vector<RecordBatch::RowKind> mutation_kinds = {
RecordBatch::RowKind::INSERT, RecordBatch::RowKind::UPDATE_AFTER,
RecordBatch::RowKind::DELETE, RecordBatch::RowKind::INSERT};
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> first_context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> first_writer,
CreateRealtimeWriter(first_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(mutations, /*partitioned=*/false, /*bucket=*/0, mutation_kinds));
ASSERT_OK(first_writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<int64_t> memory_sequences, ReadPkSequences(first_context));
ASSERT_EQ((std::vector<int64_t>{1, 2, 3, 4}), memory_sequences);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
first_writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, progress.size());
ASSERT_EQ(OffsetRange(0, 4), progress[0].offset_range);
ASSERT_EQ(1, NewFiles(progress).size());
ASSERT_EQ(2, NewFiles(progress)[0]->min_sequence_number);
ASSERT_EQ(memory_sequences.back(), NewFiles(progress)[0]->max_sequence_number);
ASSERT_EQ(1, NewFiles(progress)[0]->delete_row_count);
ASSERT_OK(Commit(progress, /*commit_identifier=*/1));
ASSERT_OK(first_writer->Close());
first_context.reset();
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_replay, ReadRows());
ASSERT_EQ((std::vector<Row>{{1, "one-new", "p0"}, {3, "three", "p0"}, {99, "seed", "p0"}}),
rows_after_replay);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> second_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> second_writer,
CreateRealtimeWriter(second_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> restart_batch,
MakeBatch({Row{4, "four", "p0"}}, /*partitioned=*/false));
ASSERT_OK(second_writer->Write(std::move(restart_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<int64_t> restart_sequences, ReadPkSequences(second_context));
ASSERT_EQ((std::vector<int64_t>{5}), restart_sequences);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> restart_progress,
second_writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
ASSERT_EQ(1, restart_progress.size());
ASSERT_EQ(OffsetRange(4, 5), restart_progress[0].offset_range);
ASSERT_EQ(5, NewFiles(restart_progress)[0]->min_sequence_number);
ASSERT_EQ(5, NewFiles(restart_progress)[0]->max_sequence_number);
ASSERT_OK(second_writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestPkCompaction) {
options_[Options::NUM_SORTED_RUNS_COMPACTION_TRIGGER] = "1";
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
int64_t latest_snapshot_id = -1;
constexpr int64_t kCommitRoundsBeforeCompaction = 4;
std::set<std::string> committed_file_names;
for (int64_t round = 0; round < kCommitRoundsBeforeCompaction; ++round) {
const bool delete_latest_live_row = round == kCommitRoundsBeforeCompaction - 1;
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> batch,
MakeBatch(
{Row{delete_latest_live_row ? round - 1 : round,
delete_latest_live_row ? "deleted" : "value-" + std::to_string(round), "p0"}},
/*partitioned=*/false, /*bucket=*/0,
delete_latest_live_row
? std::vector<RecordBatch::RowKind>{RecordBatch::RowKind::DELETE}
: std::vector<RecordBatch::RowKind>{}));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(round));
ASSERT_EQ(1, progress.size());
std::shared_ptr<CommitMessageImpl> message =
std::dynamic_pointer_cast<CommitMessageImpl>(progress[0].commit_message);
ASSERT_NE(nullptr, message);
ASSERT_TRUE(message->GetCompactIncrement().IsEmpty());
ASSERT_EQ(1, NewFiles(progress).size());
committed_file_names.insert(NewFiles(progress)[0]->file_name);
ASSERT_OK_AND_ASSIGN(latest_snapshot_id, Commit(progress, round));
ASSERT_OK(writer->RefreshCommittedSnapshot(latest_snapshot_id));
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage, GetRealtimeMemoryUsage(realtime_context));
ASSERT_EQ(0, memory_usage);
}
WriteContextBuilder compact_builder(table_path_, commit_user_);
compact_builder.SetOptions(options_).WithStreamingMode(true);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<WriteContext> compact_context, compact_builder.Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> compact_writer,
FileStoreWrite::Create(std::move(compact_context)));
ASSERT_OK(compact_writer->Compact(/*partition=*/{}, /*bucket=*/0,
/*full_compaction=*/true));
ASSERT_OK_AND_ASSIGN(
std::vector<std::shared_ptr<CommitMessage>> compact_messages,
compact_writer->PrepareCommit(/*wait_compaction=*/true, /*commit_identifier=*/4));
ASSERT_EQ(1, compact_messages.size());
std::shared_ptr<CommitMessageImpl> compact_message =
std::dynamic_pointer_cast<CommitMessageImpl>(compact_messages[0]);
ASSERT_NE(nullptr, compact_message);
ASSERT_TRUE(compact_message->GetNewFilesIncrement().IsEmpty());
ASSERT_EQ(kCommitRoundsBeforeCompaction,
compact_message->GetCompactIncrement().CompactBefore().size());
std::set<std::string> compacted_file_names;
for (const std::shared_ptr<DataFileMeta>& file :
compact_message->GetCompactIncrement().CompactBefore()) {
compacted_file_names.insert(file->file_name);
}
ASSERT_EQ(committed_file_names, compacted_file_names);
ASSERT_FALSE(compact_message->GetCompactIncrement().CompactAfter().empty());
constexpr int64_t kHistoricalMaxSequenceNumber = kCommitRoundsBeforeCompaction - 1;
int64_t compacted_live_max_sequence_number = -1;
for (const std::shared_ptr<DataFileMeta>& file :
compact_message->GetCompactIncrement().CompactAfter()) {
compacted_live_max_sequence_number =
std::max(compacted_live_max_sequence_number, file->max_sequence_number);
}
ASSERT_LT(compacted_live_max_sequence_number, kHistoricalMaxSequenceNumber);
CommitContextBuilder commit_builder(table_path_, commit_user_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> commit_context,
commit_builder.SetOptions(options_).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(commit_context)));
ASSERT_OK(commit->Commit(compact_messages, /*commit_identifier=*/4));
ASSERT_OK(compact_writer->Close());
ASSERT_OK_AND_ASSIGN(CoreOptions options, CoreOptions::FromMap(options_));
SnapshotManager snapshot_manager(options.GetFileSystem(), table_path_);
ASSERT_OK_AND_ASSIGN(std::optional<Snapshot> compact_snapshot,
snapshot_manager.LatestSnapshot());
ASSERT_TRUE(compact_snapshot);
ASSERT_EQ(Snapshot::CommitKind::Compact(), compact_snapshot->GetCommitKind());
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap offsets, ReadCommittedOffsets());
ASSERT_EQ(4, offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_OK_AND_ASSIGN(std::vector<Row> compacted_rows, ReadRows());
ASSERT_EQ((std::vector<Row>{{0, "value-0", "p0"}, {1, "value-1", "p0"}}), compacted_rows);
ASSERT_OK(writer->Close());
writer.reset();
realtime_context.reset();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> fresh_context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> fresh_writer,
CreateRealtimeWriter(fresh_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> fresh_batch,
MakeBatch({Row{4, "value-4", "p0"}},
/*partitioned=*/false));
ASSERT_OK(fresh_writer->Write(std::move(fresh_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<int64_t> fresh_sequences, ReadPkSequences(fresh_context));
ASSERT_EQ((std::vector<int64_t>{compacted_live_max_sequence_number + 1}), fresh_sequences);
ASSERT_LT(fresh_sequences.front(), kHistoricalMaxSequenceNumber);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> fresh_progress,
fresh_writer->PrepareCommitWithProgress(/*commit_identifier=*/5));
ASSERT_EQ(1, fresh_progress.size());
ASSERT_EQ(OffsetRange(4, 5), fresh_progress[0].offset_range);
ASSERT_EQ(compacted_live_max_sequence_number + 1,
NewFiles(fresh_progress)[0]->min_sequence_number);
ASSERT_EQ(compacted_live_max_sequence_number + 1,
NewFiles(fresh_progress)[0]->max_sequence_number);
ASSERT_OK_AND_ASSIGN(latest_snapshot_id, Commit(fresh_progress, /*commit_identifier=*/5));
ASSERT_OK(fresh_writer->Close());
ASSERT_OK_AND_ASSIGN(offsets, ReadCommittedOffsets());
ASSERT_EQ(5, offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_OK_AND_ASSIGN(std::vector<Row> final_rows, ReadRows());
ASSERT_EQ((std::vector<Row>{{0, "value-0", "p0"}, {1, "value-1", "p0"}, {4, "value-4", "p0"}}),
final_rows);
}
TEST_F(RealtimeWriteInteTest, TestPkMultipleStoredBatchesMergeForQueryAndCommit) {
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch({Row{4, "four", "p0"}, Row{2, "two", "p0"}, Row{1, "one", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> second_batch,
MakeBatch({Row{3, "three", "p0"}, Row{2, "deleted", "p0"}, Row{1, "one-new", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::INSERT, RecordBatch::RowKind::DELETE,
RecordBatch::RowKind::UPDATE_AFTER}));
ASSERT_OK(writer->Write(std::move(second_batch)));
const std::vector<Row> expected = {{1, "one-new", "p0"}, {3, "three", "p0"}, {4, "four", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> query_rows, ReadRows(realtime_context));
ASSERT_EQ(expected, query_rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, progress.size());
ASSERT_EQ(OffsetRange(0, 6), progress[0].offset_range);
ASSERT_OK(Commit(progress, /*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows, ReadRows());
ASSERT_EQ(expected, rows);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRollingFilesPreserveProgress) {
options_[Options::TARGET_FILE_ROW_NUM] = "10";
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer, CreateRealtimeWriter());
std::vector<Row> expected_rows;
constexpr int64_t kBatchCount = 3;
constexpr int64_t kRowsPerBatch = 10;
for (int64_t batch_index = 0; batch_index < kBatchCount; ++batch_index) {
std::vector<Row> rows =
MakeRows(batch_index * kRowsPerBatch, kRowsPerBatch, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
expected_rows.insert(expected_rows.end(), rows.begin(), rows.end());
}
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, commits.size());
ASSERT_EQ(OffsetRange(0, kBatchCount * kRowsPerBatch), commits[0].offset_range);
std::shared_ptr<CommitMessageImpl> commit_message =
std::dynamic_pointer_cast<CommitMessageImpl>(commits[0].commit_message);
ASSERT_NE(nullptr, commit_message);
ASSERT_EQ(3, commit_message->GetNewFilesIncrement().NewFiles().size());
ASSERT_OK(Commit(commits, /*commit_identifier=*/0));
ASSERT_OK(writer->Close());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows());
ASSERT_EQ(expected_rows, actual_rows);
}
TEST_F(RealtimeWriteInteTest, TestAppendScanKeepsDiskSplitsIndependent) {
options_[Options::TARGET_FILE_ROW_NUM] = "2";
options_[Options::SOURCE_SPLIT_OPEN_FILE_COST] = "1";
options_[Options::SOURCE_SPLIT_TARGET_SIZE] = "1";
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> expected_rows;
for (int64_t first_id = 0; first_id < 6; first_id += 2) {
std::vector<Row> rows = MakeRows(first_id, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
expected_rows.insert(expected_rows.end(), rows.begin(), rows.end());
}
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t snapshot_id, Commit(commits, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot_id));
std::vector<Row> memory_rows = MakeRows(/*first_id=*/6, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch(memory_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(memory_batch)));
expected_rows.insert(expected_rows.end(), memory_rows.begin(), memory_rows.end());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_EQ(3, plan->Splits().size());
ASSERT_EQ(nullptr, std::dynamic_pointer_cast<RealtimeSplit>(plan->Splits()[0]));
ASSERT_EQ(nullptr, std::dynamic_pointer_cast<RealtimeSplit>(plan->Splits()[1]));
std::shared_ptr<RealtimeSplit> realtime_split =
std::dynamic_pointer_cast<RealtimeSplit>(plan->Splits()[2]);
ASSERT_NE(nullptr, realtime_split);
ASSERT_EQ(1, realtime_split->DiskSplits().size());
ASSERT_EQ(6, realtime_split->CommittedEndOffset());
ASSERT_EQ(8, realtime_split->MemoryEndOffset());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan, realtime_context));
ASSERT_EQ(expected_rows, actual_rows);
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestAppendRealtimeReadUsesEmbeddedAndExternalBitmapIndexes) {
options_["file-index.bitmap.columns"] = "payload";
options_[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1MB";
options_[Options::REALTIME_STORE_STATS_MODE] = "full";
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> embedded_writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> embedded_batch,
MakeBatch(
{{1, "embedded-one", "p0"}, {2, "embedded-match", "p0"}, {3, "embedded-three", "p0"}},
/*partitioned=*/false));
ASSERT_OK(embedded_writer->Write(std::move(embedded_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> embedded_progress,
embedded_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
std::vector<std::shared_ptr<DataFileMeta>> embedded_files = NewFiles(embedded_progress);
ASSERT_EQ(1, embedded_files.size());
ASSERT_NE(nullptr, embedded_files[0]->embedded_index);
ASSERT_TRUE(embedded_files[0]->extra_files.empty());
ASSERT_OK_AND_ASSIGN(int64_t embedded_snapshot_id,
Commit(embedded_progress, /*commit_identifier=*/0));
ASSERT_OK(embedded_writer->RefreshCommittedSnapshot(embedded_snapshot_id));
ASSERT_OK(embedded_writer->Close());
options_[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1B";
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> external_writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> external_batch,
MakeBatch(
{{4, "external-four", "p0"}, {5, "external-match", "p0"}, {6, "external-six", "p0"}},
/*partitioned=*/false));
ASSERT_OK(external_writer->Write(std::move(external_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> external_progress,
external_writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
std::vector<std::shared_ptr<DataFileMeta>> external_files = NewFiles(external_progress);
ASSERT_EQ(1, external_files.size());
ASSERT_EQ(nullptr, external_files[0]->embedded_index);
ASSERT_EQ(1, external_files[0]->extra_files.size());
ASSERT_TRUE(external_files[0]->extra_files[0].has_value());
std::string external_index_path =
PathUtil::JoinPath(table_path_, "bucket-0/" + external_files[0]->extra_files[0].value());
ASSERT_OK_AND_ASSIGN(bool external_index_exists,
dir_->GetFileSystem()->Exists(external_index_path));
ASSERT_TRUE(external_index_exists);
ASSERT_OK_AND_ASSIGN(int64_t external_snapshot_id,
Commit(external_progress, /*commit_identifier=*/1));
ASSERT_OK(external_writer->RefreshCommittedSnapshot(external_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch({{7, "memory-seven", "p0"}}, /*partitioned=*/false));
ASSERT_OK(external_writer->Write(std::move(memory_batch)));
const std::string embedded_match = "embedded-match";
const std::string external_match = "external-match";
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<Predicate> predicate,
PredicateBuilder::Or(
{PredicateBuilder::Equal(
/*field_index=*/1, /*field_name=*/"payload", FieldType::STRING,
Literal(FieldType::STRING, embedded_match.data(), embedded_match.size())),
PredicateBuilder::Equal(
/*field_index=*/1, /*field_name=*/"payload", FieldType::STRING,
Literal(FieldType::STRING, external_match.data(), external_match.size()))}));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan, CreatePlan(realtime_context, predicate));
bool has_realtime_split = false;
for (const std::shared_ptr<Split>& split : plan->Splits()) {
has_realtime_split |= std::dynamic_pointer_cast<RealtimeSplit>(split) != nullptr;
}
ASSERT_TRUE(has_realtime_split);
// Residual filtering is disabled. Batch statistics remove the non-matching memory batch, while
// the absence of the other disk rows verifies that both bitmap index storage forms are used.
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan, realtime_context, predicate,
/*enable_predicate_filter=*/false));
std::sort(actual_rows.begin(), actual_rows.end());
ASSERT_EQ((std::vector<Row>{{2, "embedded-match", "p0"}, {5, "external-match", "p0"}}),
actual_rows);
ASSERT_OK(PrepareAndClose(external_writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestCommitOrdersPreparedOffsetRanges) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer, CreateRealtimeWriter());
std::vector<Row> first_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch(first_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, commits.size());
ASSERT_EQ(OffsetRange(0, 3), commits[0].offset_range);
std::vector<Row> second_rows = MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(second_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(second_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, second_commits.size());
ASSERT_EQ(OffsetRange(3, 5), second_commits[0].offset_range);
commits.push_back(std::move(second_commits[0]));
std::reverse(commits.begin(), commits.end());
ASSERT_OK(Commit(commits, /*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap committed_offsets, ReadCommittedOffsets());
ASSERT_EQ(5, committed_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_OK(writer->Close());
std::vector<Row> expected_rows = first_rows;
expected_rows.insert(expected_rows.end(), second_rows.begin(), second_rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows());
ASSERT_EQ(expected_rows, actual_rows);
}
TEST_F(RealtimeWriteInteTest, TestCommitWithProgressRetryReturnsLatestSnapshot) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer, CreateRealtimeWriter());
std::vector<Row> expected_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(expected_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t first_snapshot_id, Commit(commits, /*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t retry_snapshot_id, Commit(commits, /*commit_identifier=*/0));
ASSERT_EQ(first_snapshot_id, retry_snapshot_id);
ASSERT_OK(writer->RefreshCommittedSnapshot(first_snapshot_id));
std::vector<Row> second_rows = MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(second_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(second_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(int64_t second_snapshot_id,
Commit(second_commits, /*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(retry_snapshot_id, Commit(commits, /*commit_identifier=*/0));
ASSERT_EQ(second_snapshot_id, retry_snapshot_id);
ASSERT_NE(first_snapshot_id, retry_snapshot_id);
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap committed_offsets, ReadCommittedOffsets());
ASSERT_EQ(5, committed_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
expected_rows.insert(expected_rows.end(), second_rows.begin(), second_rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows());
ASSERT_EQ(expected_rows, actual_rows);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestCommitWithProgressRejectsCoveredRangesFromAnotherUser) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer, CreateRealtimeWriter());
std::vector<Row> expected_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(expected_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK(Commit(commits, /*commit_identifier=*/0));
CommitContextBuilder builder(table_path_, "another_realtime_commit_user");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<CommitContext> context,
builder.SetOptions(options_).Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreCommit> commit,
FileStoreCommit::Create(std::move(context)));
ASSERT_NOK_WITH_MSG(commit->CommitWithProgress(commits, /*commit_identifier=*/0,
/*watermark=*/std::nullopt),
"another commit user or identifier");
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows());
ASSERT_EQ(expected_rows, actual_rows);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRealtimeWriteAcrossAppendCompaction) {
options_[Options::TARGET_FILE_ROW_NUM] = "2";
options_[Options::COMPACTION_MIN_FILE_NUM] = "2";
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> first_rows;
for (int64_t first_id = 0; first_id < 5; first_id += 2) {
std::vector<Row> rows =
MakeRows(first_id, std::min<int64_t>(2, 5 - first_id), /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
first_rows.insert(first_rows.end(), rows.begin(), rows.end());
}
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, first_commits.size());
ASSERT_EQ(OffsetRange(0, 5), first_commits[0].offset_range);
std::shared_ptr<CommitMessageImpl> first_commit_message =
std::dynamic_pointer_cast<CommitMessageImpl>(first_commits[0].commit_message);
ASSERT_NE(nullptr, first_commit_message);
ASSERT_EQ(3, first_commit_message->GetNewFilesIncrement().NewFiles().size());
ASSERT_OK_AND_ASSIGN(int64_t first_snapshot_id, Commit(first_commits, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(first_snapshot_id));
ASSERT_OK_AND_ASSIGN(Snapshot compact_snapshot, CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
/*commit_identifier=*/1));
ASSERT_EQ(Snapshot::CommitKind::Compact(), compact_snapshot.GetCommitKind());
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap compacted_offsets, ReadCommittedOffsets());
ASSERT_EQ(5, compacted_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_OK(writer->RefreshCommittedSnapshot(compact_snapshot.Id()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_compaction, ReadRows(realtime_context));
ASSERT_EQ(first_rows, rows_after_compaction);
std::vector<Row> second_rows = MakeRows(/*first_id=*/5, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(second_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(second_batch)));
std::vector<Row> expected_rows = first_rows;
expected_rows.insert(expected_rows.end(), second_rows.begin(), second_rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_with_building_memory, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_with_building_memory);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
ASSERT_EQ(1, second_commits.size());
ASSERT_EQ(OffsetRange(5, 7), second_commits[0].offset_range);
ASSERT_OK_AND_ASSIGN(int64_t final_snapshot_id,
Commit(second_commits, /*commit_identifier=*/2));
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap final_offsets, ReadCommittedOffsets());
ASSERT_EQ(7, final_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_OK_AND_ASSIGN(std::vector<Row> final_rows_before_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, final_rows_before_refresh);
ASSERT_OK(writer->RefreshCommittedSnapshot(final_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> final_rows_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, final_rows_after_refresh);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestPkDvExternalBitmapCanEliminateEntireDiskFile) {
options_[Options::FILE_FORMAT] = "parquet";
options_[Options::DELETION_VECTORS_ENABLED] = "true";
options_[Options::FILE_INDEX_READ_ENABLED] = "true";
options_["file-index.bitmap.columns"] = "payload";
options_[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1B";
options_[Options::REALTIME_STORE_STATS_MODE] = "full";
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
// "middle" is inside the file's min/max range but absent from its bitmap index. Keeping the
// index external makes scan planning retain the file and lets the merge reader eliminate it.
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeBatch({{1, "aaa", "p0"}, {2, "zzz", "p0"}}, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t disk_snapshot_id, Commit(disk_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(disk_snapshot_id));
ASSERT_OK_AND_ASSIGN(Snapshot compact_snapshot, CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
/*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(compact_snapshot.Id()));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch({{3, "middle", "p0"}}, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(memory_batch)));
const std::string middle = "middle";
const std::shared_ptr<Predicate> predicate = PredicateBuilder::Equal(
/*field_index=*/1, /*field_name=*/"payload", FieldType::STRING,
Literal(FieldType::STRING, middle.data(), middle.size()));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan, CreatePlan(realtime_context, predicate));
ASSERT_EQ(1, plan->Splits().size());
std::shared_ptr<RealtimeSplit> realtime_split =
std::dynamic_pointer_cast<RealtimeSplit>(plan->Splits()[0]);
ASSERT_NE(nullptr, realtime_split);
size_t external_high_level_file_count = 0;
for (const std::shared_ptr<Split>& split : realtime_split->DiskSplits()) {
std::shared_ptr<DataSplitImpl> data_split = std::dynamic_pointer_cast<DataSplitImpl>(split);
ASSERT_NE(nullptr, data_split);
for (const std::shared_ptr<DataFileMeta>& file : data_split->DataFiles()) {
if (file->level > 0 && file->embedded_index == nullptr && !file->extra_files.empty() &&
file->extra_files[0].has_value()) {
++external_high_level_file_count;
}
}
}
ASSERT_EQ(1, external_high_level_file_count);
// The external bitmap removes the only disk reader. The in-memory matching row must still be
// returned, and constructing the empty disk run must not rely on positional file/reader pairs.
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan, realtime_context, predicate,
/*enable_predicate_filter=*/false));
ASSERT_EQ((std::vector<Row>{{3, "middle", "p0"}}), actual_rows);
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestPkDvRealtimeReadUsesEmbeddedAndExternalBitmapIndexes) {
options_[Options::FILE_FORMAT] = "parquet";
options_[Options::FILE_COMPRESSION] = "none";
options_[Options::DELETION_VECTORS_ENABLED] = "true";
options_["file-index.bitmap.columns"] = "payload";
options_[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1MB";
options_[Options::REALTIME_STORE_STATS_MODE] = "full";
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> embedded_writer,
CreateRealtimeWriter(realtime_context));
// Keep the max-level file substantially larger than the later L0 files. Otherwise the
// size-ratio picker may rewrite all levels together instead of producing a deletion vector.
// Put the padding in a non-indexed column so the data file is much larger than both L0 files
// without making the embedded bitmap index itself large.
const std::string first_padding(256 * 1024, 'X');
const std::string third_padding(256 * 1024, 'Y');
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> base_batch,
MakeBatch({{1, "high-one", first_padding},
{2, "high-match", "p0"},
{3, "high-old-three", third_padding}},
/*partitioned=*/false));
ASSERT_OK(embedded_writer->Write(std::move(base_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> base_progress,
embedded_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
std::vector<std::shared_ptr<DataFileMeta>> base_files = NewFiles(base_progress);
ASSERT_EQ(1, base_files.size());
ASSERT_NE(nullptr, base_files[0]->embedded_index);
ASSERT_TRUE(base_files[0]->extra_files.empty());
ASSERT_OK_AND_ASSIGN(int64_t base_snapshot_id, Commit(base_progress, /*commit_identifier=*/0));
ASSERT_OK(embedded_writer->RefreshCommittedSnapshot(base_snapshot_id));
ASSERT_OK_AND_ASSIGN(Snapshot full_compact_snapshot,
CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
/*commit_identifier=*/1));
ASSERT_OK(embedded_writer->RefreshCommittedSnapshot(full_compact_snapshot.Id()));
ASSERT_OK(embedded_writer->Close());
options_[Options::FILE_INDEX_IN_MANIFEST_THRESHOLD] = "1B";
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> external_writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> external_batch,
MakeBatch({{3, "external-current-three", "p0"},
{4, "external-four", "p0"},
{5, "external-match", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER,
RecordBatch::RowKind::INSERT, RecordBatch::RowKind::INSERT}));
ASSERT_OK(external_writer->Write(std::move(external_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> external_progress,
external_writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
std::vector<std::shared_ptr<DataFileMeta>> external_files = NewFiles(external_progress);
ASSERT_EQ(1, external_files.size());
ASSERT_EQ(0, external_files[0]->level);
ASSERT_EQ(nullptr, external_files[0]->embedded_index);
ASSERT_EQ(1, external_files[0]->extra_files.size());
ASSERT_TRUE(external_files[0]->extra_files[0].has_value());
std::string external_index_path =
PathUtil::JoinPath(table_path_, "bucket-0/" + external_files[0]->extra_files[0].value());
ASSERT_OK_AND_ASSIGN(bool external_index_exists,
dir_->GetFileSystem()->Exists(external_index_path));
ASSERT_TRUE(external_index_exists);
ASSERT_OK_AND_ASSIGN(int64_t external_snapshot_id,
Commit(external_progress, /*commit_identifier=*/2));
ASSERT_OK(external_writer->RefreshCommittedSnapshot(external_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_external_batch,
MakeBatch({{6, "external-six", "p0"}}, /*partitioned=*/false));
ASSERT_OK(external_writer->Write(std::move(second_external_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_external_progress,
external_writer->PrepareCommitWithProgress(/*commit_identifier=*/3));
ASSERT_OK_AND_ASSIGN(int64_t second_external_snapshot_id,
Commit(second_external_progress, /*commit_identifier=*/3));
ASSERT_OK(external_writer->RefreshCommittedSnapshot(second_external_snapshot_id));
ASSERT_OK_AND_ASSIGN(Snapshot dv_compact_snapshot,
CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
/*commit_identifier=*/4,
/*full_compaction=*/false));
ASSERT_OK(external_writer->RefreshCommittedSnapshot(dv_compact_snapshot.Id()));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch({{7, "memory-seven", "p0"}}, /*partitioned=*/false));
ASSERT_OK(external_writer->Write(std::move(memory_batch)));
const std::string high_match = "high-match";
const std::string external_match = "external-match";
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<Predicate> predicate,
PredicateBuilder::Or(
{PredicateBuilder::Equal(
/*field_index=*/1, /*field_name=*/"payload", FieldType::STRING,
Literal(FieldType::STRING, high_match.data(), high_match.size())),
PredicateBuilder::Equal(
/*field_index=*/1, /*field_name=*/"payload", FieldType::STRING,
Literal(FieldType::STRING, external_match.data(), external_match.size()))}));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan, CreatePlan(realtime_context, predicate));
ASSERT_EQ(1, plan->Splits().size());
std::shared_ptr<RealtimeSplit> realtime_split =
std::dynamic_pointer_cast<RealtimeSplit>(plan->Splits()[0]);
ASSERT_NE(nullptr, realtime_split);
ASSERT_OK_AND_ASSIGN(DiskFileLayout layout, InspectDiskFiles(realtime_split->DiskSplits()));
ASSERT_TRUE(layout.has_high_level_file);
ASSERT_TRUE(layout.has_high_level_deletion_vector);
bool has_embedded_high_level_index_with_dv = false;
bool has_external_high_level_index = false;
for (const std::shared_ptr<Split>& split : realtime_split->DiskSplits()) {
std::shared_ptr<DataSplitImpl> data_split = std::dynamic_pointer_cast<DataSplitImpl>(split);
ASSERT_NE(nullptr, data_split);
const std::vector<std::shared_ptr<DataFileMeta>>& files = data_split->DataFiles();
const std::vector<std::optional<DeletionFile>>& deletion_files =
data_split->DeletionFiles();
for (size_t i = 0; i < files.size(); ++i) {
has_embedded_high_level_index_with_dv |=
files[i]->level > 0 && files[i]->embedded_index != nullptr &&
!deletion_files.empty() && deletion_files[i].has_value();
if (files[i]->level > 0 && files[i]->embedded_index == nullptr &&
!files[i]->extra_files.empty() && files[i]->extra_files[0].has_value()) {
has_external_high_level_index = true;
std::string index_path =
PathUtil::JoinPath(data_split->BucketPath(), files[i]->extra_files[0].value());
ASSERT_OK_AND_ASSIGN(bool index_exists, dir_->GetFileSystem()->Exists(index_path));
ASSERT_TRUE(index_exists);
}
}
}
ASSERT_TRUE(has_embedded_high_level_index_with_dv);
ASSERT_TRUE(has_external_high_level_index);
// Only primary-key predicates are sent to L0 and memory in PK MOR reads. The non-key predicate
// still reaches both high-level bitmap indexes, while the unfiltered memory row remains.
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan, realtime_context, predicate,
/*enable_predicate_filter=*/false));
std::sort(actual_rows.begin(), actual_rows.end());
ASSERT_EQ((std::vector<Row>{
{2, "high-match", "p0"}, {5, "external-match", "p0"}, {7, "memory-seven", "p0"}}),
actual_rows);
ASSERT_OK(PrepareAndClose(external_writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestPkDvPredicateAcrossHighLevelLevel0AndMemory) {
options_[Options::FILE_FORMAT] = "parquet";
options_[Options::DELETION_VECTORS_ENABLED] = "true";
options_[Options::REALTIME_STORE_STATS_MODE] = "full";
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
const std::string matching_payload = "match-" + std::string(2048, 'X');
const std::vector<Row> base_rows = {{1, matching_payload, "p0"},
{2, matching_payload, "p0"},
{3, matching_payload, "p0"},
{4, "base-four", "p0"},
{5, "base-five", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> base_batch,
MakeBatch(base_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(base_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> base_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t base_snapshot_id, Commit(base_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(base_snapshot_id));
ASSERT_OK_AND_ASSIGN(Snapshot full_compact_snapshot,
CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
/*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(full_compact_snapshot.Id()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_full_compaction, ReadRows(realtime_context));
ASSERT_EQ(base_rows, rows_after_full_compaction);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> dv_update_batch,
MakeBatch({Row{5, "dv-current-five", "p0"}}, /*partitioned=*/false,
/*bucket=*/0, {RecordBatch::RowKind::UPDATE_AFTER}));
ASSERT_OK(writer->Write(std::move(dv_update_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> dv_update_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
ASSERT_OK_AND_ASSIGN(int64_t dv_update_snapshot_id,
Commit(dv_update_progress, /*commit_identifier=*/2));
ASSERT_OK(writer->RefreshCommittedSnapshot(dv_update_snapshot_id));
ASSERT_OK_AND_ASSIGN(Snapshot dv_compact_snapshot,
CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
/*commit_identifier=*/3,
/*full_compaction=*/false));
ASSERT_OK(writer->RefreshCommittedSnapshot(dv_compact_snapshot.Id()));
const std::vector<Row> expected_high_level_rows = {{1, matching_payload, "p0"},
{2, matching_payload, "p0"},
{3, matching_payload, "p0"},
{4, "base-four", "p0"},
{5, "dv-current-five", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> high_level_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_high_level_rows, high_level_rows);
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> level0_batch,
MakeBatch({Row{1, "level0-current-one", "p0"}, Row{2, "deleted-two", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER, RecordBatch::RowKind::DELETE}));
ASSERT_OK(writer->Write(std::move(level0_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> level0_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/4));
ASSERT_OK_AND_ASSIGN(int64_t level0_snapshot_id,
Commit(level0_progress, /*commit_identifier=*/4));
ASSERT_OK(writer->RefreshCommittedSnapshot(level0_snapshot_id));
const std::vector<Row> expected_level0_rows = {{1, "level0-current-one", "p0"},
{3, matching_payload, "p0"},
{4, "base-four", "p0"},
{5, "dv-current-five", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> level0_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_level0_rows, level0_rows);
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> memory_batch,
MakeBatch({Row{3, "memory-current-three", "p0"}, Row{7, matching_payload, "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER, RecordBatch::RowKind::INSERT}));
ASSERT_OK(writer->Write(std::move(memory_batch)));
const std::vector<Row> expected_memory_rows = {{1, "level0-current-one", "p0"},
{3, "memory-current-three", "p0"},
{4, "base-four", "p0"},
{5, "dv-current-five", "p0"},
{7, matching_payload, "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> memory_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_memory_rows, memory_rows);
std::shared_ptr<Predicate> predicate = PredicateBuilder::Equal(
/*field_index=*/1, /*field_name=*/"payload", FieldType::STRING,
Literal(FieldType::STRING, matching_payload.data(), matching_payload.size()));
options_[Options::SCAN_SNAPSHOT_ID] = std::to_string(level0_snapshot_id);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan, CreatePlan(realtime_context, predicate));
ASSERT_EQ(level0_snapshot_id, plan->SnapshotId());
ASSERT_EQ(1, plan->Splits().size());
std::shared_ptr<RealtimeSplit> realtime_split =
std::dynamic_pointer_cast<RealtimeSplit>(plan->Splits()[0]);
ASSERT_NE(nullptr, realtime_split);
ASSERT_GT(realtime_split->MemoryEndOffset(), realtime_split->CommittedEndOffset());
ASSERT_OK_AND_ASSIGN(DiskFileLayout layout, InspectDiskFiles(realtime_split->DiskSplits()));
ASSERT_TRUE(layout.has_level0_file);
ASSERT_TRUE(layout.has_high_level_file);
ASSERT_TRUE(layout.has_high_level_deletion_vector);
// Predicate pushdown may return false-positive candidates, but a newer unfiltered L0 or
// memory record must still suppress every matching old version from the high levels.
ASSERT_OK_AND_ASSIGN(std::vector<Row> candidates, ReadRows(plan, realtime_context, predicate,
/*enable_predicate_filter=*/false));
ASSERT_EQ((std::vector<Row>{{1, "level0-current-one", "p0"},
{3, "memory-current-three", "p0"},
{4, "base-four", "p0"},
{7, matching_payload, "p0"}}),
candidates);
std::shared_ptr<Predicate> matching_key_predicate = PredicateBuilder::Equal(
/*field_index=*/0, /*field_name=*/"id", FieldType::BIGINT, Literal(int64_t{1}));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> matching_key_plan,
CreatePlan(realtime_context, matching_key_predicate));
ASSERT_OK_AND_ASSIGN(std::vector<Row> matching_key_candidates,
ReadRows(matching_key_plan, realtime_context, matching_key_predicate,
/*enable_predicate_filter=*/true));
ASSERT_EQ((std::vector<Row>{{1, "level0-current-one", "p0"}}), matching_key_candidates);
std::shared_ptr<Predicate> deleted_key_predicate = PredicateBuilder::Equal(
/*field_index=*/0, /*field_name=*/"id", FieldType::BIGINT, Literal(int64_t{2}));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> deleted_key_plan,
CreatePlan(realtime_context, deleted_key_predicate));
ASSERT_OK_AND_ASSIGN(std::vector<Row> deleted_key_candidates,
ReadRows(deleted_key_plan, realtime_context, deleted_key_predicate,
/*enable_predicate_filter=*/true));
ASSERT_TRUE(deleted_key_candidates.empty());
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestPkDvPushesOnlyPrimaryKeyPredicatesIntoLevel0AndMemory) {
options_[Options::FILE_FORMAT] = "parquet";
options_[Options::DELETION_VECTORS_ENABLED] = "true";
options_[Options::REALTIME_STORE_STATS_MODE] = "full";
options_[Options::WRITE_BATCH_SIZE] = "1";
options_["parquet.page.size"] = "1";
options_["parquet.write.enable-page-index"] = "true";
options_["parquet.write.max-row-group-length"] = "1";
options_["parquet.read.enable-page-index-filter"] = "true";
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
auto write_one = [&](const Row& row, RecordBatch::RowKind row_kind) -> Status {
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<RecordBatch> batch,
MakeBatch({row}, /*partitioned=*/false, /*bucket=*/0, {row_kind}));
return writer->Write(std::move(batch));
};
ASSERT_OK(write_one({1, "high-match", "p0"}, RecordBatch::RowKind::INSERT));
ASSERT_OK(write_one({2, "high-other", "p0"}, RecordBatch::RowKind::INSERT));
ASSERT_OK(write_one({3, "high-old", "p0"}, RecordBatch::RowKind::INSERT));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> base_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t base_snapshot_id, Commit(base_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(base_snapshot_id));
ASSERT_OK_AND_ASSIGN(Snapshot full_compact_snapshot,
CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
/*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(full_compact_snapshot.Id()));
ASSERT_OK(write_one({3, "high-current", "p0"}, RecordBatch::RowKind::UPDATE_AFTER));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> update_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
ASSERT_OK_AND_ASSIGN(int64_t update_snapshot_id,
Commit(update_progress, /*commit_identifier=*/2));
ASSERT_OK(writer->RefreshCommittedSnapshot(update_snapshot_id));
ASSERT_OK_AND_ASSIGN(Snapshot dv_compact_snapshot,
CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
/*commit_identifier=*/3,
/*full_compaction=*/false));
ASSERT_OK(writer->RefreshCommittedSnapshot(dv_compact_snapshot.Id()));
ASSERT_OK(write_one({4, "level0-match", "p0"}, RecordBatch::RowKind::INSERT));
ASSERT_OK(write_one({5, "level0-other", "p0"}, RecordBatch::RowKind::INSERT));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> level0_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/4));
ASSERT_OK_AND_ASSIGN(int64_t level0_snapshot_id,
Commit(level0_progress, /*commit_identifier=*/4));
ASSERT_OK(writer->RefreshCommittedSnapshot(level0_snapshot_id));
ASSERT_OK(write_one({6, "memory-match", "p0"}, RecordBatch::RowKind::INSERT));
ASSERT_OK(write_one({7, "memory-other", "p0"}, RecordBatch::RowKind::INSERT));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> unfiltered_plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_EQ(1, unfiltered_plan->Splits().size());
std::shared_ptr<RealtimeSplit> realtime_split =
std::dynamic_pointer_cast<RealtimeSplit>(unfiltered_plan->Splits()[0]);
ASSERT_NE(nullptr, realtime_split);
ASSERT_GT(realtime_split->MemoryEndOffset(), realtime_split->CommittedEndOffset());
ASSERT_OK_AND_ASSIGN(DiskFileLayout layout, InspectDiskFiles(realtime_split->DiskSplits()));
ASSERT_TRUE(layout.has_level0_file);
ASSERT_TRUE(layout.has_high_level_file);
ASSERT_TRUE(layout.has_high_level_deletion_vector);
// Keep residual filtering disabled so candidate rows expose the actual source pushdown:
// high-level files receive the full predicate, while L0 and memory receive only PK predicates.
auto read_candidates =
[&](const std::shared_ptr<Predicate>& predicate) -> Result<std::vector<Row>> {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Plan> plan, CreatePlan(realtime_context, predicate));
return ReadRows(plan, realtime_context, predicate,
/*enable_predicate_filter=*/false);
};
std::shared_ptr<Predicate> level0_key_predicate = PredicateBuilder::Equal(
/*field_index=*/0, /*field_name=*/"id", FieldType::BIGINT, Literal(int64_t{4}));
ASSERT_OK_AND_ASSIGN(std::vector<Row> level0_key_candidates,
read_candidates(level0_key_predicate));
ASSERT_EQ((std::vector<Row>{{4, "level0-match", "p0"}}), level0_key_candidates);
std::shared_ptr<Predicate> memory_key_predicate = PredicateBuilder::Equal(
/*field_index=*/0, /*field_name=*/"id", FieldType::BIGINT, Literal(int64_t{6}));
ASSERT_OK_AND_ASSIGN(std::vector<Row> memory_key_candidates,
read_candidates(memory_key_predicate));
ASSERT_EQ((std::vector<Row>{{6, "memory-match", "p0"}}), memory_key_candidates);
const std::string high_match = "high-match";
std::shared_ptr<Predicate> non_key_predicate = PredicateBuilder::Equal(
/*field_index=*/1, /*field_name=*/"payload", FieldType::STRING,
Literal(FieldType::STRING, high_match.data(), high_match.size()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> non_key_candidates, read_candidates(non_key_predicate));
ASSERT_EQ((std::vector<Row>{{1, "high-match", "p0"},
{4, "level0-match", "p0"},
{5, "level0-other", "p0"},
{6, "memory-match", "p0"},
{7, "memory-other", "p0"}}),
non_key_candidates);
ASSERT_OK_AND_ASSIGN(
std::shared_ptr<Predicate> mixed_predicate,
PredicateBuilder::And(
{PredicateBuilder::GreaterOrEqual(/*field_index=*/0, /*field_name=*/"id",
FieldType::BIGINT, Literal(int64_t{4})),
PredicateBuilder::Equal(
/*field_index=*/1, /*field_name=*/"payload", FieldType::STRING,
Literal(FieldType::STRING, high_match.data(), high_match.size()))}));
ASSERT_OK_AND_ASSIGN(std::vector<Row> mixed_candidates, read_candidates(mixed_predicate));
ASSERT_EQ((std::vector<Row>{{4, "level0-match", "p0"},
{5, "level0-other", "p0"},
{6, "memory-match", "p0"},
{7, "memory-other", "p0"}}),
mixed_candidates);
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestPkDvMergesDuplicateKeysAcrossLevel0RunsAndMemory) {
options_[Options::FILE_FORMAT] = "parquet";
options_[Options::DELETION_VECTORS_ENABLED] = "true";
CreatePkTable();
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_level0_batch,
MakeBatch({Row{1, "level0-old-one", "p0"}, Row{2, "level0-old-two", "p0"},
Row{3, "level0-stable-three", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(first_level0_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_level0_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t first_level0_snapshot_id,
Commit(first_level0_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(first_level0_snapshot_id));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> second_level0_batch,
MakeBatch({Row{1, "level0-new-one", "p0"}, Row{2, "level0-deleted-two", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER, RecordBatch::RowKind::DELETE}));
ASSERT_OK(writer->Write(std::move(second_level0_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_level0_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(int64_t second_level0_snapshot_id,
Commit(second_level0_progress, /*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(second_level0_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch({Row{1, "memory-new-one", "p0"}}, /*partitioned=*/false,
/*bucket=*/0, {RecordBatch::RowKind::UPDATE_AFTER}));
ASSERT_OK(writer->Write(std::move(memory_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_EQ(1, plan->Splits().size());
std::shared_ptr<RealtimeSplit> realtime_split =
std::dynamic_pointer_cast<RealtimeSplit>(plan->Splits()[0]);
ASSERT_NE(nullptr, realtime_split);
size_t level0_file_count = 0;
for (const std::shared_ptr<Split>& disk_split : realtime_split->DiskSplits()) {
std::shared_ptr<DataSplitImpl> data_split =
std::dynamic_pointer_cast<DataSplitImpl>(disk_split);
ASSERT_NE(nullptr, data_split);
for (const std::shared_ptr<DataFileMeta>& file : data_split->DataFiles()) {
if (file->level == 0) {
++level0_file_count;
}
}
}
ASSERT_GE(level0_file_count, 2);
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan, realtime_context));
ASSERT_EQ((std::vector<Row>{{1, "memory-new-one", "p0"}, {3, "level0-stable-three", "p0"}}),
actual_rows);
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestRealtimeOffsetFileLifecycle) {
options_[Options::SNAPSHOT_NUM_RETAINED_MIN] = "1";
options_[Options::SNAPSHOT_NUM_RETAINED_MAX] = "1";
options_[Options::SNAPSHOT_TIME_RETAINED] = "1ms";
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer, CreateRealtimeWriter());
std::vector<Row> first_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch(first_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t first_snapshot_id, Commit(first_commits, /*commit_identifier=*/0));
std::vector<Row> second_rows = MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(second_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(second_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(int64_t second_snapshot_id,
Commit(second_commits, /*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(CoreOptions core_options, CoreOptions::FromMap(options_));
std::shared_ptr<FileSystem> file_system = core_options.GetFileSystem();
SnapshotManager snapshot_manager(file_system, table_path_);
ASSERT_OK_AND_ASSIGN(Snapshot first_snapshot, snapshot_manager.LoadSnapshot(first_snapshot_id));
ASSERT_OK_AND_ASSIGN(Snapshot second_snapshot,
snapshot_manager.LoadSnapshot(second_snapshot_id));
std::optional<std::string> first_offsets_path =
RealtimeCommitProperties::GetOffsetsPath(first_snapshot);
std::optional<std::string> second_offsets_path =
RealtimeCommitProperties::GetOffsetsPath(second_snapshot);
ASSERT_TRUE(first_offsets_path);
ASSERT_TRUE(second_offsets_path);
ASSERT_NE(first_offsets_path, second_offsets_path);
std::string orphan_offsets_path = PathUtil::JoinPath(
RealtimeCommitProperties::OffsetsDirectory(table_path_, core_options.GetBranch()),
"orphan.offsets");
ASSERT_OK(file_system->WriteFile(orphan_offsets_path, "orphan", /*overwrite=*/false));
CleanContextBuilder clean_builder(table_path_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<CleanContext> clean_context,
clean_builder.WithFileSystem(file_system)
.WithOlderThanMs(std::numeric_limits<int64_t>::max())
.Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<OrphanFilesCleaner> cleaner,
OrphanFilesCleaner::Create(std::move(clean_context)));
ASSERT_OK_AND_ASSIGN(std::set<std::string> cleaned_paths, cleaner->Clean());
ASSERT_EQ(std::set<std::string>({orphan_offsets_path}), cleaned_paths);
ASSERT_OK_AND_ASSIGN(bool first_offsets_exist, file_system->Exists(first_offsets_path.value()));
ASSERT_TRUE(first_offsets_exist);
ASSERT_OK_AND_ASSIGN(bool second_offsets_exist,
file_system->Exists(second_offsets_path.value()));
ASSERT_TRUE(second_offsets_exist);
ASSERT_OK_AND_ASSIGN(int32_t expired_snapshots, ExpireSnapshots());
ASSERT_EQ(1, expired_snapshots);
ASSERT_OK_AND_ASSIGN(first_offsets_exist, file_system->Exists(first_offsets_path.value()));
ASSERT_FALSE(first_offsets_exist);
ASSERT_OK_AND_ASSIGN(second_offsets_exist, file_system->Exists(second_offsets_path.value()));
ASSERT_TRUE(second_offsets_exist);
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap committed_offsets,
RealtimeCommitProperties::ReadOffsets(second_snapshot, file_system));
ASSERT_EQ(5, committed_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestCompactionSnapshotRetainsSharedOffsetFile) {
options_[Options::TARGET_FILE_ROW_NUM] = "2";
options_[Options::COMPACTION_MIN_FILE_NUM] = "2";
options_[Options::SNAPSHOT_NUM_RETAINED_MIN] = "1";
options_[Options::SNAPSHOT_NUM_RETAINED_MAX] = "1";
options_[Options::SNAPSHOT_TIME_RETAINED] = "1ms";
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer, CreateRealtimeWriter());
for (int64_t first_id = 0; first_id < 5; first_id += 2) {
std::vector<Row> rows =
MakeRows(first_id, std::min<int64_t>(2, 5 - first_id), /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
}
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t realtime_snapshot_id, Commit(commits, /*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(Snapshot compact_snapshot, CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
/*commit_identifier=*/1));
ASSERT_EQ(Snapshot::CommitKind::Compact(), compact_snapshot.GetCommitKind());
ASSERT_OK_AND_ASSIGN(CoreOptions core_options, CoreOptions::FromMap(options_));
std::shared_ptr<FileSystem> file_system = core_options.GetFileSystem();
SnapshotManager snapshot_manager(file_system, table_path_);
ASSERT_OK_AND_ASSIGN(Snapshot realtime_snapshot,
snapshot_manager.LoadSnapshot(realtime_snapshot_id));
std::optional<std::string> realtime_offsets_path =
RealtimeCommitProperties::GetOffsetsPath(realtime_snapshot);
std::optional<std::string> compact_offsets_path =
RealtimeCommitProperties::GetOffsetsPath(compact_snapshot);
ASSERT_TRUE(realtime_offsets_path);
ASSERT_TRUE(compact_offsets_path);
ASSERT_EQ(realtime_offsets_path, compact_offsets_path);
ASSERT_OK_AND_ASSIGN(int32_t expired_snapshots, ExpireSnapshots());
ASSERT_EQ(1, expired_snapshots);
ASSERT_OK_AND_ASSIGN(bool offsets_exist, file_system->Exists(compact_offsets_path.value()));
ASSERT_TRUE(offsets_exist);
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap committed_offsets,
RealtimeCommitProperties::ReadOffsets(compact_snapshot, file_system));
ASSERT_EQ(5, committed_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestReadMemoryBeforePrepareCommit) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> rows = MakeRows(/*first_id=*/0, /*count=*/10, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_EQ(1, plan->Splits().size());
std::shared_ptr<RealtimeSplit> realtime_split =
std::dynamic_pointer_cast<RealtimeSplit>(plan->Splits()[0]);
ASSERT_NE(nullptr, realtime_split);
ASSERT_EQ(RealtimeSplit::kCurrentVersion, realtime_split->Version());
ASSERT_FALSE(realtime_split->SnapshotId().has_value());
ASSERT_EQ(0, realtime_split->CommittedEndOffset());
ASSERT_EQ(10, realtime_split->MemoryEndOffset());
ASSERT_FALSE(realtime_split->OpaqueTicket().empty());
ASSERT_NOK_WITH_MSG(ReadRows(plan, /*realtime_context=*/nullptr),
"requires a real-time context");
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan, realtime_context));
ASSERT_EQ(rows, actual_rows);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContextImpl> realtime_context_impl,
RealtimeContextImpl::Cast(realtime_context));
ASSERT_NOK_WITH_MSG(realtime_context_impl->ResolveReadView(realtime_split->OpaqueTicket()),
"ticket does not exist or has expired");
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestPlanExcludesRowsWrittenAfterMemoryEndOffset) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> first_rows = MakeRows(/*first_id=*/0, /*count=*/10, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch(first_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> first_plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_EQ(1, first_plan->Splits().size());
std::shared_ptr<RealtimeSplit> first_realtime_split =
std::dynamic_pointer_cast<RealtimeSplit>(first_plan->Splits()[0]);
ASSERT_NE(nullptr, first_realtime_split);
ASSERT_EQ(10, first_realtime_split->MemoryEndOffset());
std::vector<Row> second_rows = MakeRows(/*first_id=*/10, /*count=*/5, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(second_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(second_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<Row> first_actual_rows,
ReadRows(first_plan, realtime_context));
ASSERT_EQ(first_rows, first_actual_rows);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> second_plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_EQ(1, second_plan->Splits().size());
std::shared_ptr<RealtimeSplit> second_realtime_split =
std::dynamic_pointer_cast<RealtimeSplit>(second_plan->Splits()[0]);
ASSERT_NE(nullptr, second_realtime_split);
ASSERT_EQ(15, second_realtime_split->MemoryEndOffset());
std::vector<Row> expected_rows = first_rows;
expected_rows.insert(expected_rows.end(), second_rows.begin(), second_rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> second_actual_rows,
ReadRows(second_plan, realtime_context));
ASSERT_EQ(expected_rows, second_actual_rows);
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestReadFailsAfterRealtimeSplitTicketExpires) {
options_[Options::REALTIME_READ_VIEW_TTL] = "10 ms";
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
std::this_thread::sleep_for(std::chrono::milliseconds(50));
ASSERT_NOK_WITH_MSG(ReadRows(plan, realtime_context), "ticket does not exist or has expired");
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestSuccessfulReaderCreationConsumesRealtimeSplitTicket) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ReadContextBuilder read_builder(table_path_);
read_builder.SetOptions(options_)
.SetReadFieldNames({"id", "payload", "pt"})
.WithRealtimeContext(realtime_context)
.WithMemoryPool(pool_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> read_context, read_builder.Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableRead> table_read,
TableRead::Create(std::move(read_context)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<BatchReader> first_reader,
table_read->CreateReader(plan->Splits()));
first_reader->Close();
first_reader.reset();
ASSERT_NOK_WITH_MSG(table_read->CreateReader(plan->Splits()),
"ticket does not exist or has expired");
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestFailedReaderCreationPreservesRealtimeSplitTicket) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_EQ(1, plan->Splits().size());
ReadContextBuilder read_builder(table_path_);
read_builder.SetOptions(options_)
.SetReadFieldNames({"id", "payload", "pt"})
.SetPredicate(std::make_shared<UnsupportedPredicate>())
.EnablePredicateFilter(true)
.WithRealtimeContext(realtime_context)
.WithMemoryPool(pool_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> read_context, read_builder.Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableRead> table_read,
TableRead::Create(std::move(read_context)));
ASSERT_NOK_WITH_MSG(table_read->CreateReader(plan->Splits()),
"cannot cast predicate unsupported");
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan, realtime_context));
ASSERT_EQ(rows, actual_rows);
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestCloseWriterWithBuildingDataInvalidatesContext) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_NOK_WITH_MSG(writer->Close(), "not covered by a successful");
ASSERT_NOK_WITH_MSG(ReadRows(realtime_context), "real-time context cannot be reused");
ASSERT_NOK_WITH_MSG(CreateRealtimeWriter(realtime_context),
"real-time context cannot be reused");
}
TEST_F(RealtimeWriteInteTest, TestCloseWriterWithSealedDataInvalidatesContext) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0"),
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK(writer->Seal());
ASSERT_NOK_WITH_MSG(writer->Close(), "not covered by a successful");
ASSERT_NOK_WITH_MSG(CreateRealtimeWriter(realtime_context),
"real-time context cannot be reused");
}
TEST_F(RealtimeWriteInteTest, TestPinnedPlanRemainsReadableAfterWriterClose) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_OK(PrepareAndClose(writer.get()));
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(plan, realtime_context));
ASSERT_EQ(rows, actual_rows);
}
TEST_F(RealtimeWriteInteTest, TestCloseWriterAllowsContextReuseByLaterWriter) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> first_writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> first_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch(first_rows, /*partitioned=*/false));
ASSERT_OK(first_writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
first_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, commits.size());
ASSERT_EQ(OffsetRange(0, 3), commits[0].offset_range);
ASSERT_OK(first_writer->Close());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> second_writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> second_rows = MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(second_rows, /*partitioned=*/false));
ASSERT_OK(second_writer->Write(std::move(second_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_commits,
second_writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, second_commits.size());
ASSERT_EQ(OffsetRange(3, 5), second_commits[0].offset_range);
commits.push_back(std::move(second_commits[0]));
ASSERT_OK(Commit(commits, /*commit_identifier=*/1));
std::vector<Row> expected_rows = first_rows;
expected_rows.insert(expected_rows.end(), second_rows.begin(), second_rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, actual_rows);
ASSERT_OK(second_writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestReadCommittedDiskAndBuildingMemory) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> disk_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeBatch(disk_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, disk_commits.size());
ASSERT_EQ(OffsetRange(0, 3), disk_commits[0].offset_range);
std::vector<Row> memory_rows = MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch(memory_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(memory_batch)));
ASSERT_OK(Commit(disk_commits, /*commit_identifier=*/0));
std::vector<Row> expected_rows = disk_rows;
expected_rows.insert(expected_rows.end(), memory_rows.begin(), memory_rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, actual_rows);
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestCountReaderDoesNotSupportRealtimeSplit) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_NOK_WITH_MSG(CountRows(plan, realtime_context),
"does not support process-local real-time splits");
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestProjectionAndPredicateForMemoryAndDisk) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::shared_ptr<Predicate> scan_predicate =
PredicateBuilder::GreaterThan(/*field_index=*/0, /*field_name=*/"id", FieldType::BIGINT,
Literal(static_cast<int64_t>(1)));
std::shared_ptr<Predicate> read_predicate =
PredicateBuilder::GreaterThan(/*field_index=*/1, /*field_name=*/"id", FieldType::BIGINT,
Literal(static_cast<int64_t>(1)));
const std::vector<std::string> read_fields = {"payload", "id"};
std::shared_ptr<arrow::DataType> result_type = arrow::struct_(
{arrow::field("_VALUE_KIND", arrow::int8()), arrow::field("payload", arrow::utf8()),
arrow::field("id", arrow::int64())});
std::vector<Row> disk_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeBatch(disk_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> memory_plan,
CreatePlan(realtime_context, scan_predicate));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> memory_result,
ReadPlan(memory_plan, realtime_context, read_fields, read_predicate,
/*enable_predicate_filter=*/true));
std::shared_ptr<arrow::Array> expected_memory =
arrow::ipc::internal::json::ArrayFromJSON(result_type, R"([
[0, "value-2", 2]
])")
.ValueOrDie();
ASSERT_NE(nullptr, memory_result);
ASSERT_TRUE(std::make_shared<arrow::ChunkedArray>(expected_memory)->Equals(*memory_result));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK(Commit(disk_commits, /*commit_identifier=*/0));
std::vector<Row> memory_rows = MakeRows(/*first_id=*/3, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch(memory_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(memory_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> union_plan,
CreatePlan(realtime_context, scan_predicate));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> union_result,
ReadPlan(union_plan, realtime_context, read_fields, read_predicate,
/*enable_predicate_filter=*/true));
std::shared_ptr<arrow::Array> expected_union =
arrow::ipc::internal::json::ArrayFromJSON(result_type, R"([
[0, "value-2", 2],
[0, "value-3", 3],
[0, "value-4", 4],
[0, "value-5", 5]
])")
.ValueOrDie();
ASSERT_NE(nullptr, union_result);
ASSERT_TRUE(std::make_shared<arrow::ChunkedArray>(expected_union)->Equals(*union_result));
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestDiskPredicatePushdownWithoutMemoryFiltering) {
options_[Options::FILE_FORMAT] = "parquet";
options_[Options::WRITE_BATCH_SIZE] = "1";
options_["parquet.page.size"] = "1";
options_["parquet.enable.dictionary"] = "false";
options_["parquet.write.enable-page-index"] = "true";
options_["parquet.read.enable-page-index-filter"] = "true";
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::shared_ptr<Predicate> predicate =
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"id", FieldType::BIGINT,
Literal(static_cast<int64_t>(1)));
std::vector<Row> disk_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeBatch(disk_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK(Commit(disk_commits, /*commit_identifier=*/0));
std::vector<Row> memory_rows = MakeRows(/*first_id=*/3, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch(memory_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(memory_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan, CreatePlan(realtime_context, predicate));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result,
ReadPlan(plan, realtime_context, {"id", "payload", "pt"}, predicate,
/*enable_predicate_filter=*/false));
std::shared_ptr<arrow::DataType> result_type = arrow::struct_(
{arrow::field("_VALUE_KIND", arrow::int8()), arrow::field("id", arrow::int64()),
arrow::field("payload", arrow::utf8()), arrow::field("pt", arrow::utf8())});
std::shared_ptr<arrow::Array> expected =
arrow::ipc::internal::json::ArrayFromJSON(result_type, R"([
[0, 1, "value-1", "p0"],
[0, 3, "value-3", "p0"],
[0, 4, "value-4", "p0"],
[0, 5, "value-5", "p0"]
])")
.ValueOrDie();
ASSERT_NE(nullptr, result);
ASSERT_TRUE(std::make_shared<arrow::ChunkedArray>(expected)->Equals(*result))
<< result->ToString();
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestMemoryBatchStatisticsPredicatePushdown) {
CreateTable(/*partition_keys=*/{});
std::shared_ptr<arrow::DataType> result_type = arrow::struct_(
{arrow::field("_VALUE_KIND", arrow::int8()), arrow::field("id", arrow::int64()),
arrow::field("payload", arrow::utf8()), arrow::field("pt", arrow::utf8())});
auto make_expected = [&](const std::string& json) {
std::shared_ptr<arrow::Array> array =
arrow::ipc::internal::json::ArrayFromJSON(result_type, json).ValueOrDie();
return std::make_shared<arrow::ChunkedArray>(array);
};
std::vector<std::shared_ptr<Predicate>> predicates = {
PredicateBuilder::GreaterThan(/*field_index=*/0, /*field_name=*/"id", FieldType::BIGINT,
Literal(static_cast<int64_t>(100))),
PredicateBuilder::GreaterThan(/*field_index=*/0, /*field_name=*/"id", FieldType::BIGINT,
Literal(static_cast<int64_t>(5))),
PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"id", FieldType::BIGINT,
Literal(static_cast<int64_t>(10))),
};
auto check_candidates =
[&](const std::string& statistics_mode,
const std::vector<std::shared_ptr<arrow::ChunkedArray>>& expected) -> Status {
if (expected.size() != predicates.size()) {
return Status::Invalid("unexpected real-time candidate result count");
}
options_[Options::REALTIME_STORE_STATS_MODE] = statistics_mode;
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
for (int64_t first_id : {0, 10, 20}) {
std::vector<Row> rows = MakeRows(first_id, /*count=*/3, /*partition=*/"p0");
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
PAIMON_RETURN_NOT_OK(writer->Write(std::move(batch)));
}
for (size_t i = 0; i < predicates.size(); ++i) {
const std::shared_ptr<Predicate>& predicate = predicates[i];
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, predicate));
PAIMON_ASSIGN_OR_RAISE(
std::shared_ptr<arrow::ChunkedArray> result,
ReadPlan(plan, realtime_context, {"id", "payload", "pt"}, predicate,
/*enable_predicate_filter=*/false));
std::shared_ptr<arrow::ChunkedArray> actual = result ? result : make_expected("[]");
if (!expected[i]->Equals(*actual)) {
return Status::Invalid("unexpected real-time candidate rows: " +
actual->ToString());
}
}
PAIMON_RETURN_NOT_OK(PrepareAndClose(writer.get()));
return Status::OK();
};
std::shared_ptr<arrow::ChunkedArray> all_rows = make_expected(R"([
[0, 0, "value-0", "p0"],
[0, 1, "value-1", "p0"],
[0, 2, "value-2", "p0"],
[0, 10, "value-10", "p0"],
[0, 11, "value-11", "p0"],
[0, 12, "value-12", "p0"],
[0, 20, "value-20", "p0"],
[0, 21, "value-21", "p0"],
[0, 22, "value-22", "p0"]
])");
ASSERT_OK(check_candidates("none", {all_rows, all_rows, all_rows}));
std::shared_ptr<arrow::ChunkedArray> partially_filtered = make_expected(R"([
[0, 10, "value-10", "p0"],
[0, 11, "value-11", "p0"],
[0, 12, "value-12", "p0"],
[0, 20, "value-20", "p0"],
[0, 21, "value-21", "p0"],
[0, 22, "value-22", "p0"]
])");
std::shared_ptr<arrow::ChunkedArray> matching_batch = make_expected(R"([
[0, 10, "value-10", "p0"],
[0, 11, "value-11", "p0"],
[0, 12, "value-12", "p0"]
])");
ASSERT_OK(check_candidates("full", {make_expected("[]"), partially_filtered, matching_batch}));
}
TEST_F(RealtimeWriteInteTest, TestMemoryBatchStatisticsPredicatePushdownWithDisk) {
options_[Options::FILE_FORMAT] = "parquet";
options_[Options::WRITE_BATCH_SIZE] = "1";
options_[Options::REALTIME_STORE_STATS_MODE] = "full";
options_["parquet.page.size"] = "1";
options_["parquet.enable.dictionary"] = "false";
options_["parquet.write.enable-page-index"] = "true";
options_["parquet.read.enable-page-index-filter"] = "true";
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> disk_rows = MakeRows(/*first_id=*/0, /*count=*/6, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeBatch(disk_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK(Commit(disk_commits, /*commit_identifier=*/0));
for (int64_t first_id : {0, 10}) {
std::vector<Row> rows = MakeRows(first_id, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
}
std::shared_ptr<Predicate> predicate =
PredicateBuilder::GreaterThan(/*field_index=*/0, /*field_name=*/"id", FieldType::BIGINT,
Literal(static_cast<int64_t>(3)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan, CreatePlan(realtime_context, predicate));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result,
ReadPlan(plan, realtime_context, {"id", "payload", "pt"}, predicate,
/*enable_predicate_filter=*/false));
std::shared_ptr<arrow::DataType> result_type = arrow::struct_(
{arrow::field("_VALUE_KIND", arrow::int8()), arrow::field("id", arrow::int64()),
arrow::field("payload", arrow::utf8()), arrow::field("pt", arrow::utf8())});
std::shared_ptr<arrow::Array> expected =
arrow::ipc::internal::json::ArrayFromJSON(result_type, R"([
[0, 4, "value-4", "p0"],
[0, 5, "value-5", "p0"],
[0, 10, "value-10", "p0"],
[0, 11, "value-11", "p0"],
[0, 12, "value-12", "p0"]
])")
.ValueOrDie();
ASSERT_NE(nullptr, result);
ASSERT_TRUE(std::make_shared<arrow::ChunkedArray>(expected)->Equals(*result))
<< result->ToString();
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestNullPredicateForMemoryAndDisk) {
options_[Options::FILE_FORMAT] = "parquet";
options_[Options::WRITE_BATCH_SIZE] = "1";
options_[Options::REALTIME_STORE_STATS_MODE] = "full";
options_["parquet.page.size"] = "1";
options_["parquet.enable.dictionary"] = "false";
options_["parquet.write.enable-page-index"] = "true";
options_["parquet.write.max-row-group-length"] = "1";
options_["parquet.read.enable-page-index-filter"] = "true";
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeUnpartitionedBatchFromJson(R"([
[0, 0, null, "p0"],
[1, 1, "disk-value", "p0"]
])"));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK(Commit(disk_commits, /*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> non_null_memory_batch,
MakeUnpartitionedBatchFromJson(R"([
[2, 2, "memory-value-2", "p0"],
[3, 3, "memory-value-3", "p0"]
])"));
ASSERT_OK(writer->Write(std::move(non_null_memory_batch)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> nullable_memory_batch,
MakeUnpartitionedBatchFromJson(R"([
[4, 4, null, "p0"],
[5, 5, "memory-value-5", "p0"]
])"));
ASSERT_OK(writer->Write(std::move(nullable_memory_batch)));
std::shared_ptr<Predicate> predicate = PredicateBuilder::IsNull(
/*field_index=*/1, /*field_name=*/"payload", FieldType::STRING);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan, CreatePlan(realtime_context, predicate));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result,
ReadPlan(plan, realtime_context, {"id", "payload", "pt"}, predicate,
/*enable_predicate_filter=*/false));
std::shared_ptr<arrow::DataType> result_type = arrow::struct_(
{arrow::field("_VALUE_KIND", arrow::int8()), arrow::field("id", arrow::int64()),
arrow::field("payload", arrow::utf8()), arrow::field("pt", arrow::utf8())});
std::shared_ptr<arrow::Array> expected =
arrow::ipc::internal::json::ArrayFromJSON(result_type, R"([
[0, 0, null, "p0"],
[0, 4, null, "p0"],
[0, 5, "memory-value-5", "p0"]
])")
.ValueOrDie();
ASSERT_NE(nullptr, result);
ASSERT_TRUE(std::make_shared<arrow::ChunkedArray>(expected)->Equals(*result))
<< result->ToString();
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestUnionReadAfterColumnRename) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> first_context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> first_writer,
CreateRealtimeWriter(first_context));
std::vector<Row> disk_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeBatch(disk_rows, /*partitioned=*/false));
ASSERT_OK(first_writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_commits,
first_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK(Commit(disk_commits, /*commit_identifier=*/0));
ASSERT_OK(first_writer->Close());
std::shared_ptr<arrow::Field> renamed_payload = arrow::field("renamed_payload", arrow::utf8());
ASSERT_OK(TestHelper::WriteNextSchema(
dir_->GetFileSystem(), table_path_,
{DataField(0, fields_[0]), DataField(1, renamed_payload), DataField(2, fields_[2])},
/*highest_field_id=*/2, options_));
fields_[1] = renamed_payload;
schema_ = arrow::schema(fields_);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> second_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> second_writer,
CreateRealtimeWriter(second_context));
std::vector<Row> memory_rows = MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch(memory_rows, /*partitioned=*/false));
ASSERT_OK(second_writer->Write(std::move(memory_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(second_context, /*predicate=*/nullptr));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result,
ReadPlan(plan, second_context, {"id", "renamed_payload", "pt"},
/*predicate=*/nullptr, /*enable_predicate_filter=*/false));
std::shared_ptr<arrow::DataType> result_type = arrow::struct_(
{arrow::field("_VALUE_KIND", arrow::int8()), arrow::field("id", arrow::int64()),
renamed_payload, arrow::field("pt", arrow::utf8())});
std::shared_ptr<arrow::Array> expected =
arrow::ipc::internal::json::ArrayFromJSON(result_type, R"([
[0, 0, "value-0", "p0"],
[0, 1, "value-1", "p0"],
[0, 2, "value-2", "p0"],
[0, 3, "value-3", "p0"],
[0, 4, "value-4", "p0"]
])")
.ValueOrDie();
ASSERT_NE(nullptr, result);
ASSERT_TRUE(std::make_shared<arrow::ChunkedArray>(expected)->Equals(*result))
<< result->ToString();
ASSERT_OK(PrepareAndClose(second_writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestUnionReadWithNestedStructProjection) {
std::shared_ptr<arrow::DataType> address_type =
arrow::struct_({arrow::field("city", arrow::utf8()), arrow::field("zip", arrow::int64())});
std::shared_ptr<arrow::DataType> profile_type = arrow::struct_(
{arrow::field("name", arrow::utf8()), arrow::field("address", address_type)});
fields_ = {arrow::field("id", arrow::int64()), arrow::field("profile", profile_type),
arrow::field("pt", arrow::utf8())};
schema_ = arrow::schema(fields_);
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeUnpartitionedBatchFromJson(R"([
[0, 0, ["disk-0", ["hangzhou", 310000]], "p0"],
[1, 1, ["disk-1", ["shanghai", 200000]], "p0"]
])"));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t disk_snapshot_id, Commit(disk_commits, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(disk_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeUnpartitionedBatchFromJson(R"([
[2, 2, ["memory-2", ["beijing", 100000]], "p0"],
[3, 3, ["memory-3", ["shenzhen", 518000]], "p0"]
])"));
ASSERT_OK(writer->Write(std::move(memory_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
std::shared_ptr<arrow::DataType> projected_address_type =
arrow::struct_({arrow::field("city", arrow::utf8())});
std::shared_ptr<arrow::DataType> projected_profile_type =
arrow::struct_({arrow::field("address", projected_address_type)});
std::shared_ptr<arrow::Schema> projected_schema = arrow::schema(
{arrow::field("id", arrow::int64()), arrow::field("profile", projected_profile_type),
arrow::field("pt", arrow::utf8())});
ReadPlanWithSchemaAndCheck(plan, realtime_context, projected_schema, R"([
[0, 0, [["hangzhou"]], "p0"],
[0, 1, [["shanghai"]], "p0"],
[0, 2, [["beijing"]], "p0"],
[0, 3, [["shenzhen"]], "p0"]
])");
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestUnionReadWithSelectedMapKeys) {
auto run_test = [this](bool primary_key) {
dir_ = UniqueTestDirectory::Create("local");
ASSERT_NE(nullptr, dir_);
table_path_ = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
std::shared_ptr<arrow::DataType> map_type = arrow::map(arrow::utf8(), arrow::int64());
fields_ = {arrow::field("id", arrow::int64()), arrow::field("tags", map_type),
arrow::field("pt", arrow::utf8())};
schema_ = arrow::schema(fields_);
options_["fields.tags.map.storage-layout"] = "shared-shredding";
options_["fields.tags.map.shared-shredding.max-columns"] = "2";
if (primary_key) {
CreatePkTable();
} else {
CreateTable(/*partition_keys=*/{});
}
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeUnpartitionedBatchFromJson(R"([
[0, 0, [["a", 10], ["b", 20]], "p0"],
[1, 1, [["c", 30]], "p0"]
])"));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t disk_snapshot_id,
Commit(disk_commits, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(disk_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeUnpartitionedBatchFromJson(R"([
[2, 2, [["a", 40], ["c", 50]], "p0"],
[3, 3, null, "p0"]
])"));
ASSERT_OK(writer->Write(std::move(memory_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
std::shared_ptr<arrow::KeyValueMetadata> selected_keys =
arrow::KeyValueMetadata::Make({DataField::MAP_SELECTED_KEYS}, {"c,a,missing"});
std::shared_ptr<arrow::Field> selected_map_field = fields_[1]->WithMetadata(selected_keys);
auto selected_map_schema = arrow::schema({fields_[0], selected_map_field, fields_[2]});
ReadPlanWithSchemaAndCheck(plan, realtime_context, selected_map_schema, R"([
[0, 0, [["a", 10]], "p0"],
[0, 1, [["c", 30]], "p0"],
[0, 2, [["c", 50], ["a", 40]], "p0"],
[0, 3, null, "p0"]
])");
ASSERT_OK_AND_ASSIGN(plan, CreatePlan(realtime_context, /*predicate=*/nullptr));
auto c_map_field = std::make_unique<ArrowSchema>();
ASSERT_TRUE(arrow::ExportField(*fields_[1], c_map_field.get()).ok());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<MapSharedShreddingAccessBuilder> access_builder,
MapSharedShreddingAccessBuilder::Create(c_map_field.get()));
ASSERT_OK(access_builder->AddKey("a"));
ASSERT_OK(access_builder->AddKey("missing"));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ArrowSchema> c_access_field, access_builder->Build());
auto access_field_result = arrow::ImportField(c_access_field.get());
ASSERT_TRUE(access_field_result.ok()) << access_field_result.status().ToString();
auto selected_struct_schema =
arrow::schema({fields_[0], access_field_result.ValueOrDie(), fields_[2]});
ReadPlanWithSchemaAndCheck(plan, realtime_context, selected_struct_schema, R"([
[0, 0, [10, null], "p0"],
[0, 1, [null, null], "p0"],
[0, 2, [40, null], "p0"],
[0, 3, null, "p0"]
])");
ASSERT_OK(PrepareAndClose(writer.get()));
};
run_test(/*primary_key=*/false);
ASSERT_FALSE(HasFatalFailure());
run_test(/*primary_key=*/true);
ASSERT_FALSE(HasFatalFailure());
}
TEST_F(RealtimeWriteInteTest, TestUnionReadWithVariantAccess) {
auto run_test = [this](bool primary_key) {
dir_ = UniqueTestDirectory::Create("local");
ASSERT_NE(nullptr, dir_);
table_path_ = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
fields_ = {arrow::field("id", arrow::int32()), VariantTypeUtils::ToArrowField("v")};
schema_ = arrow::schema(fields_);
options_[Options::FILE_FORMAT] = "parquet";
options_[Options::VARIANT_SHREDDING_SCHEMA] = R"({
"type": "ROW",
"fields": [{
"id": 0,
"name": "v",
"type": {
"type": "ROW",
"fields": [
{"id": 1, "name": "age", "type": "BIGINT"},
{"id": 2, "name": "city", "type": "STRING"}
]
}
}]
})";
if (primary_key) {
CreatePkTable();
} else {
CreateTable(/*partition_keys=*/{});
}
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::StructArray> disk_data,
VariantTestData::BuildVariantBatch(
fields_[0], fields_[1],
{R"({"age":10,"city":"disk-a","note":"disk-fallback-a"})",
R"({"age":20,"city":"disk-b","note":"disk-fallback-b"})"},
pool_));
std::shared_ptr<arrow::Array> disk_offsets =
arrow::ipc::internal::json::ArrayFromJSON(arrow::int64(), "[0, 1]").ValueOrDie();
arrow::ArrayVector disk_columns = {std::move(disk_offsets)};
disk_columns.insert(disk_columns.end(), disk_data->fields().begin(),
disk_data->fields().end());
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RealtimeSchemaLayout> schema_layout,
RealtimeSchemaLayout::Create(
primary_key ? RealtimeStoreMode::PRIMARY_KEY : RealtimeStoreMode::APPEND_ONLY,
schema_));
const std::shared_ptr<arrow::Schema>& realtime_schema = schema_layout->InputSchema();
std::shared_ptr<arrow::StructArray> disk_realtime_data =
arrow::StructArray::Make(std::move(disk_columns), realtime_schema->fields())
.ValueOrDie();
ArrowArray disk_c_array;
ASSERT_TRUE(arrow::ExportArray(*disk_realtime_data, &disk_c_array).ok());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
RecordBatchBuilder(&disk_c_array).SetBucket(0).Finish());
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t disk_snapshot_id,
Commit(disk_commits, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(disk_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::StructArray> memory_data,
VariantTestData::BuildVariantBatch(
fields_[0], fields_[1],
{R"({"age":30,"city":"memory-a"})",
R"({"age":40,"city":"memory-b","note":"memory-fallback-b"})"},
pool_, /*id_offset=*/2));
std::shared_ptr<arrow::Array> memory_offsets =
arrow::ipc::internal::json::ArrayFromJSON(arrow::int64(), "[2, 3]").ValueOrDie();
arrow::ArrayVector memory_columns = {std::move(memory_offsets)};
memory_columns.insert(memory_columns.end(), memory_data->fields().begin(),
memory_data->fields().end());
std::shared_ptr<arrow::StructArray> memory_realtime_data =
arrow::StructArray::Make(std::move(memory_columns), realtime_schema->fields())
.ValueOrDie();
ArrowArray memory_c_array;
ASSERT_TRUE(arrow::ExportArray(*memory_realtime_data, &memory_c_array).ok());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
RecordBatchBuilder(&memory_c_array).SetBucket(0).Finish());
ASSERT_OK(writer->Write(std::move(memory_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
VariantAccessBuilder access_builder;
auto age_target = std::make_unique<ArrowSchema>();
auto city_target = std::make_unique<ArrowSchema>();
auto note_target = std::make_unique<ArrowSchema>();
ASSERT_TRUE(
arrow::ExportField(*arrow::field("age", arrow::int64()), age_target.get()).ok());
ASSERT_TRUE(
arrow::ExportField(*arrow::field("city", arrow::utf8()), city_target.get()).ok());
ASSERT_TRUE(
arrow::ExportField(*arrow::field("note", arrow::utf8()), note_target.get()).ok());
ASSERT_OK(access_builder.AddField(age_target.get(), "$.age", /*fail_on_error=*/false));
ASSERT_OK(access_builder.AddField(city_target.get(), "$.city", /*fail_on_error=*/false));
// `note` is intentionally absent from variant.shreddingSchema and must use binary fallback.
ASSERT_OK(access_builder.AddField(note_target.get(), "$.note", /*fail_on_error=*/false));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ArrowSchema> c_access_field,
access_builder.Build("v"));
auto access_field_result = arrow::ImportField(c_access_field.get());
ASSERT_TRUE(access_field_result.ok()) << access_field_result.status().ToString();
auto access_schema = arrow::schema({fields_[0], access_field_result.ValueOrDie()});
ReadPlanWithSchemaAndCheck(plan, realtime_context, access_schema, R"([
[0, 0, [10, "disk-a", "disk-fallback-a"]],
[0, 1, [20, "disk-b", "disk-fallback-b"]],
[0, 2, [30, "memory-a", null]],
[0, 3, [40, "memory-b", "memory-fallback-b"]]
])");
ASSERT_OK(PrepareAndClose(writer.get()));
};
run_test(/*primary_key=*/false);
ASSERT_FALSE(HasFatalFailure());
run_test(/*primary_key=*/true);
ASSERT_FALSE(HasFatalFailure());
}
TEST_F(RealtimeWriteInteTest, TestRefreshCommittedSnapshotReclaimsMemory) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> disk_rows = MakeRows(/*first_id=*/0, /*count=*/10, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeBatch(disk_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, disk_commits.size());
ASSERT_OK_AND_ASSIGN(int64_t committed_snapshot_id,
Commit(disk_commits, /*commit_identifier=*/0));
std::vector<Row> memory_rows = MakeRows(/*first_id=*/10, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch(memory_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(memory_batch)));
std::vector<Row> expected_rows = disk_rows;
expected_rows.insert(expected_rows.end(), memory_rows.begin(), memory_rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> read1, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, read1);
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage_before_refresh,
GetRealtimeMemoryUsage(realtime_context));
ASSERT_OK(writer->RefreshCommittedSnapshot(committed_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> read2, ReadRows(realtime_context));
ASSERT_EQ(read1, read2);
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage_after_refresh,
GetRealtimeMemoryUsage(realtime_context));
ASSERT_LT(memory_usage_after_refresh, memory_usage_before_refresh);
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestPlanPinsMemoryAcrossRefresh) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> disk_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeBatch(disk_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t committed_snapshot_id,
Commit(disk_commits, /*commit_identifier=*/0));
std::vector<Row> memory_rows = MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch(memory_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(memory_batch)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> pinned_plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_OK(writer->RefreshCommittedSnapshot(committed_snapshot_id));
std::vector<Row> expected_rows = disk_rows;
expected_rows.insert(expected_rows.end(), memory_rows.begin(), memory_rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> pinned_rows, ReadRows(pinned_plan, realtime_context));
ASSERT_EQ(expected_rows, pinned_rows);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> refreshed_plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_OK_AND_ASSIGN(std::vector<Row> refreshed_rows,
ReadRows(refreshed_plan, realtime_context));
ASSERT_EQ(pinned_rows, refreshed_rows);
ASSERT_OK(PrepareAndClose(writer.get()));
}
TEST_F(RealtimeWriteInteTest, TestReaderPinsMemoryAcrossRefresh) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, commits.size());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_EQ(1, plan->Splits().size());
std::shared_ptr<RealtimeSplit> realtime_split =
std::dynamic_pointer_cast<RealtimeSplit>(plan->Splits()[0]);
ASSERT_NE(nullptr, realtime_split);
ReadContextBuilder read_builder(table_path_);
read_builder.SetOptions(options_)
.SetReadFieldNames({"id", "payload", "pt"})
.WithRealtimeContext(realtime_context)
.WithMemoryPool(pool_);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<ReadContext> read_context, read_builder.Finish());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<TableRead> table_read,
TableRead::Create(std::move(read_context)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<BatchReader> reader,
table_read->CreateReader(plan->Splits()));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContextImpl> realtime_context_impl,
RealtimeContextImpl::Cast(realtime_context));
ASSERT_NOK_WITH_MSG(realtime_context_impl->ResolveReadView(realtime_split->OpaqueTicket()),
"ticket does not exist or has expired");
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage_before_refresh,
GetRealtimeMemoryUsage(realtime_context));
ASSERT_GT(memory_usage_before_refresh, 0);
ASSERT_OK_AND_ASSIGN(int64_t committed_snapshot_id, Commit(commits, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(committed_snapshot_id));
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage_after_refresh,
GetRealtimeMemoryUsage(realtime_context));
ASSERT_LT(memory_usage_after_refresh, memory_usage_before_refresh);
ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> result,
ReadResultCollector::CollectResult(std::move(reader)));
std::shared_ptr<arrow::DataType> result_type = arrow::struct_(
{arrow::field("_VALUE_KIND", arrow::int8()), arrow::field("id", arrow::int64()),
arrow::field("payload", arrow::utf8()), arrow::field("pt", arrow::utf8())});
std::shared_ptr<arrow::Array> expected =
arrow::ipc::internal::json::ArrayFromJSON(result_type, R"([
[0, 0, "value-0", "p0"],
[0, 1, "value-1", "p0"],
[0, 2, "value-2", "p0"]
])")
.ValueOrDie();
ASSERT_NE(nullptr, result);
ASSERT_TRUE(std::make_shared<arrow::ChunkedArray>(expected)->Equals(*result))
<< result->ToString();
ASSERT_OK(writer->Close());
}
class RealtimePrepareSpillInteTest
: public RealtimeWriteInteTest,
public ::testing::WithParamInterface<std::tuple<bool, bool, bool>> {};
TEST_P(RealtimePrepareSpillInteTest, TestPrepareFreezesWithoutSpilling) {
const auto [primary_key, deletion_vectors, seal_before_prepare] = GetParam();
const bool deduplicate = !primary_key && deletion_vectors;
const bool merge_versions = primary_key || deduplicate;
options_[Options::FILE_FORMAT] = "parquet";
options_[Options::DELETION_VECTORS_ENABLED] = deletion_vectors ? "true" : "false";
options_[Options::REALTIME_SPILL_ENABLED] = "true";
if (primary_key) {
CreatePkTable();
} else if (deduplicate) {
ConfigureRealtimeDeduplicateTable();
} else {
CreateTable(/*partition_keys=*/{});
}
const std::string temp_directory = PathUtil::JoinPath(dir_->Str(), "prepare-without-spill");
ASSERT_OK(dir_->GetFileSystem()->Mkdirs(temp_directory));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(context, temp_directory));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> initial,
MakeBatch({{1, "old", "p0"}, {2, "two", "p0"}},
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(initial)));
if (seal_before_prepare) {
ASSERT_OK(writer->Seal());
}
const int64_t expected_spill_files = seal_before_prepare ? 1 : 0;
ASSERT_EQ(expected_spill_files,
TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory));
const std::vector<Row> next_rows =
merge_versions ? std::vector<Row>{{1, "new", "p0"}, {3, "three", "p0"}}
: std::vector<Row>{{3, "three", "p0"}, {4, "four", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> next,
MakeBatch(next_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(next)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, progress.size());
ASSERT_EQ(OffsetRange(0, 4), progress[0].offset_range);
if (deduplicate) {
ASSERT_NE(nullptr, progress[0].deleted_offsets);
ASSERT_EQ("{0}", progress[0].deleted_offsets->ToString());
}
ASSERT_EQ(expected_spill_files,
TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory));
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues usage, ReadRealtimeMetrics(writer->GetMetrics()));
ASSERT_EQ(0, usage.building_rows);
ASSERT_EQ(4, usage.sealed_rows);
ASSERT_GT(usage.sealed_memory, 0);
const std::vector<Row> expected =
merge_versions
? std::vector<Row>{{1, "new", "p0"}, {2, "two", "p0"}, {3, "three", "p0"}}
: std::vector<Row>{
{1, "old", "p0"}, {2, "two", "p0"}, {3, "three", "p0"}, {4, "four", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> visible, ReadRows(context));
std::sort(visible.begin(), visible.end());
ASSERT_EQ(expected, visible);
ASSERT_OK(writer->Seal());
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> empty,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_TRUE(empty.empty());
ASSERT_EQ(expected_spill_files,
TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory));
ASSERT_OK_AND_ASSIGN(int64_t snapshot, Commit(progress, /*commit_identifier=*/1));
std::shared_ptr<RealtimeContext> disk_context;
if (primary_key && deletion_vectors) {
// Ordinary PK DV scans skip L0; an empty context reads it without any memory tail.
ASSERT_OK_AND_ASSIGN(disk_context, RealtimeContext::Create());
}
ASSERT_OK_AND_ASSIGN(visible, ReadRows(disk_context));
std::sort(visible.begin(), visible.end());
ASSERT_EQ(expected, visible);
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot));
ASSERT_TRUE(WaitForChannelFileCount(temp_directory, /*expected_count=*/0));
ASSERT_OK_AND_ASSIGN(usage, ReadRealtimeMetrics(writer->GetMetrics()));
ASSERT_EQ(0, usage.sealed_memory);
ASSERT_EQ(0, usage.total_rows);
ASSERT_OK_AND_ASSIGN(visible, ReadRows(context));
std::sort(visible.begin(), visible.end());
ASSERT_EQ(expected, visible);
ASSERT_OK(writer->Close());
}
INSTANTIATE_TEST_SUITE_P(Prepare, RealtimePrepareSpillInteTest,
::testing::Combine(::testing::Bool(), ::testing::Bool(),
::testing::Bool()));
TEST_F(RealtimeWriteInteTest, TestAppendRealtimeSpillFileLifecycle) {
options_[Options::REALTIME_SPILL_ENABLED] = "true";
CreateTable(/*partition_keys=*/{});
const std::string temp_directory = PathUtil::JoinPath(dir_->Str(), "append-realtime-spill");
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context, temp_directory));
std::vector<Row> first_rows = MakeRows(/*first_id=*/0, /*count=*/4, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch(std::vector<Row>(first_rows.begin(), first_rows.begin() + 2),
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(std::vector<Row>(first_rows.begin() + 2, first_rows.end()),
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(second_batch)));
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues memory_metrics_before_spill,
ReadRealtimeMetrics(writer->GetMetrics()));
ASSERT_GT(memory_metrics_before_spill.building_memory, 0);
ASSERT_EQ(0, memory_metrics_before_spill.sealed_memory);
ASSERT_EQ(memory_metrics_before_spill.building_memory,
memory_metrics_before_spill.total_memory);
ASSERT_OK(writer->Seal());
ASSERT_EQ(1, TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory));
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues memory_metrics_after_spill,
ReadRealtimeMetrics(writer->GetMetrics()));
ASSERT_EQ(0, memory_metrics_after_spill.building_memory);
ASSERT_EQ(0, memory_metrics_after_spill.sealed_memory);
ASSERT_EQ(0, memory_metrics_after_spill.total_memory);
ASSERT_EQ(0, memory_metrics_after_spill.building_rows);
ASSERT_EQ(4, memory_metrics_after_spill.sealed_rows);
ASSERT_EQ(4, memory_metrics_after_spill.total_rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, first_progress.size());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> pinned_plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_EQ(1, pinned_plan->Splits().size());
ASSERT_NE(nullptr, std::dynamic_pointer_cast<RealtimeSplit>(pinned_plan->Splits()[0]));
ASSERT_OK_AND_ASSIGN(int64_t first_snapshot_id,
Commit(first_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(first_snapshot_id));
ASSERT_EQ(1, TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory));
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues memory_metrics_after_reclaim,
ReadRealtimeMetrics(writer->GetMetrics()));
ASSERT_EQ(0, memory_metrics_after_reclaim.total_memory);
ASSERT_EQ(0, memory_metrics_after_reclaim.total_rows);
ASSERT_OK_AND_ASSIGN(std::vector<Row> pinned_rows, ReadRows(pinned_plan, realtime_context));
ASSERT_EQ(first_rows, pinned_rows);
ASSERT_TRUE(WaitForChannelFileCount(temp_directory, /*expected_count=*/0));
std::vector<Row> second_rows = MakeRows(/*first_id=*/4, /*count=*/4, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> third_batch,
MakeBatch(std::vector<Row>(second_rows.begin(), second_rows.begin() + 2),
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(third_batch)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> fourth_batch,
MakeBatch(std::vector<Row>(second_rows.begin() + 2, second_rows.end()),
/*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(fourth_batch)));
ASSERT_OK(writer->Seal());
ASSERT_EQ(1, TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, second_progress.size());
ASSERT_OK_AND_ASSIGN(int64_t second_snapshot_id,
Commit(second_progress, /*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(second_snapshot_id));
ASSERT_EQ(0, TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory));
std::vector<Row> expected_rows = first_rows;
expected_rows.insert(expected_rows.end(), second_rows.begin(), second_rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, actual_rows);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestDeduplicateRealtimeSpillFileLifecycle) {
options_[Options::REALTIME_SPILL_ENABLED] = "true";
ConfigureRealtimeDeduplicateTable();
const std::string temp_directory = PathUtil::JoinPath(dir_->Str(), "deduplicate-spill");
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(context, temp_directory));
const std::vector<Row> initial_rows = {{1, "old", "p0"}, {2, "two", "p0"}};
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> initial,
MakeBatch(initial_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(initial)));
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> pinned, CreatePlan(context, /*predicate=*/nullptr));
ASSERT_OK(writer->Seal());
ASSERT_EQ(1, TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory));
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues sealed_usage,
ReadRealtimeMetrics(writer->GetMetrics()));
// Arrow payloads have spilled, but the sealed key index still occupies memory.
ASSERT_GT(sealed_usage.sealed_memory, 0);
ASSERT_GT(sealed_usage.building_memory, 0);
ASSERT_EQ(2, sealed_usage.sealed_rows);
ASSERT_EQ(0, sealed_usage.building_rows);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> updates,
MakeBatch({{1, "new", "p0"}, {2, "deleted", "p0"}, {3, "three", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::INSERT, RecordBatch::RowKind::DELETE,
RecordBatch::RowKind::INSERT}));
ASSERT_OK(writer->Write(std::move(updates)));
const std::vector<Row> expected = {{1, "new", "p0"}, {3, "three", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> visible, ReadRows(context));
ASSERT_EQ(expected, visible);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, progress.size());
ASSERT_EQ("{0,1}", progress[0].deleted_offsets->ToString());
ASSERT_OK_AND_ASSIGN(int64_t snapshot, Commit(progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(snapshot));
ASSERT_OK_AND_ASSIGN(visible, ReadRows(context));
ASSERT_EQ(expected, visible);
ASSERT_OK_AND_ASSIGN(RealtimeMetricValues reclaimed_usage,
ReadRealtimeMetrics(writer->GetMetrics()));
ASSERT_EQ(0, reclaimed_usage.sealed_memory);
ASSERT_EQ(0, reclaimed_usage.total_rows);
// The current, empty building key index retains its allocated buckets.
ASSERT_GT(reclaimed_usage.building_memory, 0);
ASSERT_EQ(reclaimed_usage.building_memory, reclaimed_usage.total_memory);
ASSERT_OK_AND_ASSIGN(visible, ReadRows(pinned, context));
ASSERT_EQ(initial_rows, visible);
ASSERT_TRUE(WaitForChannelFileCount(temp_directory, /*expected_count=*/0));
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestPkDvRealtimeSpillFileLifecycle) {
options_[Options::FILE_FORMAT] = "parquet";
options_[Options::DELETION_VECTORS_ENABLED] = "true";
options_[Options::REALTIME_SPILL_ENABLED] = "true";
CreatePkTable();
const std::string temp_directory = PathUtil::JoinPath(dir_->Str(), "pk-dv-realtime-spill");
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context, temp_directory));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch({Row{1, "old-one", "p0"}, Row{2, "two", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::INSERT, RecordBatch::RowKind::INSERT}));
ASSERT_OK(writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(
std::unique_ptr<RecordBatch> second_batch,
MakeBatch({Row{1, "new-one", "p0"}, Row{3, "three", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::UPDATE_AFTER, RecordBatch::RowKind::INSERT}));
ASSERT_OK(writer->Write(std::move(second_batch)));
ASSERT_OK(writer->Seal());
ASSERT_EQ(1, TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, first_progress.size());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<Plan> pinned_plan,
CreatePlan(realtime_context, /*predicate=*/nullptr));
ASSERT_EQ(1, pinned_plan->Splits().size());
ASSERT_NE(nullptr, std::dynamic_pointer_cast<RealtimeSplit>(pinned_plan->Splits()[0]));
ASSERT_OK_AND_ASSIGN(int64_t first_snapshot_id,
Commit(first_progress, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(first_snapshot_id));
ASSERT_EQ(1, TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory));
ASSERT_OK_AND_ASSIGN(std::vector<Row> pinned_rows, ReadRows(pinned_plan, realtime_context));
ASSERT_EQ((std::vector<Row>{{1, "new-one", "p0"}, {2, "two", "p0"}, {3, "three", "p0"}}),
pinned_rows);
ASSERT_TRUE(WaitForChannelFileCount(temp_directory, /*expected_count=*/0));
ASSERT_OK_AND_ASSIGN(Snapshot compact_snapshot, CompactAndCommit(/*partition=*/{}, /*bucket=*/0,
/*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(compact_snapshot.Id()));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> third_batch,
MakeBatch({Row{2, "latest-two", "p0"}}, /*partitioned=*/false,
/*bucket=*/0, {RecordBatch::RowKind::UPDATE_AFTER}));
ASSERT_OK(writer->Write(std::move(third_batch)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> fourth_batch,
MakeBatch({Row{3, "deleted-three", "p0"}, Row{4, "four", "p0"}},
/*partitioned=*/false, /*bucket=*/0,
{RecordBatch::RowKind::DELETE, RecordBatch::RowKind::INSERT}));
ASSERT_OK(writer->Write(std::move(fourth_batch)));
ASSERT_OK(writer->Seal());
ASSERT_EQ(1, TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_progress,
writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
ASSERT_EQ(1, second_progress.size());
ASSERT_OK_AND_ASSIGN(int64_t second_snapshot_id,
Commit(second_progress, /*commit_identifier=*/2));
const std::vector<Row> expected_rows = {
{1, "new-one", "p0"}, {2, "latest-two", "p0"}, {4, "four", "p0"}};
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_reclaim, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_before_reclaim);
ASSERT_OK(writer->RefreshCommittedSnapshot(second_snapshot_id));
ASSERT_EQ(0, TestHelper::CountChannelFiles(dir_->GetFileSystem(), temp_directory));
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, actual_rows);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRepeatedCommitReadAndRefresh) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
constexpr int64_t kRoundCount = 3;
constexpr int64_t kRowsPerRound = 4;
std::vector<Row> expected_rows;
for (int64_t round = 0; round < kRoundCount; ++round) {
std::vector<Row> rows = MakeRows(round * kRowsPerRound, kRowsPerRound, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/round));
ASSERT_EQ(1, commits.size());
ASSERT_EQ(OffsetRange(round * kRowsPerRound, (round + 1) * kRowsPerRound),
commits[0].offset_range);
ASSERT_OK_AND_ASSIGN(int64_t committed_snapshot_id,
Commit(commits, /*commit_identifier=*/round));
expected_rows.insert(expected_rows.end(), rows.begin(), rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> read_before_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, read_before_refresh);
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage_before_refresh,
GetRealtimeMemoryUsage(realtime_context));
ASSERT_GT(memory_usage_before_refresh, 0);
ASSERT_OK(writer->RefreshCommittedSnapshot(committed_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> read_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(read_before_refresh, read_after_refresh);
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage_after_refresh,
GetRealtimeMemoryUsage(realtime_context));
ASSERT_EQ(0, memory_usage_after_refresh);
}
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestRefreshLatestSnapshotReclaimsMultipleCommittedSegments) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
constexpr int64_t kSnapshotCount = 3;
constexpr int64_t kRowsPerSnapshot = 2;
std::vector<Row> expected_rows;
int64_t latest_snapshot_id = -1;
for (int64_t snapshot_index = 0; snapshot_index < kSnapshotCount; ++snapshot_index) {
std::vector<Row> rows =
MakeRows(snapshot_index * kRowsPerSnapshot, kRowsPerSnapshot, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(batch)));
ASSERT_OK_AND_ASSIGN(
std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/snapshot_index));
ASSERT_EQ(1, commits.size());
ASSERT_OK_AND_ASSIGN(latest_snapshot_id,
Commit(commits, /*commit_identifier=*/snapshot_index));
expected_rows.insert(expected_rows.end(), rows.begin(), rows.end());
}
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_before_refresh, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, rows_before_refresh);
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage_before_refresh,
GetRealtimeMemoryUsage(realtime_context));
ASSERT_GT(memory_usage_before_refresh, 0);
ASSERT_OK(writer->RefreshCommittedSnapshot(latest_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_refresh, ReadRows(realtime_context));
ASSERT_EQ(rows_before_refresh, rows_after_refresh);
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage_after_refresh,
GetRealtimeMemoryUsage(realtime_context));
ASSERT_EQ(0, memory_usage_after_refresh);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestOverwriteRequiresReopenRealtimeContext) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> committed_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> committed_batch,
MakeBatch(committed_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(committed_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t committed_snapshot_id, Commit(commits, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(committed_snapshot_id));
std::vector<Row> building_rows = MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> building_batch,
MakeBatch(building_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(building_batch)));
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage_before_overwrite,
GetRealtimeMemoryUsage(realtime_context));
ASSERT_GT(memory_usage_before_overwrite, 0);
ASSERT_OK_AND_ASSIGN(int64_t overwrite_snapshot_id, TruncateTable(/*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(CoreOptions core_options, CoreOptions::FromMap(options_));
SnapshotManager snapshot_manager(core_options.GetFileSystem(), table_path_);
ASSERT_OK_AND_ASSIGN(Snapshot overwrite_snapshot,
snapshot_manager.LoadSnapshot(overwrite_snapshot_id));
ASSERT_EQ(Snapshot::CommitKind::Overwrite(), overwrite_snapshot.GetCommitKind());
ASSERT_FALSE(RealtimeCommitProperties::GetOffsetsPath(overwrite_snapshot));
ASSERT_NOK_WITH_MSG(writer->RefreshCommittedSnapshot(overwrite_snapshot_id),
"recreate RealtimeContext");
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage_after_failed_refresh,
GetRealtimeMemoryUsage(realtime_context));
ASSERT_EQ(memory_usage_before_overwrite, memory_usage_after_failed_refresh);
ASSERT_NOK_WITH_MSG(writer->Close(), "not covered by a successful");
writer.reset();
realtime_context.reset();
ASSERT_OK_AND_ASSIGN(realtime_context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(writer, CreateRealtimeWriter(realtime_context));
ResetExternalOffset(/*partition=*/{}, /*bucket=*/0, /*next_offset=*/0);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> replay_batch,
MakeBatch(building_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(replay_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> replay_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
ASSERT_EQ(1, replay_commits.size());
ASSERT_EQ(OffsetRange(0, 2), replay_commits[0].offset_range);
ASSERT_OK_AND_ASSIGN(int64_t replay_snapshot_id,
Commit(replay_commits, /*commit_identifier=*/2));
ASSERT_OK(writer->RefreshCommittedSnapshot(replay_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> replayed_rows, ReadRows(realtime_context));
ASSERT_EQ(building_rows, replayed_rows);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestReopenRealtimeContextAfterRollback) {
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> first_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch(first_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t first_snapshot_id, Commit(first_commits, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(first_snapshot_id));
std::vector<Row> second_rows = MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(second_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(second_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(int64_t second_snapshot_id,
Commit(second_commits, /*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(second_snapshot_id));
ASSERT_OK_AND_ASSIGN(int64_t rollback_snapshot_id, RollbackToAsLatest(first_snapshot_id));
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap rollback_offsets, ReadCommittedOffsets());
ASSERT_EQ(1, rollback_offsets.size());
ASSERT_EQ(3, rollback_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_NOK_WITH_MSG(writer->RefreshCommittedSnapshot(rollback_snapshot_id),
"recreate RealtimeContext");
ASSERT_OK(writer->Close());
writer.reset();
realtime_context.reset();
// Reopen the same real-time writer identity from the target snapshot's progress and replay
// input after that restored boundary.
ASSERT_OK_AND_ASSIGN(realtime_context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(writer, CreateRealtimeWriter(realtime_context));
ResetExternalOffset(/*partition=*/{}, /*bucket=*/0, /*next_offset=*/3);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> replay_batch,
MakeBatch(second_rows, /*partitioned=*/false));
ASSERT_OK(writer->Write(std::move(replay_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> replay_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
ASSERT_EQ(1, replay_commits.size());
ASSERT_EQ(OffsetRange(3, 5), replay_commits[0].offset_range);
ASSERT_OK(Commit(replay_commits, /*commit_identifier=*/2));
std::vector<Row> expected_rows = first_rows;
expected_rows.insert(expected_rows.end(), second_rows.begin(), second_rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows());
ASSERT_EQ(expected_rows, actual_rows);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestConcurrentWritePrepareCommitReadAndRefresh) {
auto run_test = [this](bool primary_key) {
dir_ = UniqueTestDirectory::Create("local");
ASSERT_NE(nullptr, dir_);
table_path_ = PathUtil::JoinPath(dir_->Str(), "foo.db/bar");
ResetExternalOffset(/*partition=*/{}, /*bucket=*/0, /*next_offset=*/0);
if (primary_key) {
CreatePkTable();
} else {
CreateTable(/*partition_keys=*/{});
}
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
constexpr int32_t kPrepareThreadCount = 4;
constexpr int32_t kReadThreadCount = 4;
constexpr int64_t kBatchCount = 12;
constexpr int64_t kRowsPerBatch = 2;
const int64_t total_rows = kBatchCount * (primary_key ? 3 : kRowsPerBatch);
std::vector<std::vector<Row>> pk_batches;
std::vector<std::vector<RecordBatch::RowKind>> pk_row_kinds;
std::vector<std::vector<Row>> pk_expected_states(1);
if (primary_key) {
std::map<int64_t, Row> current_rows;
for (int64_t batch_index = 0; batch_index < kBatchCount; ++batch_index) {
const int64_t key = batch_index % 4;
const int64_t deleted_key = (key + 2) % 4;
std::vector<Row> rows = {
{key, "update-" + std::to_string(batch_index), "p0"},
{key, "latest-" + std::to_string(batch_index), "p0"},
{deleted_key, "deleted-" + std::to_string(batch_index), "p0"}};
pk_batches.push_back(rows);
pk_row_kinds.push_back({batch_index < 4 ? RecordBatch::RowKind::INSERT
: RecordBatch::RowKind::UPDATE_AFTER,
RecordBatch::RowKind::UPDATE_AFTER,
RecordBatch::RowKind::DELETE});
current_rows[key] = rows[1];
current_rows.erase(deleted_key);
std::vector<Row> expected;
for (const auto& [id, row] : current_rows) {
static_cast<void>(id);
expected.push_back(row);
}
pk_expected_states.push_back(std::move(expected));
}
}
auto validate_read = [&](const std::vector<Row>& rows) {
if (!primary_key) {
return ValidateReadPrefix(rows, total_rows);
}
if (std::find(pk_expected_states.begin(), pk_expected_states.end(), rows) ==
pk_expected_states.end()) {
return Status::Invalid("PK real-time read does not match any completed write");
}
return Status::OK();
};
std::atomic<bool> writer_done{false};
std::atomic<bool> prepare_done{false};
std::atomic<bool> commit_done{false};
std::atomic<bool> refresh_done{false};
std::atomic<int64_t> next_prepare_identifier{0};
std::atomic<int32_t> commit_count{0};
std::atomic<int32_t> refresh_count{0};
ConcurrentTestState state;
std::map<int64_t, RealtimeCommitProgress> pending_commits;
std::deque<int64_t> pending_snapshot_ids;
std::vector<int32_t> prepare_call_counts(kPrepareThreadCount, 0);
std::vector<int32_t> read_call_counts(kReadThreadCount, 0);
auto enqueue_prepared_commits = [&](std::vector<RealtimeCommitProgress>&& commits) {
std::string error;
{
std::lock_guard<std::mutex> lock(state.mutex);
for (RealtimeCommitProgress& commit : commits) {
int64_t offset_begin = commit.offset_range.begin;
if (!pending_commits.emplace(offset_begin, std::move(commit)).second) {
error = "duplicate prepared real-time offset range";
break;
}
}
}
if (!error.empty()) {
state.RecordError(error);
}
state.progress_cv.notify_all();
};
std::thread write_thread([&]() {
state.WaitForStart();
for (int64_t batch_index = 0; batch_index < kBatchCount && !state.ShouldStop();
++batch_index) {
std::vector<Row> rows = primary_key
? pk_batches[static_cast<size_t>(batch_index)]
: MakeRows(batch_index * kRowsPerBatch, kRowsPerBatch,
/*partition=*/"p0");
Result<std::unique_ptr<RecordBatch>> batch_result =
primary_key ? MakeBatch(rows, /*partitioned=*/false, /*bucket=*/0,
pk_row_kinds[static_cast<size_t>(batch_index)])
: MakeBatch(rows, /*partitioned=*/false);
if (state.RecordErrorIfNotOk(batch_result)) {
break;
}
Status status = writer->Write(std::move(batch_result).value());
if (state.RecordErrorIfNotOk(status)) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(2));
// Pause midway until one refresh completes to guarantee write and refresh overlap.
if (batch_index + 1 == kBatchCount / 2) {
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
while (refresh_count.load(std::memory_order_acquire) == 0 &&
!state.ShouldStop() && std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
if (refresh_count.load(std::memory_order_acquire) == 0 && !state.ShouldStop()) {
state.RecordError("timed out waiting for a refresh while writing");
break;
}
}
}
writer_done.store(true, std::memory_order_release);
});
std::vector<std::thread> prepare_threads;
prepare_threads.reserve(kPrepareThreadCount);
for (int32_t thread_index = 0; thread_index < kPrepareThreadCount; ++thread_index) {
prepare_threads.emplace_back([&, thread_index]() {
state.WaitForStart();
do {
int64_t identifier = next_prepare_identifier.fetch_add(1);
Result<std::vector<RealtimeCommitProgress>> result =
writer->PrepareCommitWithProgress(identifier);
++prepare_call_counts[thread_index];
if (state.RecordErrorIfNotOk(result)) {
break;
}
enqueue_prepared_commits(std::move(result).value());
std::this_thread::sleep_for(std::chrono::milliseconds(1));
} while (!writer_done.load(std::memory_order_acquire) && !state.ShouldStop());
});
}
std::thread commit_thread([&]() {
state.WaitForStart();
int64_t next_offset = 0;
int64_t commit_identifier = 0;
while (!state.ShouldStop()) {
std::optional<RealtimeCommitProgress> next_commit;
{
std::unique_lock<std::mutex> lock(state.mutex);
state.progress_cv.wait(lock, [&]() {
return state.ShouldStop() || pending_commits.count(next_offset) > 0 ||
prepare_done.load(std::memory_order_acquire);
});
if (state.ShouldStop()) {
break;
}
auto iter = pending_commits.find(next_offset);
if (iter == pending_commits.end()) {
if (prepare_done.load(std::memory_order_acquire)) {
if (!pending_commits.empty()) {
lock.unlock();
state.RecordError("prepared real-time offset ranges contain a gap");
}
break;
}
continue;
}
next_commit = std::move(iter->second);
pending_commits.erase(iter);
}
std::vector<RealtimeCommitProgress> commits;
commits.push_back(std::move(next_commit).value());
int64_t committed_end_offset = commits[0].offset_range.end;
Result<int64_t> commit_result = Commit(commits, commit_identifier++);
if (state.RecordErrorIfNotOk(commit_result)) {
break;
}
next_offset = committed_end_offset;
{
std::lock_guard<std::mutex> lock(state.mutex);
pending_snapshot_ids.push_back(std::move(commit_result).value());
}
++commit_count;
state.snapshot_cv.notify_all();
}
commit_done.store(true, std::memory_order_release);
state.snapshot_cv.notify_all();
});
std::thread refresh_thread([&]() {
state.WaitForStart();
while (!state.ShouldStop()) {
std::optional<int64_t> snapshot_id;
{
std::unique_lock<std::mutex> lock(state.mutex);
state.snapshot_cv.wait(lock, [&]() {
return state.ShouldStop() || !pending_snapshot_ids.empty() ||
commit_done.load(std::memory_order_acquire);
});
if (state.ShouldStop()) {
break;
}
if (pending_snapshot_ids.empty()) {
if (commit_done.load(std::memory_order_acquire)) {
break;
}
continue;
}
snapshot_id = pending_snapshot_ids.front();
pending_snapshot_ids.pop_front();
}
Status status = writer->RefreshCommittedSnapshot(snapshot_id.value());
if (state.RecordErrorIfNotOk(status)) {
break;
}
++refresh_count;
}
refresh_done.store(true, std::memory_order_release);
});
std::vector<std::thread> read_threads;
read_threads.reserve(kReadThreadCount);
for (int32_t thread_index = 0; thread_index < kReadThreadCount; ++thread_index) {
read_threads.emplace_back([&, thread_index]() {
state.WaitForStart();
while (!refresh_done.load(std::memory_order_acquire) && !state.ShouldStop()) {
Result<std::vector<Row>> result = ReadRows(realtime_context);
++read_call_counts[thread_index];
if (state.RecordErrorIfNotOk(result)) {
break;
}
Status status = validate_read(result.value());
if (state.RecordErrorIfNotOk(status)) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
});
}
constexpr int32_t kWorkerCount = 1 + kPrepareThreadCount + 1 + 1 + kReadThreadCount;
state.StartWhenReady(kWorkerCount);
write_thread.join();
for (std::thread& thread : prepare_threads) {
thread.join();
}
if (!state.ShouldStop()) {
Result<std::vector<RealtimeCommitProgress>> final_result =
writer->PrepareCommitWithProgress(next_prepare_identifier.fetch_add(1));
if (!state.RecordErrorIfNotOk(final_result)) {
enqueue_prepared_commits(std::move(final_result).value());
}
}
prepare_done.store(true, std::memory_order_release);
state.progress_cv.notify_all();
commit_thread.join();
refresh_thread.join();
for (std::thread& thread : read_threads) {
thread.join();
}
ASSERT_TRUE(state.Errors().empty())
<< (state.Errors().empty() ? "" : state.Errors().front());
for (int32_t call_count : prepare_call_counts) {
ASSERT_GT(call_count, 0);
}
for (int32_t call_count : read_call_counts) {
ASSERT_GT(call_count, 0);
}
ASSERT_GE(commit_count.load(), 2);
ASSERT_GE(refresh_count.load(), 2);
ASSERT_OK_AND_ASSIGN(std::vector<Row> final_rows, ReadRows(realtime_context));
if (primary_key) {
ASSERT_EQ(pk_expected_states.back(), final_rows);
} else {
ASSERT_EQ(total_rows, static_cast<int64_t>(final_rows.size()));
ASSERT_OK(ValidateReadPrefix(final_rows, total_rows));
}
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap committed_offsets, ReadCommittedOffsets());
ASSERT_EQ(total_rows,
committed_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage, GetRealtimeMemoryUsage(realtime_context));
ASSERT_EQ(0, memory_usage);
ASSERT_OK(writer->Close());
};
run_test(/*primary_key=*/false);
ASSERT_FALSE(HasFatalFailure());
run_test(/*primary_key=*/true);
ASSERT_FALSE(HasFatalFailure());
}
TEST_F(RealtimeWriteInteTest, TestMultiplePartitions) {
CreateTable(/*partition_keys=*/{"pt"});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<std::vector<Row>> disk_rows;
for (int64_t partition_index = 0; partition_index < 2; ++partition_index) {
std::string partition = "p" + std::to_string(partition_index);
std::vector<Row> rows = MakeRows(partition_index * 10, /*count=*/10, partition);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(rows, /*partitioned=*/true));
ASSERT_OK(writer->Write(std::move(batch)));
disk_rows.push_back(std::move(rows));
}
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(2, disk_commits.size());
ASSERT_OK(Commit(disk_commits, /*commit_identifier=*/0));
std::vector<Row> p0_memory_rows = MakeRows(/*first_id=*/20, /*count=*/5, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> p0_memory_batch,
MakeBatch(p0_memory_rows, /*partitioned=*/true));
ASSERT_OK(writer->Write(std::move(p0_memory_batch)));
std::vector<Row> p2_memory_rows = MakeRows(/*first_id=*/30, /*count=*/5, /*partition=*/"p2");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> p2_memory_batch,
MakeBatch(p2_memory_rows, /*partitioned=*/true));
ASSERT_OK(writer->Write(std::move(p2_memory_batch)));
// p0 has disk and memory rows, p1 is disk-only, and p2 is memory-only.
std::vector<Row> expected_rows = disk_rows[0];
expected_rows.insert(expected_rows.end(), p0_memory_rows.begin(), p0_memory_rows.end());
expected_rows.insert(expected_rows.end(), disk_rows[1].begin(), disk_rows[1].end());
expected_rows.insert(expected_rows.end(), p2_memory_rows.begin(), p2_memory_rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, actual_rows);
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap committed_offsets, ReadCommittedOffsets());
ASSERT_EQ(2, committed_offsets.size());
for (int64_t partition_index = 0; partition_index < 2; ++partition_index) {
RealtimePartitionBucket partition_bucket({{"pt", "p" + std::to_string(partition_index)}},
/*bucket=*/0);
ASSERT_EQ(10, committed_offsets.at(partition_bucket));
}
ASSERT_EQ(committed_offsets.end(),
committed_offsets.find(RealtimePartitionBucket({{"pt", "p2"}}, /*bucket=*/0)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> final_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(2, final_commits.size());
ASSERT_OK_AND_ASSIGN(int64_t final_snapshot_id, Commit(final_commits, /*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(final_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> committed_rows, ReadRows(realtime_context));
ASSERT_EQ(expected_rows, committed_rows);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestDropPartitionRequiresReopenRealtimeContext) {
CreateTable(/*partition_keys=*/{"pt"});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> disk_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> disk_batch,
MakeBatch(disk_rows, /*partitioned=*/true));
ASSERT_OK(writer->Write(std::move(disk_batch)));
std::vector<Row> retained_disk_rows =
MakeRows(/*first_id=*/10, /*count=*/3, /*partition=*/"p1");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> retained_disk_batch,
MakeBatch(retained_disk_rows, /*partitioned=*/true));
ASSERT_OK(writer->Write(std::move(retained_disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> disk_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t disk_snapshot_id, Commit(disk_commits, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(disk_snapshot_id));
std::vector<Row> memory_rows = MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> memory_batch,
MakeBatch(memory_rows, /*partitioned=*/true));
ASSERT_OK(writer->Write(std::move(memory_batch)));
std::vector<Row> rows_before_drop = disk_rows;
rows_before_drop.insert(rows_before_drop.end(), memory_rows.begin(), memory_rows.end());
rows_before_drop.insert(rows_before_drop.end(), retained_disk_rows.begin(),
retained_disk_rows.end());
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows_before_drop, ReadRows(realtime_context));
ASSERT_EQ(rows_before_drop, actual_rows_before_drop);
ASSERT_OK_AND_ASSIGN(int64_t drop_snapshot_id,
DropPartition({{"pt", "p0"}}, /*commit_identifier=*/1));
RealtimePartitionBucket partition_bucket({{"pt", "p0"}}, /*bucket=*/0);
RealtimePartitionBucket retained_partition_bucket({{"pt", "p1"}}, /*bucket=*/0);
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap offsets_after_drop, ReadCommittedOffsets());
ASSERT_EQ(1, offsets_after_drop.size());
ASSERT_EQ(3, offsets_after_drop.at(retained_partition_bucket));
ASSERT_EQ(offsets_after_drop.end(), offsets_after_drop.find(partition_bucket));
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage_before_refresh,
GetRealtimeMemoryUsage(realtime_context));
ASSERT_NOK_WITH_MSG(writer->RefreshCommittedSnapshot(drop_snapshot_id),
"recreate RealtimeContext");
ASSERT_OK_AND_ASSIGN(uint64_t memory_usage_after_refresh,
GetRealtimeMemoryUsage(realtime_context));
ASSERT_EQ(memory_usage_before_refresh, memory_usage_after_refresh);
ASSERT_NOK_WITH_MSG(writer->Close(), "not covered by a successful");
writer.reset();
realtime_context.reset();
// Reopen with p1's retained progress and replay p0 input that existed only in the old context.
ASSERT_OK_AND_ASSIGN(realtime_context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(writer, CreateRealtimeWriter(realtime_context));
ResetExternalOffset({{"pt", "p0"}}, /*bucket=*/0, /*next_offset=*/0);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> replay_batch,
MakeBatch(memory_rows, /*partitioned=*/true));
ASSERT_OK(writer->Write(std::move(replay_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<Row> replayed_rows, ReadRows(realtime_context));
// The retained p1 disk split is read before the tail real-time split containing p0.
std::vector<Row> expected_replayed_rows = retained_disk_rows;
expected_replayed_rows.insert(expected_replayed_rows.end(), memory_rows.begin(),
memory_rows.end());
ASSERT_EQ(expected_replayed_rows, replayed_rows);
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> memory_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
ASSERT_EQ(1, memory_commits.size());
ASSERT_EQ(OffsetRange(0, 2), memory_commits[0].offset_range);
ASSERT_OK_AND_ASSIGN(int64_t memory_snapshot_id,
Commit(memory_commits, /*commit_identifier=*/2));
ASSERT_OK(writer->RefreshCommittedSnapshot(memory_snapshot_id));
ASSERT_OK_AND_ASSIGN(std::vector<Row> rows_after_memory_commit, ReadRows(realtime_context));
std::vector<Row> expected_committed_rows = memory_rows;
expected_committed_rows.insert(expected_committed_rows.end(), retained_disk_rows.begin(),
retained_disk_rows.end());
ASSERT_EQ(expected_committed_rows, rows_after_memory_commit);
ASSERT_OK_AND_ASSIGN(uint64_t final_memory_usage, GetRealtimeMemoryUsage(realtime_context));
ASSERT_EQ(0, final_memory_usage);
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap final_offsets, ReadCommittedOffsets());
ASSERT_EQ(2, final_offsets.size());
ASSERT_EQ(2, final_offsets.at(partition_bucket));
ASSERT_EQ(3, final_offsets.at(retained_partition_bucket));
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestDropInactivePartitionDoesNotRequireReopenRealtimeContext) {
CreateTable(/*partition_keys=*/{"pt"});
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> seed_context, RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> seed_writer,
CreateRealtimeWriter(seed_context));
for (int64_t partition_index = 0; partition_index < 2; ++partition_index) {
std::string partition = "p" + std::to_string(partition_index);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> batch,
MakeBatch(MakeRows(partition_index * 10, /*count=*/3, partition),
/*partitioned=*/true));
ASSERT_OK(seed_writer->Write(std::move(batch)));
}
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> seed_commits,
seed_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(2, seed_commits.size());
ASSERT_OK(Commit(seed_commits, /*commit_identifier=*/0));
ASSERT_OK(seed_writer->Close());
seed_writer.reset();
seed_context.reset();
// The new context loads offsets for both partitions, but creates a store only for p0.
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer,
CreateRealtimeWriter(realtime_context));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> p0_batch,
MakeBatch(MakeRows(/*first_id=*/20, /*count=*/1, /*partition=*/"p0"),
/*partitioned=*/true));
ASSERT_OK(writer->Write(std::move(p0_batch)));
ASSERT_OK_AND_ASSIGN(int64_t drop_snapshot_id,
DropPartition({{"pt", "p1"}}, /*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(drop_snapshot_id));
const RealtimePartitionBucket p0_partition_bucket({{"pt", "p0"}}, /*bucket=*/0);
const RealtimePartitionBucket p1_partition_bucket({{"pt", "p1"}}, /*bucket=*/0);
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap offsets_after_drop, ReadCommittedOffsets());
ASSERT_EQ(1, offsets_after_drop.size());
ASSERT_EQ(3, offsets_after_drop.at(p0_partition_bucket));
ASSERT_EQ(offsets_after_drop.end(), offsets_after_drop.find(p1_partition_bucket));
// Since p1 was never active in this context, writing it after the drop starts from zero.
ResetExternalOffset({{"pt", "p1"}}, /*bucket=*/0, /*next_offset=*/0);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> p1_batch,
MakeBatch(MakeRows(/*first_id=*/30, /*count=*/2, /*partition=*/"p1"),
/*partitioned=*/true));
ASSERT_OK(writer->Write(std::move(p1_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/2));
ASSERT_EQ(2, commits.size());
auto p1_commit =
std::find_if(commits.begin(), commits.end(), [&](const RealtimeCommitProgress& commit) {
return commit.partition_bucket == p1_partition_bucket;
});
ASSERT_NE(commits.end(), p1_commit);
ASSERT_EQ(OffsetRange(0, 2), p1_commit->offset_range);
ASSERT_OK(writer->Close());
}
TEST_F(RealtimeWriteInteTest, TestDropDatePartitionRemovesOffsetWithLegacyPartitionName) {
CheckDropDatePartitionRemovesOffset(/*legacy_partition_name_enabled=*/true);
}
TEST_F(RealtimeWriteInteTest, TestDropDatePartitionRemovesOffsetWithoutLegacyPartitionName) {
CheckDropDatePartitionRemovesOffset(/*legacy_partition_name_enabled=*/false);
}
TEST_F(RealtimeWriteInteTest, TestMultipleBucketsRestoreIndependentOffsets) {
options_[Options::BUCKET] = "2";
CreateTable(/*partition_keys=*/{});
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> first_writer, CreateRealtimeWriter());
std::vector<Row> bucket0_disk_rows = MakeRows(/*first_id=*/0, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> bucket0_disk_batch,
MakeBatch(bucket0_disk_rows, /*partitioned=*/false, /*bucket=*/0));
ASSERT_OK(first_writer->Write(std::move(bucket0_disk_batch)));
std::vector<Row> bucket1_disk_rows = MakeRows(/*first_id=*/10, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> bucket1_disk_batch,
MakeBatch(bucket1_disk_rows, /*partitioned=*/false, /*bucket=*/1));
ASSERT_OK(first_writer->Write(std::move(bucket1_disk_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_commits,
first_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(2, first_commits.size());
ASSERT_OK(Commit(first_commits, /*commit_identifier=*/0));
ASSERT_OK(first_writer->Close());
ASSERT_OK_AND_ASSIGN(std::shared_ptr<RealtimeContext> realtime_context,
RealtimeContext::Create());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> second_writer,
CreateRealtimeWriter(realtime_context));
std::vector<Row> bucket0_memory_rows =
MakeRows(/*first_id=*/2, /*count=*/1, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> bucket0_memory_batch,
MakeBatch(bucket0_memory_rows, /*partitioned=*/false, /*bucket=*/0));
ASSERT_OK(second_writer->Write(std::move(bucket0_memory_batch)));
std::vector<Row> bucket1_memory_rows =
MakeRows(/*first_id=*/13, /*count=*/1, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> bucket1_memory_batch,
MakeBatch(bucket1_memory_rows, /*partitioned=*/false, /*bucket=*/1));
ASSERT_OK(second_writer->Write(std::move(bucket1_memory_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_commits,
second_writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(2, second_commits.size());
std::map<int32_t, OffsetRange> prepared_ranges;
for (const RealtimeCommitProgress& commit : second_commits) {
prepared_ranges.emplace(commit.partition_bucket.bucket, commit.offset_range);
}
ASSERT_EQ(OffsetRange(2, 3), prepared_ranges.at(0));
ASSERT_EQ(OffsetRange(3, 4), prepared_ranges.at(1));
ASSERT_OK_AND_ASSIGN(std::vector<Row> actual_rows, ReadRows(realtime_context));
std::vector<Row> expected_rows = bucket0_disk_rows;
expected_rows.insert(expected_rows.end(), bucket0_memory_rows.begin(),
bucket0_memory_rows.end());
expected_rows.insert(expected_rows.end(), bucket1_disk_rows.begin(), bucket1_disk_rows.end());
expected_rows.insert(expected_rows.end(), bucket1_memory_rows.begin(),
bucket1_memory_rows.end());
std::sort(expected_rows.begin(), expected_rows.end());
std::sort(actual_rows.begin(), actual_rows.end());
ASSERT_EQ(expected_rows, actual_rows);
ASSERT_OK(Commit(second_commits, /*commit_identifier=*/1));
ASSERT_OK(second_writer->Close());
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap committed_offsets, ReadCommittedOffsets());
ASSERT_EQ(2, committed_offsets.size());
ASSERT_EQ(3, committed_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/0)));
ASSERT_EQ(4, committed_offsets.at(RealtimePartitionBucket(/*partition=*/{}, /*bucket=*/1)));
}
TEST_F(RealtimeWriteInteTest, TestRestoreOffsetFromCommittedSnapshot) {
CreateTable(/*partition_keys=*/{});
std::vector<Row> first_rows = MakeRows(/*first_id=*/0, /*count=*/3, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> first_writer, CreateRealtimeWriter());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> first_batch,
MakeBatch(first_rows, /*partitioned=*/false));
ASSERT_OK(first_writer->Write(std::move(first_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_commits,
first_writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_EQ(1, first_commits.size());
ASSERT_EQ(OffsetRange(0, 3), first_commits[0].offset_range);
ASSERT_OK(Commit(first_commits, /*commit_identifier=*/0));
ASSERT_OK(first_writer->Close());
std::vector<Row> second_rows = MakeRows(/*first_id=*/3, /*count=*/2, /*partition=*/"p0");
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> second_writer, CreateRealtimeWriter());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(second_rows, /*partitioned=*/false));
ASSERT_OK(second_writer->Write(std::move(second_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_commits,
second_writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_EQ(1, second_commits.size());
ASSERT_EQ(OffsetRange(3, 5), second_commits[0].offset_range);
std::vector<Row> expected_rows = MakeRows(/*first_id=*/0, /*count=*/5, /*partition=*/"p0");
FinalizeCommitAndCheck(second_writer.get(), std::move(second_commits),
/*prepare_identifier=*/1, std::move(expected_rows));
RealtimePartitionBucket partition_bucket(/*partition=*/{}, /*bucket=*/0);
ASSERT_OK_AND_ASSIGN(RealtimeOffsetMap second_committed_offsets, ReadCommittedOffsets());
ASSERT_EQ(5, second_committed_offsets.at(partition_bucket));
}
TEST_F(RealtimeWriteInteTest, TestReadRealtimeOffsetsFromExactSnapshot) {
options_[Options::BUCKET] = "2";
CreateTable(/*partition_keys=*/{"pt"});
ASSERT_OK_AND_ASSIGN(std::unique_ptr<FileStoreWrite> writer, CreateRealtimeWriter());
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> p0_bucket0_batch,
MakeBatch(MakeRows(/*first_id=*/0, /*count=*/2, /*partition=*/"p0"),
/*partitioned=*/true, /*bucket=*/0));
ASSERT_OK(writer->Write(std::move(p0_bucket0_batch)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> p0_bucket1_batch,
MakeBatch(MakeRows(/*first_id=*/10, /*count=*/3, /*partition=*/"p0"),
/*partitioned=*/true, /*bucket=*/1));
ASSERT_OK(writer->Write(std::move(p0_bucket1_batch)));
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> p1_bucket0_batch,
MakeBatch(MakeRows(/*first_id=*/20, /*count=*/1, /*partition=*/"p1"),
/*partitioned=*/true, /*bucket=*/0));
ASSERT_OK(writer->Write(std::move(p1_bucket0_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> first_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/0));
ASSERT_OK_AND_ASSIGN(int64_t first_snapshot_id, Commit(first_commits, /*commit_identifier=*/0));
ASSERT_OK(writer->RefreshCommittedSnapshot(first_snapshot_id));
const RealtimePartitionBucket p0_bucket0({{"pt", "p0"}}, /*bucket=*/0);
const RealtimePartitionBucket p0_bucket1({{"pt", "p0"}}, /*bucket=*/1);
const RealtimePartitionBucket p1_bucket0({{"pt", "p1"}}, /*bucket=*/0);
ASSERT_OK_AND_ASSIGN(
RealtimeOffsetMap first_offsets,
RealtimeSnapshotOffsets::ReadAllOffsets(table_path_, /*branch=*/"", first_snapshot_id,
options_, /*file_system=*/nullptr));
ASSERT_EQ(3, first_offsets.size());
ASSERT_EQ(2, first_offsets.at(p0_bucket0));
ASSERT_EQ(3, first_offsets.at(p0_bucket1));
ASSERT_EQ(1, first_offsets.at(p1_bucket0));
ASSERT_OK_AND_ASSIGN(int64_t p0_bucket1_offset,
RealtimeSnapshotOffsets::ReadOffset(
table_path_, /*branch=*/"", first_snapshot_id, p0_bucket1, options_,
/*file_system=*/nullptr));
ASSERT_EQ(3, p0_bucket1_offset);
ASSERT_OK_AND_ASSIGN(int64_t missing_offset,
RealtimeSnapshotOffsets::ReadOffset(
table_path_, /*branch=*/"", first_snapshot_id,
RealtimePartitionBucket({{"pt", "missing"}}, /*bucket=*/0), options_,
/*file_system=*/nullptr));
ASSERT_EQ(-1, missing_offset);
ASSERT_OK_AND_ASSIGN(std::unique_ptr<RecordBatch> second_batch,
MakeBatch(MakeRows(/*first_id=*/2, /*count=*/1, /*partition=*/"p0"),
/*partitioned=*/true, /*bucket=*/0));
ASSERT_OK(writer->Write(std::move(second_batch)));
ASSERT_OK_AND_ASSIGN(std::vector<RealtimeCommitProgress> second_commits,
writer->PrepareCommitWithProgress(/*commit_identifier=*/1));
ASSERT_OK_AND_ASSIGN(int64_t second_snapshot_id,
Commit(second_commits, /*commit_identifier=*/1));
ASSERT_OK(writer->RefreshCommittedSnapshot(second_snapshot_id));
ASSERT_OK_AND_ASSIGN(int64_t first_snapshot_offset,
RealtimeSnapshotOffsets::ReadOffset(
table_path_, /*branch=*/"", first_snapshot_id, p0_bucket0, options_,
/*file_system=*/nullptr));
ASSERT_OK_AND_ASSIGN(int64_t second_snapshot_offset,
RealtimeSnapshotOffsets::ReadOffset(
table_path_, /*branch=*/"", second_snapshot_id, p0_bucket0, options_,
/*file_system=*/nullptr));
ASSERT_EQ(2, first_snapshot_offset);
ASSERT_EQ(3, second_snapshot_offset);
ASSERT_OK(writer->Close());
}
} // namespace paimon::test