5050
5151namespace 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).
5855class 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.
414395TEST_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+
479483TEST_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.
516518TEST_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.
548548TEST_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.
567566TEST_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.
582579TEST_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.
600596TEST_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-
620612TEST_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.
630620TEST_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.
640629TEST_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.
668656TEST_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.
705690TEST_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.
726709TEST_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.
769751TEST_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.
786766TEST_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).
805780TEST_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.
910882TEST_F (OverwriteFilesTest, PropertyConflictFilterResolution) {
911883 {
912884 const int64_t first_id = CommitFileA ();
0 commit comments