Skip to content

Commit b93b4da

Browse files
shuxu.liwgtmac
authored andcommitted
fix(update): Fix OverwriteFiles data/delete content validation for direct AddFile/DeleteFile paths and trim overly verbose test comments.
1 parent 9d3ba45 commit b93b4da

2 files changed

Lines changed: 42 additions & 64 deletions

File tree

src/iceberg/test/overwrite_files_test.cc

Lines changed: 36 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,8 @@
5050

5151
namespace iceberg {
5252

53-
// Test harness for OverwriteFiles.
54-
//
5553
// The base table (TableMetadataV2ValidMinimal.json) has schema {x: long (id 1),
56-
// y: long (id 2), z: long (id 3)} and a single partition spec (spec id 0) that
57-
// partitions by identity(x).
54+
// y: long (id 2), z: long (id 3)} and partitions by identity(x).
5855
class OverwriteFilesTest : public UpdateTestBase {
5956
protected:
6057
static void SetUpTestSuite() { avro::RegisterAll(); }
@@ -73,7 +70,6 @@ class OverwriteFilesTest : public UpdateTestBase {
7370
file_b_ = MakeDataFile("/data/file_b.parquet", /*partition_x=*/2L);
7471
}
7572

76-
// A plain data file in spec 0 (identity(x)) with the given partition value for x.
7773
std::shared_ptr<DataFile> MakeDataFile(const std::string& path, int64_t partition_x,
7874
int64_t record_count = 100) {
7975
auto f = std::make_shared<DataFile>();
@@ -87,8 +83,7 @@ class OverwriteFilesTest : public UpdateTestBase {
8783
return f;
8884
}
8985

90-
// A data file carrying column metrics for column y (field id 2): lower/upper bounds
91-
// plus value/null counts, so the StrictMetricsEvaluator can reason about it.
86+
// Add y metrics so StrictMetricsEvaluator can prove row-filter containment.
9287
std::shared_ptr<DataFile> MakeDataFileWithYBounds(const std::string& path,
9388
int64_t partition_x, int64_t y_lower,
9489
int64_t y_upper) {
@@ -106,7 +101,6 @@ class OverwriteFilesTest : public UpdateTestBase {
106101
return f;
107102
}
108103

109-
// An equality delete file in spec 0 (partition x = partition_x).
110104
std::shared_ptr<DataFile> MakeEqualityDeleteFile(const std::string& path,
111105
int64_t partition_x) {
112106
auto f = MakeDeleteFile(path, partition_x);
@@ -115,9 +109,7 @@ class OverwriteFilesTest : public UpdateTestBase {
115109
return f;
116110
}
117111

118-
// Write a delete manifest containing the given delete files, with the snapshot id and
119-
// sequence number assigned on each entry (so the manifest list writer does not need to
120-
// inherit them).
112+
// Entries carry assigned snapshot and sequence numbers.
121113
Result<ManifestFile> WriteDeleteManifest(
122114
const std::string& path, const std::vector<std::shared_ptr<DataFile>>& files,
123115
int64_t snapshot_id, int64_t sequence_number) {
@@ -137,8 +129,6 @@ class OverwriteFilesTest : public UpdateTestBase {
137129
return writer->ToManifestFile();
138130
}
139131

140-
// Build a synthetic snapshot from the given manifests (mirrors the merging-update test
141-
// harness). Used to inject a concurrent commit between read and validate.
142132
Result<std::shared_ptr<Snapshot>> MakeSyntheticSnapshot(
143133
std::string operation, int64_t snapshot_id,
144134
std::optional<int64_t> parent_snapshot_id, int64_t sequence_number,
@@ -161,10 +151,7 @@ class OverwriteFilesTest : public UpdateTestBase {
161151
return std::shared_ptr<Snapshot>(std::move(snapshot));
162152
}
163153

164-
// Inject a concurrent snapshot that adds an equality delete file covering partition
165-
// `partition_x`, layered on top of `parent`. Returns the metadata containing the new
166-
// snapshot (as current) plus the new snapshot itself, so a subsequent Validate(...) can
167-
// scan the range (parent, new] for new deletes.
154+
// Build metadata with a synthetic concurrent equality delete after `parent`.
168155
struct ConcurrentDelete {
169156
std::shared_ptr<TableMetadata> metadata;
170157
std::shared_ptr<Snapshot> snapshot;
@@ -199,7 +186,6 @@ class OverwriteFilesTest : public UpdateTestBase {
199186
return table_->NewOverwrite();
200187
}
201188

202-
// Commit file_a_ with FastAppend and refresh the table; returns its snapshot id.
203189
int64_t CommitFileA() {
204190
auto fa = table_->NewFastAppend();
205191
EXPECT_TRUE(fa.has_value());
@@ -211,7 +197,6 @@ class OverwriteFilesTest : public UpdateTestBase {
211197
return snap.value()->snapshot_id;
212198
}
213199

214-
// Append a single file via FastAppend and refresh; returns the new snapshot.
215200
std::shared_ptr<Snapshot> CommitFastAppend(const std::shared_ptr<DataFile>& file) {
216201
auto fa = table_->NewFastAppend();
217202
EXPECT_TRUE(fa.has_value());
@@ -406,11 +391,7 @@ TEST_F(OverwriteFilesTest, CommitCustomSummaryProperty) {
406391
EXPECT_EQ(snapshot->summary.at("custom-prop"), "custom-value");
407392
}
408393

409-
// A DataFileSet + DeleteFileSet forwards data files to DeleteDataFile and delete files
410-
// to DeleteDeleteFile; the committed snapshot reflects the data-file removal. (The
411-
// delete file is forwarded to DeleteDeleteFile; with no matching committed delete file
412-
// present its removal is a harmless no-op, mirroring the inherited missing-delete
413-
// behavior.)
394+
// With no matching committed delete file, deleting `del_file` is a harmless no-op.
414395
TEST_F(OverwriteFilesTest, BulkDeleteFilesRemovesDataAndDeleteFiles) {
415396
{
416397
ICEBERG_UNWRAP_OR_FAIL(auto seed, NewOverwrite());
@@ -475,7 +456,30 @@ TEST_F(OverwriteFilesTest, BulkDeleteFilesEquivalentToRepeatedDeleteFile) {
475456
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kTotalDataFiles), "0");
476457
}
477458

478-
// DeleteFiles validates content because both input sets store DataFile pointers.
459+
// OverwriteFiles validates content because the C++ API stores data and delete files in
460+
// DataFile pointers, while Java uses separate DataFile/DeleteFile types.
461+
TEST_F(OverwriteFilesTest, AddFileRejectsDeleteFileContent) {
462+
auto del_file = MakeDeleteFile("/delete/del_as_data.parquet", 1L);
463+
464+
ICEBERG_UNWRAP_OR_FAIL(auto op, NewOverwrite());
465+
op->AddFile(del_file);
466+
auto result = op->Commit();
467+
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
468+
EXPECT_THAT(result, HasErrorMessage("Invalid data file to add"));
469+
EXPECT_THAT(result, HasErrorMessage("has delete-file content"));
470+
}
471+
472+
TEST_F(OverwriteFilesTest, DeleteFileRejectsDeleteFileContent) {
473+
auto del_file = MakeDeleteFile("/delete/del_as_delete.parquet", 1L);
474+
475+
ICEBERG_UNWRAP_OR_FAIL(auto op, NewOverwrite());
476+
op->DeleteFile(del_file);
477+
auto result = op->Commit();
478+
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
479+
EXPECT_THAT(result, HasErrorMessage("Invalid data file to delete"));
480+
EXPECT_THAT(result, HasErrorMessage("has delete-file content"));
481+
}
482+
479483
TEST_F(OverwriteFilesTest, BulkDeleteFilesRejectsDeleteFileInDataSet) {
480484
auto del_file =
481485
MakeDeleteFile("/delete/del_a.parquet", 1L); // content = positionDeletes
@@ -511,8 +515,6 @@ TEST_F(OverwriteFilesTest, BulkDeleteFilesAcceptsEqualityDeleteInDeleteSet) {
511515
EXPECT_THAT(op->Commit(), IsOk());
512516
}
513517

514-
// ValidateNoConflictingData: a competing FastAppend that added a data file matching the
515-
// resolved conflict-detection filter makes the overwrite commit fail.
516518
TEST_F(OverwriteFilesTest, ValidateNoConflictingDataDetectsConflictingAdd) {
517519
const int64_t first_id = CommitFileA();
518520
CommitFastAppend(file_b_);
@@ -543,8 +545,6 @@ TEST_F(OverwriteFilesTest, ValidateNoConflictingDataAllowsNonConflictingChange)
543545
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kOperation), expected_operation);
544546
}
545547

546-
// ValidateNoConflictingDeletes: a competing snapshot that deleted a data file in the
547-
// overwrite range makes the commit fail.
548548
TEST_F(OverwriteFilesTest, ValidateNoConflictingDeletesDetectsConflictingDelete) {
549549
const int64_t first_id = CommitFileA();
550550

@@ -563,10 +563,8 @@ TEST_F(OverwriteFilesTest, ValidateNoConflictingDeletesDetectsConflictingDelete)
563563
EXPECT_THAT(op->Commit(), IsError(ErrorKind::kValidationFailed));
564564
}
565565

566-
// ValidateNoConflictingDeletes allows a non-conflicting concurrent append to commit.
567566
TEST_F(OverwriteFilesTest, ValidateNoConflictingDeletesAllowsNonConflictingChange) {
568567
const int64_t first_id = CommitFileA();
569-
// Competing append in partition x=2 (no deletes in the x=1 range).
570568
CommitFastAppend(file_b_);
571569

572570
ICEBERG_UNWRAP_OR_FAIL(auto op, NewOverwrite());
@@ -577,8 +575,7 @@ TEST_F(OverwriteFilesTest, ValidateNoConflictingDeletesAllowsNonConflictingChang
577575
EXPECT_THAT(op->Commit(), IsOk());
578576
}
579577

580-
// Explicit replaced-file validation applies ConflictDetectionFilter to concurrent delete
581-
// files that cover replaced data files.
578+
// Explicit replaced-file validation checks concurrent deletes covering replaced files.
582579
TEST_F(OverwriteFilesTest, PathBExplicitDeletesDetectsConcurrentDeleteWithoutFilter) {
583580
CommitFileA();
584581
ICEBERG_UNWRAP_OR_FAIL(auto first_snapshot, table_->current_snapshot());
@@ -595,8 +592,7 @@ TEST_F(OverwriteFilesTest, PathBExplicitDeletesDetectsConcurrentDeleteWithoutFil
595592
IsError(ErrorKind::kValidationFailed));
596593
}
597594

598-
// A conflict filter that does not cover the replaced file's partition narrows the scope,
599-
// so the concurrent delete is filtered out and validation succeeds.
595+
// A narrower conflict filter can exclude the concurrent delete.
600596
TEST_F(OverwriteFilesTest, PathBExplicitDeletesConflictFilterNarrowsScope) {
601597
CommitFileA();
602598
ICEBERG_UNWRAP_OR_FAIL(auto first_snapshot, table_->current_snapshot());
@@ -613,10 +609,6 @@ TEST_F(OverwriteFilesTest, PathBExplicitDeletesConflictFilterNarrowsScope) {
613609
EXPECT_THAT(op->Validate(*concurrent.metadata, concurrent.snapshot), IsOk());
614610
}
615611

616-
// These exercise OverwriteFiles::Validate(...) directly (the same entry point the commit
617-
// kernel invokes), which is sufficient and deterministic: the strict-range branch does
618-
// not depend on concurrent snapshots.
619-
620612
TEST_F(OverwriteFilesTest, StrictRangeAcceptedByStrictProjection) {
621613
ICEBERG_UNWRAP_OR_FAIL(auto op, NewOverwrite());
622614
op->OverwriteByRowFilter(Expressions::Equal("x", Literal::Long(1L)));
@@ -625,8 +617,6 @@ TEST_F(OverwriteFilesTest, StrictRangeAcceptedByStrictProjection) {
625617
EXPECT_THAT(op->Validate(*table_->metadata(), /*snapshot=*/nullptr), IsOk());
626618
}
627619

628-
// Strict partition projection is insufficient (filter on a non-partition column) but the
629-
// StrictMetricsEvaluator proves containment from the file's bounds.
630620
TEST_F(OverwriteFilesTest, StrictRangeAcceptedByStrictMetrics) {
631621
ICEBERG_UNWRAP_OR_FAIL(auto op, NewOverwrite());
632622
op->OverwriteByRowFilter(Expressions::Equal("y", Literal::Long(5L)));
@@ -636,7 +626,6 @@ TEST_F(OverwriteFilesTest, StrictRangeAcceptedByStrictMetrics) {
636626
EXPECT_THAT(op->Validate(*table_->metadata(), /*snapshot=*/nullptr), IsOk());
637627
}
638628

639-
// Neither the strict projection nor the metrics can prove containment.
640629
TEST_F(OverwriteFilesTest, StrictRangeRejectedWhenNotProvable) {
641630
ICEBERG_UNWRAP_OR_FAIL(auto op, NewOverwrite());
642631
op->OverwriteByRowFilter(Expressions::Equal("y", Literal::Long(5L)));
@@ -664,10 +653,8 @@ TEST_F(OverwriteFilesTest, StrictRangeRequiresRowFilter) {
664653
IsError(ErrorKind::kValidationFailed));
665654
}
666655

667-
// ValidateAddedFilesMatchOverwriteFilter resolves one data spec for all added files.
668656
TEST_F(OverwriteFilesTest, StrictRangeRejectsMultiplePartitionSpecs) {
669-
// Add a second partition spec before creating the builder,
670-
// so the producer's base metadata can resolve both specs when files are staged.
657+
// Add the second spec before creating the builder so staged files can resolve it.
671658
ICEBERG_UNWRAP_OR_FAIL(
672659
auto spec1, PartitionSpec::Make(*schema_, /*spec_id=*/1,
673660
{PartitionField(/*source_id=*/1, /*field_id=*/1001,
@@ -699,9 +686,7 @@ TEST_F(OverwriteFilesTest, StrictRangeRejectsEmptyAddedFiles) {
699686
IsError(ErrorKind::kInvalidArgument));
700687
}
701688

702-
// Case-insensitive binding accepts a differently-cased column name where the
703-
// case-sensitive (default) binding rejects it. Observed through the strict-range
704-
// validation, which binds the row filter using the configured case sensitivity.
689+
// Strict-range validation binds the row filter with the configured case sensitivity.
705690
TEST_F(OverwriteFilesTest, CaseSensitivityAffectsFilterBinding) {
706691
{
707692
ICEBERG_UNWRAP_OR_FAIL(auto op, NewOverwrite());
@@ -721,8 +706,6 @@ TEST_F(OverwriteFilesTest, CaseSensitivityAffectsFilterBinding) {
721706
}
722707
}
723708

724-
// Null arguments to the pointer-taking builder methods surface at Commit() without
725-
// crashing.
726709
TEST_F(OverwriteFilesTest, NullAddFileRejectedAtCommit) {
727710
ICEBERG_UNWRAP_OR_FAIL(auto op, NewOverwrite());
728711
op->AddFile(nullptr);
@@ -764,8 +747,7 @@ TEST_F(OverwriteFilesTest, NullArgumentDoesNotCrashBuilderChain) {
764747
EXPECT_THAT(result, HasErrorMessage("Invalid data file: null"));
765748
}
766749

767-
// A null element cannot enter a DataFileSet / DeleteFileSet (insert() rejects nullptr),
768-
// so DeleteFiles({null...}, {null...}) is a no-op rather than an error.
750+
// DataFileSet/DeleteFileSet reject nullptr on insert.
769751
TEST_F(OverwriteFilesTest, DeleteFilesNullElementsCannotEnterSets) {
770752
DataFileSet data_files;
771753
data_files.insert(std::shared_ptr<DataFile>{nullptr});
@@ -780,9 +762,7 @@ TEST_F(OverwriteFilesTest, DeleteFilesNullElementsCannotEnterSets) {
780762
EXPECT_THAT(op->Commit(), IsOk());
781763
}
782764

783-
// ValidateFromSnapshot accepts a non-negative id (0 is in the generated id range
784-
// [0, INT64_MAX]) and rejects a negative id (including kInvalidSnapshotId == -1) as a
785-
// deferred error surfaced at Commit().
765+
// Snapshot id 0 is valid; negative ids are rejected as builder errors.
786766
TEST_F(OverwriteFilesTest, ValidateFromSnapshotRejectsNegativeSnapshotId) {
787767
ICEBERG_UNWRAP_OR_FAIL(auto op, NewOverwrite());
788768
op->AddFile(file_a_).ValidateFromSnapshot(-1);
@@ -797,11 +777,6 @@ TEST_F(OverwriteFilesTest, ValidateFromSnapshotAcceptsZeroSnapshotId) {
797777
EXPECT_THAT(op->Commit(), IsOk());
798778
}
799779

800-
// AddedDataFiles() / deleted_data_files_ are not publicly observable, so the properties
801-
// are checked indirectly through operation(): for any non-null file, AddFile-only yields
802-
// `append` (the file was added and the row filter is untouched), while DeleteFile-only
803-
// and DeleteFiles-only (data portion) yield `delete` (the file was registered for
804-
// deletion and DeletesDataFiles() became true).
805780
TEST_F(OverwriteFilesTest, PropertyBuilderForwardingAndDualTracking) {
806781
const std::vector<std::shared_ptr<DataFile>> files = {
807782
MakeDataFile("/data/p0.parquet", 1L),
@@ -903,10 +878,7 @@ TEST_F(OverwriteFilesTest, PropertyOperationTruthTable) {
903878
}
904879
}
905880

906-
// DataConflictDetectionFilter() is private, so its three resolution outcomes are observed
907-
// indirectly through ValidateNoConflictingData against a competing concurrent add of
908-
// file_b (partition x=2):
909-
// explicit filter, row filter only, and explicit file replacement.
881+
// Exercise explicit filter, row-filter-only, and explicit file replacement resolution.
910882
TEST_F(OverwriteFilesTest, PropertyConflictFilterResolution) {
911883
{
912884
const int64_t first_id = CommitFileA();

src/iceberg/update/overwrite_files.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,18 @@ OverwriteFiles::~OverwriteFiles() = default;
5656

5757
OverwriteFiles& OverwriteFiles::AddFile(const std::shared_ptr<DataFile>& file) {
5858
ICEBERG_BUILDER_CHECK(file != nullptr, "Invalid data file: null");
59+
ICEBERG_BUILDER_CHECK(file->content == DataFile::Content::kData,
60+
"Invalid data file to add: {} has delete-file content",
61+
file->file_path);
5962
ICEBERG_BUILDER_RETURN_IF_ERROR(AddDataFile(file));
6063
return *this;
6164
}
6265

6366
OverwriteFiles& OverwriteFiles::DeleteFile(const std::shared_ptr<DataFile>& file) {
6467
ICEBERG_BUILDER_CHECK(file != nullptr, "Invalid data file: null");
68+
ICEBERG_BUILDER_CHECK(file->content == DataFile::Content::kData,
69+
"Invalid data file to delete: {} has delete-file content",
70+
file->file_path);
6571
deleted_data_files_.insert(file);
6672
ICEBERG_BUILDER_RETURN_IF_ERROR(DeleteDataFile(file));
6773
return *this;

0 commit comments

Comments
 (0)