blob: 8853d419c2e7bd160a56ea6db41c1948a913fadc [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 "iceberg/update/fast_append.h"
#include <format>
#include <optional>
#include <string>
#include <thread>
#include <unordered_set>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "iceberg/avro/avro_register.h"
#include "iceberg/constants.h"
#include "iceberg/manifest/manifest_entry.h"
#include "iceberg/manifest/manifest_writer.h"
#include "iceberg/partition_spec.h"
#include "iceberg/schema.h"
#include "iceberg/snapshot.h"
#include "iceberg/table_metadata.h"
#include "iceberg/test/matchers.h"
#include "iceberg/test/update_test_base.h"
#include "iceberg/transaction.h"
namespace iceberg {
namespace {
class TestSnapshotUpdate : public SnapshotUpdate {
public:
explicit TestSnapshotUpdate(std::shared_ptr<TransactionContext> ctx)
: SnapshotUpdate(std::move(ctx)) {}
using SnapshotUpdate::ManifestPath;
Status CleanUncommitted(const std::unordered_set<std::string>&) override { return {}; }
std::string operation() override { return "test"; }
Result<std::vector<ManifestFile>> Apply(const TableMetadata&,
const std::shared_ptr<Snapshot>&) override {
return std::vector<ManifestFile>{};
}
std::unordered_map<std::string, std::string> Summary() override { return {}; }
};
} // namespace
class FastAppendTest : public UpdateTestBase {
protected:
static void SetUpTestSuite() { avro::RegisterAll(); }
std::string MetadataResource() const override {
return "TableMetadataV2ValidMinimal.json";
}
void SetUp() override {
UpdateTestBase::SetUp();
// Get partition spec and schema from the base table
ICEBERG_UNWRAP_OR_FAIL(spec_, table_->spec());
ICEBERG_UNWRAP_OR_FAIL(schema_, table_->schema());
// Create test data files
file_a_ =
CreateDataFile("/data/file_a.parquet", /*size=*/100, /*partition_value=*/1024);
file_b_ =
CreateDataFile("/data/file_b.parquet", /*size=*/200, /*partition_value=*/2048);
}
std::shared_ptr<DataFile> CreateDataFile(const std::string& path, int64_t record_count,
int64_t size, int64_t partition_value = 0) {
auto data_file = std::make_shared<DataFile>();
data_file->content = DataFile::Content::kData;
data_file->file_path = table_location_ + path;
data_file->file_format = FileFormatType::kParquet;
// The base table has partition spec with identity(x), so we need 1 partition value
data_file->partition =
PartitionValues(std::vector<Literal>{Literal::Long(partition_value)});
data_file->file_size_in_bytes = size;
data_file->record_count = record_count;
data_file->partition_spec_id = spec_->spec_id();
return data_file;
}
Result<ManifestFile> WriteManifest(
const std::string& path, const std::vector<std::shared_ptr<DataFile>>& files) {
ICEBERG_ASSIGN_OR_RAISE(
auto writer, ManifestWriter::MakeWriter(table_->metadata()->format_version,
kInvalidSnapshotId, path, file_io_, spec_,
schema_, ManifestContent::kData));
for (const auto& file : files) {
ManifestEntry entry;
entry.status = ManifestStatus::kAdded;
entry.snapshot_id = std::nullopt;
entry.data_file = file;
ICEBERG_RETURN_UNEXPECTED(writer->WriteAddedEntry(entry));
}
ICEBERG_RETURN_UNEXPECTED(writer->Close());
return writer->ToManifestFile();
}
std::shared_ptr<PartitionSpec> spec_;
std::shared_ptr<Schema> schema_;
std::shared_ptr<DataFile> file_a_;
std::shared_ptr<DataFile> file_b_;
};
class SnapshotUpdateTest : public UpdateTestBase {};
TEST_F(FastAppendTest, AppendDataFile) {
std::shared_ptr<FastAppend> fast_append;
ICEBERG_UNWRAP_OR_FAIL(fast_append, table_->NewFastAppend());
fast_append->AppendFile(file_a_);
EXPECT_THAT(fast_append->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
EXPECT_EQ(snapshot->summary.at("added-data-files"), "1");
EXPECT_EQ(snapshot->summary.at("added-records"), "100");
EXPECT_EQ(snapshot->summary.at("added-files-size"), "1024");
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kManifestsCreated), "1");
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kManifestsKept), "0");
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kManifestsReplaced), "0");
}
TEST_F(FastAppendTest, AppendMultipleDataFiles) {
std::shared_ptr<FastAppend> fast_append;
ICEBERG_UNWRAP_OR_FAIL(fast_append, table_->NewFastAppend());
fast_append->AppendFile(file_a_);
fast_append->AppendFile(file_b_);
EXPECT_THAT(fast_append->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
EXPECT_EQ(snapshot->summary.at("added-data-files"), "2");
EXPECT_EQ(snapshot->summary.at("added-records"), "300");
EXPECT_EQ(snapshot->summary.at("added-files-size"), "3072");
}
TEST_F(FastAppendTest, AppendManyFiles) {
std::shared_ptr<FastAppend> fast_append;
ICEBERG_UNWRAP_OR_FAIL(fast_append, table_->NewFastAppend());
int64_t total_records = 0;
int64_t total_size = 0;
constexpr int kFileCount = 10;
for (int index = 0; index < kFileCount; ++index) {
auto data_file = CreateDataFile(std::format("/data/file_{}.parquet", index),
/*record_count=*/10 + index,
/*size=*/100 + index * 10,
/*partition_value=*/index % 2);
total_records += data_file->record_count;
total_size += data_file->file_size_in_bytes;
fast_append->AppendFile(std::move(data_file));
}
EXPECT_THAT(fast_append->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
EXPECT_EQ(snapshot->summary.at("added-data-files"), std::to_string(kFileCount));
EXPECT_EQ(snapshot->summary.at("added-records"), std::to_string(total_records));
EXPECT_EQ(snapshot->summary.at("added-files-size"), std::to_string(total_size));
}
TEST_F(FastAppendTest, EmptyTableAppendUpdatesSequenceNumbers) {
EXPECT_THAT(table_->current_snapshot(), HasErrorMessage("No current snapshot"));
const int64_t base_sequence_number = table_->metadata()->last_sequence_number;
std::shared_ptr<FastAppend> fast_append;
ICEBERG_UNWRAP_OR_FAIL(fast_append, table_->NewFastAppend());
fast_append->AppendFile(file_a_);
EXPECT_THAT(fast_append->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
EXPECT_EQ(snapshot->sequence_number, base_sequence_number + 1);
EXPECT_EQ(table_->metadata()->last_sequence_number, base_sequence_number + 1);
}
TEST_F(FastAppendTest, AppendNullFile) {
std::shared_ptr<FastAppend> fast_append;
ICEBERG_UNWRAP_OR_FAIL(fast_append, table_->NewFastAppend());
fast_append->AppendFile(nullptr);
auto result = fast_append->Commit();
EXPECT_FALSE(result.has_value());
EXPECT_THAT(result, HasErrorMessage("Invalid data file: null"));
EXPECT_THAT(table_->current_snapshot(), HasErrorMessage("No current snapshot"));
}
TEST_F(FastAppendTest, FinalizeIgnoresCleanupDeleteFailure) {
std::shared_ptr<FastAppend> fast_append;
ICEBERG_UNWRAP_OR_FAIL(fast_append, table_->NewFastAppend());
fast_append->AppendFile(file_a_);
fast_append->DeleteWith([](const std::string&) { return IOError("delete failed"); });
EXPECT_THAT(static_cast<SnapshotUpdate&>(*fast_append).Apply(), IsOk());
EXPECT_THAT(fast_append->Finalize(Result<const TableMetadata*>(
std::unexpected(CommitFailed("commit failed").error()))),
IsOk());
}
TEST_F(FastAppendTest, RetryCopiesAppendManifestAgain) {
table_->metadata()->format_version = 1;
const auto path = table_location_ + "/metadata/input.avro";
ICEBERG_UNWRAP_OR_FAIL(auto manifest, WriteManifest(path, {file_a_}));
std::shared_ptr<FastAppend> fast_append;
ICEBERG_UNWRAP_OR_FAIL(fast_append, table_->NewFastAppend());
std::vector<std::string> deleted_paths;
fast_append->DeleteWith([&](const std::string& deleted_path) {
deleted_paths.push_back(deleted_path);
return file_io_->DeleteFile(deleted_path);
});
fast_append->AppendManifest(manifest);
auto& update = static_cast<SnapshotUpdate&>(*fast_append);
// First Apply() copies the input manifest because v1 cannot inherit snapshot IDs.
ICEBERG_UNWRAP_OR_FAIL(auto first_apply, update.Apply());
SnapshotCache first_cache(first_apply.snapshot.get());
ICEBERG_UNWRAP_OR_FAIL(auto first_manifests, first_cache.Manifests(file_io_));
ASSERT_EQ(first_manifests.size(), 1U);
const auto first_rewritten_path = first_manifests[0].manifest_path;
EXPECT_NE(first_rewritten_path, path);
// Second Apply() simulates retry cleanup, then copies the original manifest again.
ICEBERG_UNWRAP_OR_FAIL(auto second_apply, update.Apply());
EXPECT_THAT(deleted_paths, testing::Contains(first_rewritten_path));
SnapshotCache second_cache(second_apply.snapshot.get());
ICEBERG_UNWRAP_OR_FAIL(auto second_manifests, second_cache.Manifests(file_io_));
ASSERT_EQ(second_manifests.size(), 1U);
EXPECT_NE(second_manifests[0].manifest_path, path);
EXPECT_NE(second_manifests[0].manifest_path, first_rewritten_path);
}
TEST_F(FastAppendTest, AppendDuplicateFile) {
std::shared_ptr<FastAppend> fast_append;
ICEBERG_UNWRAP_OR_FAIL(fast_append, table_->NewFastAppend());
fast_append->AppendFile(file_a_);
fast_append->AppendFile(file_a_); // Add same file twice
EXPECT_THAT(fast_append->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
EXPECT_EQ(snapshot->summary.at("added-data-files"), "1");
EXPECT_EQ(snapshot->summary.at("added-records"), "100");
}
TEST_F(FastAppendTest, SetSnapshotProperty) {
std::shared_ptr<FastAppend> fast_append;
ICEBERG_UNWRAP_OR_FAIL(fast_append, table_->NewFastAppend());
fast_append->Set("custom-property", "custom-value");
fast_append->AppendFile(file_a_);
EXPECT_THAT(fast_append->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
EXPECT_EQ(snapshot->summary.at("custom-property"), "custom-value");
}
TEST_F(SnapshotUpdateTest, ConcurrentManifestPaths) {
ICEBERG_UNWRAP_OR_FAIL(auto ctx,
TransactionContext::Make(table_, TransactionKind::kUpdate));
TestSnapshotUpdate update(std::move(ctx));
constexpr int kThreadCount = 8;
constexpr int kPathsPerThread = 32;
std::vector<std::string> paths(kThreadCount * kPathsPerThread);
std::vector<std::thread> threads;
threads.reserve(kThreadCount);
for (int thread_index = 0; thread_index < kThreadCount; ++thread_index) {
threads.emplace_back([&, thread_index] {
for (int path_index = 0; path_index < kPathsPerThread; ++path_index) {
paths[thread_index * kPathsPerThread + path_index] = update.ManifestPath();
}
});
}
for (auto& thread : threads) {
thread.join();
}
std::unordered_set<std::string> unique_paths(paths.begin(), paths.end());
ASSERT_EQ(unique_paths.size(), paths.size());
for (const auto& path : paths) {
EXPECT_THAT(path, ::testing::HasSubstr("/metadata/"));
EXPECT_THAT(path, ::testing::HasSubstr("-m"));
}
}
} // namespace iceberg