blob: 5906e7e39ed20a39c72a767d57be121b243329ca [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/row_delta.h"
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "iceberg/avro/avro_register.h"
#include "iceberg/manifest/manifest_entry.h"
#include "iceberg/partition_spec.h"
#include "iceberg/row/partition_values.h"
#include "iceberg/schema.h"
#include "iceberg/snapshot.h"
#include "iceberg/table.h"
#include "iceberg/table_metadata.h"
#include "iceberg/test/matchers.h"
#include "iceberg/test/update_test_base.h"
#include "iceberg/update/delete_files.h"
#include "iceberg/update/fast_append.h"
namespace iceberg {
class RowDeltaTest : public MinimalUpdateTestBase {
protected:
static void SetUpTestSuite() { avro::RegisterAll(); }
void SetUp() override {
MinimalUpdateTestBase::SetUp();
ICEBERG_UNWRAP_OR_FAIL(spec_, table_->spec());
ICEBERG_UNWRAP_OR_FAIL(schema_, table_->schema());
file_a_ = MakeDataFile("/data/file_a.parquet", /*partition_x=*/1L);
file_b_ = MakeDataFile("/data/file_b.parquet", /*partition_x=*/2L);
}
std::shared_ptr<DataFile> MakeDataFile(const std::string& path, int64_t partition_x) {
auto file = std::make_shared<DataFile>();
file->content = DataFile::Content::kData;
file->file_path = table_location_ + path;
file->file_format = FileFormatType::kParquet;
file->partition = PartitionValues(std::vector<Literal>{Literal::Long(partition_x)});
file->file_size_in_bytes = 1024;
file->record_count = 100;
file->partition_spec_id = spec_->spec_id();
return file;
}
std::shared_ptr<DataFile> MakeDeleteFile(const std::string& path, int64_t partition_x) {
auto file = MakeDataFile(path, partition_x);
file->content = DataFile::Content::kPositionDeletes;
file->file_size_in_bytes = 256;
file->record_count = 7;
return file;
}
std::shared_ptr<DataFile> MakeDeletionVector(const std::string& path,
const std::string& referenced_data_file,
int64_t partition_x,
int64_t content_offset = 0) {
auto file = MakeDeleteFile(path, partition_x);
file->file_format = FileFormatType::kPuffin;
file->referenced_data_file = referenced_data_file;
file->content_offset = content_offset;
file->content_size_in_bytes = 10;
return file;
}
void AppendFileAToTable() {
ICEBERG_UNWRAP_OR_FAIL(auto fast_append, table_->NewFastAppend());
fast_append->AppendFile(file_a_);
EXPECT_THAT(fast_append->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
}
void SetTableFormatVersion(int8_t format_version) {
table_->metadata()->format_version = format_version;
}
std::shared_ptr<PartitionSpec> spec_;
std::shared_ptr<Schema> schema_;
std::shared_ptr<DataFile> file_a_;
std::shared_ptr<DataFile> file_b_;
};
TEST_F(RowDeltaTest, AddRowsCommitsAppendOperation) {
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->AddRows(file_a_);
EXPECT_THAT(row_delta->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
EXPECT_EQ(snapshot->Operation(), std::make_optional(DataOperation::kAppend));
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedDataFiles), "1");
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedRecords), "100");
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedFileSize), "1024");
}
TEST_F(RowDeltaTest, AddDeletesCommitsDeleteOperation) {
auto delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
/*partition_x=*/1L);
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->AddDeletes(delete_file);
EXPECT_THAT(row_delta->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
EXPECT_EQ(snapshot->Operation(), std::make_optional(DataOperation::kDelete));
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedDeleteFiles), "1");
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedPosDeleteFiles), "1");
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedPosDeletes), "7");
}
TEST_F(RowDeltaTest, RemoveRowsCommitsOverwriteOperation) {
AppendFileAToTable();
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->RemoveRows(file_a_);
EXPECT_THAT(row_delta->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
EXPECT_EQ(snapshot->Operation(), std::make_optional(DataOperation::kOverwrite));
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kDeletedDataFiles), "1");
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kDeletedRecords), "100");
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kRemovedFileSize), "1024");
}
TEST_F(RowDeltaTest, RemoveRowsAndAddDeletesCommitsDeleteOperation) {
AppendFileAToTable();
auto delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
/*partition_x=*/1L);
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->RemoveRows(file_a_);
row_delta->AddDeletes(delete_file);
EXPECT_THAT(row_delta->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
EXPECT_EQ(snapshot->Operation(), std::make_optional(DataOperation::kDelete));
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kDeletedDataFiles), "1");
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedDeleteFiles), "1");
}
TEST_F(RowDeltaTest, AddRowsAndRemoveDeletesCommitsAppendOperation) {
auto delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
/*partition_x=*/1L);
{
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->AddDeletes(delete_file);
EXPECT_THAT(row_delta->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
}
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->AddRows(file_a_);
row_delta->RemoveDeletes(delete_file);
EXPECT_THAT(row_delta->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
EXPECT_EQ(snapshot->Operation(), std::make_optional(DataOperation::kAppend));
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedDataFiles), "1");
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kRemovedDeleteFiles), "1");
}
TEST_F(RowDeltaTest, AddDeletesAndRemoveDeletesCommitsDeleteOperation) {
auto old_delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
/*partition_x=*/1L);
{
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->AddDeletes(old_delete_file);
EXPECT_THAT(row_delta->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
}
auto new_delete_file = MakeDeleteFile("/delete/file_b_pos_deletes.parquet",
/*partition_x=*/2L);
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->AddDeletes(new_delete_file);
row_delta->RemoveDeletes(old_delete_file);
EXPECT_THAT(row_delta->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
EXPECT_EQ(snapshot->Operation(), std::make_optional(DataOperation::kDelete));
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedDeleteFiles), "1");
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kRemovedDeleteFiles), "1");
}
TEST_F(RowDeltaTest, ValidateNoConflictingDataFilesFailsForConcurrentAppend) {
AppendFileAToTable();
ICEBERG_UNWRAP_OR_FAIL(auto starting_snapshot, table_->current_snapshot());
ICEBERG_UNWRAP_OR_FAIL(auto concurrent_append, table_->NewFastAppend());
concurrent_append->AppendFile(file_b_);
EXPECT_THAT(concurrent_append->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
auto file_c = MakeDataFile("/data/file_c.parquet", /*partition_x=*/3L);
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->ValidateFromSnapshot(starting_snapshot->snapshot_id);
row_delta->ValidateNoConflictingDataFiles();
row_delta->AddRows(file_c);
auto result = row_delta->Commit();
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
EXPECT_THAT(result, HasErrorMessage("Found conflicting files"));
EXPECT_THAT(result, HasErrorMessage(file_b_->file_path));
}
TEST_F(RowDeltaTest, ValidateNoConflictingDeleteFilesFailsForConcurrentDelete) {
AppendFileAToTable();
ICEBERG_UNWRAP_OR_FAIL(auto starting_snapshot, table_->current_snapshot());
auto delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
/*partition_x=*/1L);
std::shared_ptr<RowDelta> concurrent_delta;
ICEBERG_UNWRAP_OR_FAIL(concurrent_delta, table_->NewRowDelta());
concurrent_delta->AddDeletes(delete_file);
EXPECT_THAT(concurrent_delta->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
auto file_c = MakeDataFile("/data/file_c.parquet", /*partition_x=*/3L);
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->ValidateFromSnapshot(starting_snapshot->snapshot_id);
row_delta->ValidateNoConflictingDeleteFiles();
row_delta->AddRows(file_c);
auto result = row_delta->Commit();
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
EXPECT_THAT(result, HasErrorMessage("Found new conflicting delete files"));
EXPECT_THAT(result, HasErrorMessage(delete_file->file_path));
}
TEST_F(RowDeltaTest, ValidateDataFilesExistSkipsConcurrentDeleteByDefault) {
AppendFileAToTable();
ICEBERG_UNWRAP_OR_FAIL(auto starting_snapshot, table_->current_snapshot());
ICEBERG_UNWRAP_OR_FAIL(auto delete_files, table_->NewDeleteFiles());
delete_files->DeleteFile(file_a_);
EXPECT_THAT(delete_files->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
auto delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
/*partition_x=*/1L);
delete_file->referenced_data_file = file_a_->file_path;
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->ValidateFromSnapshot(starting_snapshot->snapshot_id);
std::vector<std::string> referenced_files{file_a_->file_path};
row_delta->ValidateDataFilesExist(referenced_files);
row_delta->AddDeletes(delete_file);
EXPECT_THAT(row_delta->Commit(), IsOk());
}
TEST_F(RowDeltaTest,
ValidateDataFilesExistFailsForConcurrentDeleteWithValidateDeletedFiles) {
AppendFileAToTable();
ICEBERG_UNWRAP_OR_FAIL(auto starting_snapshot, table_->current_snapshot());
ICEBERG_UNWRAP_OR_FAIL(auto delete_files, table_->NewDeleteFiles());
delete_files->DeleteFile(file_a_);
EXPECT_THAT(delete_files->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
auto delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
/*partition_x=*/1L);
delete_file->referenced_data_file = file_a_->file_path;
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->ValidateFromSnapshot(starting_snapshot->snapshot_id);
std::vector<std::string> referenced_files{file_a_->file_path};
row_delta->ValidateDataFilesExist(referenced_files);
row_delta->ValidateDeletedFiles();
row_delta->AddDeletes(delete_file);
auto result = row_delta->Commit();
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
EXPECT_THAT(result, HasErrorMessage("Cannot commit, missing data files"));
EXPECT_THAT(result, HasErrorMessage(file_a_->file_path));
}
TEST_F(RowDeltaTest, CannotRemoveReferencedDataFile) {
AppendFileAToTable();
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
std::vector<std::string> referenced_files{file_a_->file_path};
row_delta->ValidateDataFilesExist(referenced_files);
row_delta->RemoveRows(file_a_);
auto result = row_delta->Commit();
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
EXPECT_THAT(result, HasErrorMessage("Cannot delete data files"));
EXPECT_THAT(result, HasErrorMessage(file_a_->file_path));
}
TEST_F(RowDeltaTest, AddDeleteFileForRemovedDataFileCommitsDeleteOperation) {
AppendFileAToTable();
auto delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
/*partition_x=*/1L);
delete_file->referenced_data_file = file_a_->file_path;
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->RemoveRows(file_a_);
row_delta->AddDeletes(delete_file);
EXPECT_THAT(row_delta->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
EXPECT_EQ(snapshot->Operation(), std::make_optional(DataOperation::kDelete));
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kDeletedDataFiles), "1");
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedDeleteFiles), "1");
}
TEST_F(RowDeltaTest, ValidateDeletedFilesAllowsMissingRowsOnEmptyTable) {
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->ValidateDeletedFiles();
row_delta->RemoveRows(file_a_);
EXPECT_THAT(row_delta->Commit(), IsOk());
}
TEST_F(RowDeltaTest, ValidateDeletedFilesAllowsMissingDeletesOnEmptyTable) {
auto delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
/*partition_x=*/1L);
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->ValidateDeletedFiles();
row_delta->RemoveDeletes(delete_file);
EXPECT_THAT(row_delta->Commit(), IsOk());
}
TEST_F(RowDeltaTest, AddDeletionVectorValidatesConcurrentDVs) {
AppendFileAToTable();
ICEBERG_UNWRAP_OR_FAIL(auto starting_snapshot, table_->current_snapshot());
SetTableFormatVersion(3);
auto concurrent_dv =
MakeDeletionVector("/delete/concurrent-dv-a.puffin", file_a_->file_path,
/*partition_x=*/1L, /*content_offset=*/0);
std::shared_ptr<RowDelta> concurrent_delta;
ICEBERG_UNWRAP_OR_FAIL(concurrent_delta, table_->NewRowDelta());
concurrent_delta->AddDeletes(concurrent_dv);
EXPECT_THAT(concurrent_delta->Commit(), IsOk());
EXPECT_THAT(table_->Refresh(), IsOk());
SetTableFormatVersion(3);
auto dv = MakeDeletionVector("/delete/dv-a.puffin", file_a_->file_path,
/*partition_x=*/1L, /*content_offset=*/10);
std::shared_ptr<RowDelta> row_delta;
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
row_delta->ValidateFromSnapshot(starting_snapshot->snapshot_id);
row_delta->AddDeletes(dv);
auto result = row_delta->Commit();
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
EXPECT_THAT(result, HasErrorMessage("Found concurrently added DV"));
EXPECT_THAT(result, HasErrorMessage(file_a_->file_path));
}
} // namespace iceberg