Skip to content

Commit 0c5f895

Browse files
manuzhangcodex
authored andcommitted
feat: add DeleteFiles update API
Extend MergingSnapshotProducer with a DeleteFiles API and expose it through table and transaction update flows. Add DeleteFiles coverage for path matching, case-insensitive row filters, empty delete commits, and strict-projection partial-match rejection. Co-authored-by: Codex <codex@openai.com>
1 parent e9a68b7 commit 0c5f895

12 files changed

Lines changed: 380 additions & 0 deletions

File tree

src/iceberg/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ set(ICEBERG_SOURCES
9999
transform.cc
100100
transform_function.cc
101101
type.cc
102+
update/delete_files.cc
102103
update/expire_snapshots.cc
103104
update/fast_append.cc
104105
update/merge_append.cc

src/iceberg/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ iceberg_sources = files(
124124
'transform.cc',
125125
'transform_function.cc',
126126
'type.cc',
127+
'update/delete_files.cc',
127128
'update/expire_snapshots.cc',
128129
'update/fast_append.cc',
129130
'update/merge_append.cc',

src/iceberg/table.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include "iceberg/table_properties.h"
3232
#include "iceberg/table_scan.h"
3333
#include "iceberg/transaction.h"
34+
#include "iceberg/update/delete_files.h"
3435
#include "iceberg/update/expire_snapshots.h"
3536
#include "iceberg/update/fast_append.h"
3637
#include "iceberg/update/merge_append.h"
@@ -224,6 +225,12 @@ Result<std::shared_ptr<MergeAppend>> Table::NewMergeAppend() {
224225
return MergeAppend::Make(name().name, std::move(ctx));
225226
}
226227

228+
Result<std::shared_ptr<DeleteFiles>> Table::NewDeleteFiles() {
229+
ICEBERG_ASSIGN_OR_RAISE(
230+
auto ctx, TransactionContext::Make(shared_from_this(), TransactionKind::kUpdate));
231+
return DeleteFiles::Make(name().name, std::move(ctx));
232+
}
233+
227234
Result<std::shared_ptr<UpdateStatistics>> Table::NewUpdateStatistics() {
228235
ICEBERG_ASSIGN_OR_RAISE(
229236
auto ctx, TransactionContext::Make(shared_from_this(), TransactionKind::kUpdate));

src/iceberg/table.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,9 @@ class ICEBERG_EXPORT Table : public std::enable_shared_from_this<Table> {
179179
/// \brief Create a new MergeAppend to append data files and merge manifests.
180180
virtual Result<std::shared_ptr<MergeAppend>> NewMergeAppend();
181181

182+
/// \brief Create a new DeleteFiles to delete data files and commit the changes.
183+
virtual Result<std::shared_ptr<DeleteFiles>> NewDeleteFiles();
184+
182185
/// \brief Create a new SnapshotManager to manage snapshots and snapshot references.
183186
virtual Result<std::shared_ptr<SnapshotManager>> NewSnapshotManager();
184187

src/iceberg/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ if(ICEBERG_BUILD_BUNDLE)
222222
add_iceberg_test(table_update_test
223223
USE_BUNDLE
224224
SOURCES
225+
delete_files_test.cc
225226
expire_snapshots_test.cc
226227
fast_append_test.cc
227228
manifest_filter_manager_test.cc
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include "iceberg/update/delete_files.h"
21+
22+
#include <memory>
23+
#include <string>
24+
#include <vector>
25+
26+
#include <gmock/gmock.h>
27+
#include <gtest/gtest.h>
28+
29+
#include "iceberg/avro/avro_register.h"
30+
#include "iceberg/expression/expressions.h"
31+
#include "iceberg/expression/literal.h"
32+
#include "iceberg/manifest/manifest_entry.h"
33+
#include "iceberg/partition_spec.h"
34+
#include "iceberg/row/partition_values.h"
35+
#include "iceberg/schema.h"
36+
#include "iceberg/snapshot.h"
37+
#include "iceberg/table.h"
38+
#include "iceberg/test/matchers.h"
39+
#include "iceberg/test/update_test_base.h"
40+
#include "iceberg/update/fast_append.h"
41+
42+
namespace iceberg {
43+
44+
class DeleteFilesTest : public MinimalUpdateTestBase {
45+
protected:
46+
static void SetUpTestSuite() { avro::RegisterAll(); }
47+
48+
void SetUp() override {
49+
MinimalUpdateTestBase::SetUp();
50+
51+
ICEBERG_UNWRAP_OR_FAIL(spec_, table_->spec());
52+
ICEBERG_UNWRAP_OR_FAIL(schema_, table_->schema());
53+
file_a_ = MakeDataFile("/data/file_a.parquet", /*partition_x=*/1L);
54+
file_b_ = MakeDataFile("/data/file_b.parquet", /*partition_x=*/2L);
55+
}
56+
57+
std::shared_ptr<DataFile> MakeDataFile(const std::string& path, int64_t partition_x) {
58+
auto file = std::make_shared<DataFile>();
59+
file->content = DataFile::Content::kData;
60+
file->file_path = table_location_ + path;
61+
file->file_format = FileFormatType::kParquet;
62+
file->partition = PartitionValues(std::vector<Literal>{Literal::Long(partition_x)});
63+
file->file_size_in_bytes = 1024;
64+
file->record_count = 100;
65+
file->partition_spec_id = spec_->spec_id();
66+
return file;
67+
}
68+
69+
void SetLongBounds(const std::shared_ptr<DataFile>& file, int32_t field_id,
70+
int64_t lower, int64_t upper) {
71+
ASSERT_NE(file, nullptr);
72+
ICEBERG_UNWRAP_OR_FAIL(auto lower_bound, Literal::Long(lower).Serialize());
73+
ICEBERG_UNWRAP_OR_FAIL(auto upper_bound, Literal::Long(upper).Serialize());
74+
file->value_counts[field_id] = file->record_count;
75+
file->null_value_counts[field_id] = 0;
76+
file->lower_bounds[field_id] = lower_bound;
77+
file->upper_bounds[field_id] = upper_bound;
78+
}
79+
80+
void CommitFiles(const std::vector<std::shared_ptr<DataFile>>& files) {
81+
ICEBERG_UNWRAP_OR_FAIL(auto append, table_->NewFastAppend());
82+
for (const auto& file : files) {
83+
append->AppendFile(file);
84+
}
85+
ASSERT_THAT(append->Commit(), IsOk());
86+
ASSERT_THAT(table_->Refresh(), IsOk());
87+
}
88+
89+
void CommitInitialFiles() { CommitFiles({file_a_, file_b_}); }
90+
91+
void ExpectOneFileDeleted() {
92+
ASSERT_THAT(table_->Refresh(), IsOk());
93+
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
94+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kOperation),
95+
DataOperation::kDelete);
96+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kDeletedDataFiles), "1");
97+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kDeletedRecords), "100");
98+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kRemovedFileSize), "1024");
99+
}
100+
101+
std::shared_ptr<PartitionSpec> spec_;
102+
std::shared_ptr<Schema> schema_;
103+
std::shared_ptr<DataFile> file_a_;
104+
std::shared_ptr<DataFile> file_b_;
105+
106+
static constexpr int32_t kYFieldId = 2;
107+
};
108+
109+
TEST_F(DeleteFilesTest, DeleteFileByPath) {
110+
CommitInitialFiles();
111+
112+
ICEBERG_UNWRAP_OR_FAIL(auto delete_files, table_->NewDeleteFiles());
113+
delete_files->DeleteFile(file_a_->file_path);
114+
115+
EXPECT_THAT(delete_files->Commit(), IsOk());
116+
ExpectOneFileDeleted();
117+
}
118+
119+
TEST_F(DeleteFilesTest, DeleteFileByDataFile) {
120+
CommitInitialFiles();
121+
122+
ICEBERG_UNWRAP_OR_FAIL(auto delete_files, table_->NewDeleteFiles());
123+
delete_files->DeleteFile(file_a_);
124+
125+
EXPECT_THAT(delete_files->Commit(), IsOk());
126+
ExpectOneFileDeleted();
127+
}
128+
129+
TEST_F(DeleteFilesTest, DeleteFromRowFilterCaseInsensitive) {
130+
CommitInitialFiles();
131+
132+
ICEBERG_UNWRAP_OR_FAIL(auto delete_files, table_->NewDeleteFiles());
133+
delete_files->CaseSensitive(false).DeleteFromRowFilter(
134+
Expressions::Equal("X", Literal::Long(1L)));
135+
136+
EXPECT_THAT(delete_files->Commit(), IsOk());
137+
ExpectOneFileDeleted();
138+
}
139+
140+
TEST_F(DeleteFilesTest, EmptyDeleteCommit) {
141+
CommitInitialFiles();
142+
ICEBERG_UNWRAP_OR_FAIL(auto previous_snapshot, table_->current_snapshot());
143+
144+
ICEBERG_UNWRAP_OR_FAIL(auto delete_files, table_->NewDeleteFiles());
145+
146+
EXPECT_THAT(delete_files->Commit(), IsOk());
147+
148+
ASSERT_THAT(table_->Refresh(), IsOk());
149+
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
150+
ASSERT_TRUE(snapshot->parent_snapshot_id.has_value());
151+
EXPECT_EQ(snapshot->parent_snapshot_id.value(), previous_snapshot->snapshot_id);
152+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kOperation),
153+
DataOperation::kDelete);
154+
EXPECT_EQ(snapshot->summary.count(SnapshotSummaryFields::kDeletedDataFiles), 0U);
155+
EXPECT_EQ(snapshot->summary.count(SnapshotSummaryFields::kDeletedRecords), 0U);
156+
EXPECT_EQ(snapshot->summary.count(SnapshotSummaryFields::kRemovedFileSize), 0U);
157+
}
158+
159+
TEST_F(DeleteFilesTest, DeleteFromRowFilter) {
160+
CommitInitialFiles();
161+
162+
ICEBERG_UNWRAP_OR_FAIL(auto delete_files, table_->NewDeleteFiles());
163+
delete_files->DeleteFromRowFilter(Expressions::Equal("x", Literal::Long(1L)));
164+
165+
EXPECT_THAT(delete_files->Commit(), IsOk());
166+
ExpectOneFileDeleted();
167+
}
168+
169+
TEST_F(DeleteFilesTest, DeleteFromRowFilterRejectsPartialMatchFile) {
170+
auto partial_match_file = MakeDataFile("/data/partial_match.parquet",
171+
/*partition_x=*/1L);
172+
SetLongBounds(partial_match_file, kYFieldId, /*lower=*/0L, /*upper=*/10L);
173+
CommitFiles({partial_match_file});
174+
ICEBERG_UNWRAP_OR_FAIL(auto previous_snapshot, table_->current_snapshot());
175+
176+
ICEBERG_UNWRAP_OR_FAIL(auto delete_files, table_->NewDeleteFiles());
177+
delete_files->DeleteFromRowFilter(Expressions::Equal("y", Literal::Long(5L)));
178+
179+
auto status = delete_files->Commit();
180+
EXPECT_THAT(status, IsError(ErrorKind::kValidationFailed));
181+
EXPECT_THAT(status,
182+
HasErrorMessage("Cannot delete file where some, but not all, rows match "
183+
"filter"));
184+
EXPECT_THAT(status, HasErrorMessage(partial_match_file->file_path));
185+
186+
ASSERT_THAT(table_->Refresh(), IsOk());
187+
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
188+
EXPECT_EQ(snapshot->snapshot_id, previous_snapshot->snapshot_id);
189+
}
190+
191+
TEST_F(DeleteFilesTest, ValidateFilesExistRejectsMissingPath) {
192+
CommitInitialFiles();
193+
194+
ICEBERG_UNWRAP_OR_FAIL(auto delete_files, table_->NewDeleteFiles());
195+
delete_files->DeleteFile(table_location_ + "/data/missing.parquet")
196+
.ValidateFilesExist();
197+
198+
auto status = delete_files->Commit();
199+
EXPECT_THAT(status, IsError(ErrorKind::kValidationFailed));
200+
EXPECT_THAT(status, HasErrorMessage("Missing required files to delete"));
201+
}
202+
203+
} // namespace iceberg

src/iceberg/transaction.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "iceberg/table_requirement.h"
3333
#include "iceberg/table_requirements.h"
3434
#include "iceberg/table_update.h"
35+
#include "iceberg/update/delete_files.h"
3536
#include "iceberg/update/expire_snapshots.h"
3637
#include "iceberg/update/fast_append.h"
3738
#include "iceberg/update/merge_append.h"
@@ -497,6 +498,13 @@ Result<std::shared_ptr<MergeAppend>> Transaction::NewMergeAppend() {
497498
return merge_append;
498499
}
499500

501+
Result<std::shared_ptr<DeleteFiles>> Transaction::NewDeleteFiles() {
502+
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<DeleteFiles> delete_files,
503+
DeleteFiles::Make(ctx_->table->name().name, ctx_));
504+
ICEBERG_RETURN_UNEXPECTED(AddUpdate(delete_files));
505+
return delete_files;
506+
}
507+
500508
Result<std::shared_ptr<UpdateStatistics>> Transaction::NewUpdateStatistics() {
501509
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<UpdateStatistics> update_statistics,
502510
UpdateStatistics::Make(ctx_));

src/iceberg/transaction.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ class ICEBERG_EXPORT Transaction : public std::enable_shared_from_this<Transacti
109109
/// \brief Create a new MergeAppend to append data files and merge manifests.
110110
Result<std::shared_ptr<MergeAppend>> NewMergeAppend();
111111

112+
/// \brief Create a new DeleteFiles to delete data files and commit the changes.
113+
Result<std::shared_ptr<DeleteFiles>> NewDeleteFiles();
114+
112115
/// \brief Create a new SnapshotManager to manage snapshots.
113116
Result<std::shared_ptr<SnapshotManager>> NewSnapshotManager();
114117

src/iceberg/type_fwd.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ class Transaction;
223223
class TransactionContext;
224224

225225
/// \brief Update family.
226+
class DeleteFiles;
226227
class ExpireSnapshots;
227228
class FastAppend;
228229
class MergeAppend;

src/iceberg/update/delete_files.cc

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include "iceberg/update/delete_files.h"
21+
22+
#include <memory>
23+
#include <string>
24+
#include <string_view>
25+
26+
#include "iceberg/snapshot.h"
27+
#include "iceberg/transaction.h"
28+
#include "iceberg/util/error_collector.h"
29+
#include "iceberg/util/macros.h"
30+
31+
namespace iceberg {
32+
33+
Result<std::unique_ptr<DeleteFiles>> DeleteFiles::Make(
34+
std::string table_name, std::shared_ptr<TransactionContext> ctx) {
35+
ICEBERG_PRECHECK(!table_name.empty(), "Table name cannot be empty");
36+
ICEBERG_PRECHECK(ctx != nullptr, "Cannot create DeleteFiles without a context");
37+
return std::unique_ptr<DeleteFiles>(
38+
new DeleteFiles(std::move(table_name), std::move(ctx)));
39+
}
40+
41+
DeleteFiles::DeleteFiles(std::string table_name, std::shared_ptr<TransactionContext> ctx)
42+
: MergingSnapshotUpdate(std::move(table_name), std::move(ctx)) {}
43+
44+
DeleteFiles& DeleteFiles::DeleteFile(std::string_view path) {
45+
ICEBERG_BUILDER_CHECK(!path.empty(), "Cannot delete an empty file path");
46+
ICEBERG_BUILDER_RETURN_IF_ERROR(DeleteByPath(path));
47+
return *this;
48+
}
49+
50+
DeleteFiles& DeleteFiles::DeleteFile(const std::shared_ptr<DataFile>& file) {
51+
ICEBERG_BUILDER_RETURN_IF_ERROR(DeleteDataFile(file));
52+
return *this;
53+
}
54+
55+
DeleteFiles& DeleteFiles::DeleteFromRowFilter(std::shared_ptr<Expression> expr) {
56+
ICEBERG_BUILDER_RETURN_IF_ERROR(DeleteByRowFilter(std::move(expr)));
57+
return *this;
58+
}
59+
60+
DeleteFiles& DeleteFiles::CaseSensitive(bool case_sensitive) {
61+
MergingSnapshotUpdate::CaseSensitive(case_sensitive);
62+
return *this;
63+
}
64+
65+
DeleteFiles& DeleteFiles::ValidateFilesExist() {
66+
validate_files_to_delete_exist_ = true;
67+
return *this;
68+
}
69+
70+
std::string DeleteFiles::operation() { return DataOperation::kDelete; }
71+
72+
Status DeleteFiles::Validate(const TableMetadata&, const std::shared_ptr<Snapshot>&) {
73+
if (validate_files_to_delete_exist_) {
74+
FailMissingDeletePaths();
75+
}
76+
return {};
77+
}
78+
79+
} // namespace iceberg

0 commit comments

Comments
 (0)