@@ -53,13 +53,17 @@ ReplacePartitions& ReplacePartitions::AddFile(const std::shared_ptr<DataFile>& f
5353 ICEBERG_BUILDER_ASSIGN_OR_RETURN (auto spec, base ().PartitionSpecById (spec_id));
5454
5555 ICEBERG_BUILDER_RETURN_IF_ERROR (AddDataFile (file));
56- // DropPartition(spec_id, partition) registers the (spec_id, partition_values)
57- // tuple with both data and delete filter managers. For an unpartitioned spec
58- // the partition values are empty and naturally match every file under that
59- // spec — no separate AlwaysTrue path is needed, and validation stays scoped
60- // to the spec rather than the whole table.
61- ICEBERG_BUILDER_RETURN_IF_ERROR (DropPartition (spec_id, file->partition ));
62- replaced_partitions_.add (spec_id, file->partition );
56+ if (spec->fields ().empty ()) {
57+ // Unpartitioned spec: Java's BaseReplacePartitions treats this as a
58+ // table-wide replace rather than a spec-scoped DropPartition with empty
59+ // partition values. Mirror that so every existing data file is dropped
60+ // and conflict validation runs against AlwaysTrue.
61+ ICEBERG_BUILDER_RETURN_IF_ERROR (DeleteByRowFilter (Expressions::AlwaysTrue ()));
62+ replace_by_row_filter_ = true ;
63+ } else {
64+ ICEBERG_BUILDER_RETURN_IF_ERROR (DropPartition (spec_id, file->partition ));
65+ replaced_partitions_.add (spec_id, file->partition );
66+ }
6367 return *this ;
6468}
6569
@@ -90,21 +94,43 @@ Status ReplacePartitions::Validate(const TableMetadata& current_metadata,
9094 if (snapshot == nullptr ) {
9195 return {};
9296 }
93- // No-op update: no partitions were staged, so there is nothing to conflict
94- // with. Calling the validators with AlwaysTrue here would turn an empty
95- // builder into a full-table conflict check.
96- if (replaced_partitions_.empty ()) {
97+ // No-op update: no partitions were staged and no table-wide replace was
98+ // requested, so there is nothing to conflict with. Calling the validators
99+ // with AlwaysTrue here would turn an empty builder into a full-table check.
100+ if (!replace_by_row_filter_ && replaced_partitions_.empty ()) {
97101 return {};
98102 }
99103
100104 auto io = ctx_->table ->io ();
101105 if (validate_conflicting_data_) {
102- ICEBERG_RETURN_UNEXPECTED (ValidateAddedDataFiles (
103- current_metadata, starting_snapshot_id_, replaced_partitions_, snapshot, io));
106+ if (replace_by_row_filter_) {
107+ ICEBERG_RETURN_UNEXPECTED (ValidateAddedDataFiles (
108+ current_metadata, starting_snapshot_id_, Expressions::AlwaysTrue (), snapshot,
109+ io, IsCaseSensitive ()));
110+ } else {
111+ ICEBERG_RETURN_UNEXPECTED (ValidateAddedDataFiles (
112+ current_metadata, starting_snapshot_id_, replaced_partitions_, snapshot, io));
113+ }
104114 }
105115 if (validate_conflicting_deletes_) {
106- ICEBERG_RETURN_UNEXPECTED (ValidateNoNewDeleteFiles (
107- current_metadata, starting_snapshot_id_, replaced_partitions_, snapshot, io));
116+ // Java's BaseReplacePartitions.validate gates both ValidateNoNewDeleteFiles
117+ // and ValidateDeletedDataFiles on the same validateNewDeletes flag. The
118+ // second check rejects concurrent overwrite/delete commits in the replaced
119+ // partitions; without it a concurrent delete in a replaced partition would
120+ // commit silently.
121+ if (replace_by_row_filter_) {
122+ ICEBERG_RETURN_UNEXPECTED (ValidateNoNewDeleteFiles (
123+ current_metadata, starting_snapshot_id_, Expressions::AlwaysTrue (), snapshot,
124+ io, IsCaseSensitive ()));
125+ ICEBERG_RETURN_UNEXPECTED (ValidateDeletedDataFiles (
126+ current_metadata, starting_snapshot_id_, Expressions::AlwaysTrue (), snapshot,
127+ io, IsCaseSensitive ()));
128+ } else {
129+ ICEBERG_RETURN_UNEXPECTED (ValidateNoNewDeleteFiles (
130+ current_metadata, starting_snapshot_id_, replaced_partitions_, snapshot, io));
131+ ICEBERG_RETURN_UNEXPECTED (ValidateDeletedDataFiles (
132+ current_metadata, starting_snapshot_id_, replaced_partitions_, snapshot, io));
133+ }
108134 }
109135 return {};
110136}
0 commit comments