Skip to content

Commit 0dea7d5

Browse files
manuzhangcodex
andcommitted
feat: add row delta update
Implements the RowDelta update builder, table and transaction factory methods, and focused tests for row-level add/delete flows. Co-authored-by: Codex <codex@openai.com>
1 parent ae29c3d commit 0dea7d5

13 files changed

Lines changed: 620 additions & 6 deletions

src/iceberg/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ set(ICEBERG_SOURCES
9898
update/fast_append.cc
9999
update/merging_snapshot_update.cc
100100
update/pending_update.cc
101+
update/row_delta.cc
101102
update/set_snapshot.cc
102103
update/snapshot_manager.cc
103104
update/snapshot_update.cc

src/iceberg/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ iceberg_sources = files(
120120
'update/fast_append.cc',
121121
'update/merging_snapshot_update.cc',
122122
'update/pending_update.cc',
123+
'update/row_delta.cc',
123124
'update/set_snapshot.cc',
124125
'update/snapshot_manager.cc',
125126
'update/snapshot_update.cc',

src/iceberg/table.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include "iceberg/transaction.h"
3434
#include "iceberg/update/expire_snapshots.h"
3535
#include "iceberg/update/fast_append.h"
36+
#include "iceberg/update/row_delta.h"
3637
#include "iceberg/update/set_snapshot.h"
3738
#include "iceberg/update/snapshot_manager.h"
3839
#include "iceberg/update/update_location.h"
@@ -217,6 +218,12 @@ Result<std::shared_ptr<FastAppend>> Table::NewFastAppend() {
217218
return FastAppend::Make(name().name, std::move(ctx));
218219
}
219220

221+
Result<std::shared_ptr<RowDelta>> Table::NewRowDelta() {
222+
ICEBERG_ASSIGN_OR_RAISE(
223+
auto ctx, TransactionContext::Make(shared_from_this(), TransactionKind::kUpdate));
224+
return RowDelta::Make(name().name, std::move(ctx));
225+
}
226+
220227
Result<std::shared_ptr<UpdateStatistics>> Table::NewUpdateStatistics() {
221228
ICEBERG_ASSIGN_OR_RAISE(
222229
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
@@ -176,6 +176,9 @@ class ICEBERG_EXPORT Table : public std::enable_shared_from_this<Table> {
176176
/// \brief Create a new FastAppend to append data files and commit the changes.
177177
virtual Result<std::shared_ptr<FastAppend>> NewFastAppend();
178178

179+
/// \brief Create a new RowDelta to add rows and row-level deletes.
180+
virtual Result<std::shared_ptr<RowDelta>> NewRowDelta();
181+
179182
/// \brief Create a new SnapshotManager to manage snapshots and snapshot references.
180183
virtual Result<std::shared_ptr<SnapshotManager>> NewSnapshotManager();
181184

src/iceberg/test/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ if(ICEBERG_BUILD_BUNDLE)
220220
manifest_filter_manager_test.cc
221221
merging_snapshot_update_test.cc
222222
name_mapping_update_test.cc
223+
row_delta_test.cc
223224
snapshot_manager_test.cc
224225
transaction_test.cc
225226
update_location_test.cc

src/iceberg/test/row_delta_test.cc

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
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/row_delta.h"
21+
22+
#include <memory>
23+
#include <optional>
24+
#include <string>
25+
#include <vector>
26+
27+
#include <gmock/gmock.h>
28+
#include <gtest/gtest.h>
29+
30+
#include "iceberg/avro/avro_register.h"
31+
#include "iceberg/manifest/manifest_entry.h"
32+
#include "iceberg/partition_spec.h"
33+
#include "iceberg/row/partition_values.h"
34+
#include "iceberg/schema.h"
35+
#include "iceberg/snapshot.h"
36+
#include "iceberg/table.h"
37+
#include "iceberg/table_metadata.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 RowDeltaTest : 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+
54+
file_a_ = MakeDataFile("/data/file_a.parquet", /*partition_x=*/1L);
55+
file_b_ = MakeDataFile("/data/file_b.parquet", /*partition_x=*/2L);
56+
}
57+
58+
std::shared_ptr<DataFile> MakeDataFile(const std::string& path, int64_t partition_x) {
59+
auto file = std::make_shared<DataFile>();
60+
file->content = DataFile::Content::kData;
61+
file->file_path = table_location_ + path;
62+
file->file_format = FileFormatType::kParquet;
63+
file->partition = PartitionValues(std::vector<Literal>{Literal::Long(partition_x)});
64+
file->file_size_in_bytes = 1024;
65+
file->record_count = 100;
66+
file->partition_spec_id = spec_->spec_id();
67+
return file;
68+
}
69+
70+
std::shared_ptr<DataFile> MakeDeleteFile(const std::string& path, int64_t partition_x) {
71+
auto file = MakeDataFile(path, partition_x);
72+
file->content = DataFile::Content::kPositionDeletes;
73+
file->file_size_in_bytes = 256;
74+
file->record_count = 7;
75+
return file;
76+
}
77+
78+
std::shared_ptr<DataFile> MakeDeletionVector(const std::string& path,
79+
const std::string& referenced_data_file,
80+
int64_t partition_x,
81+
int64_t content_offset = 0) {
82+
auto file = MakeDeleteFile(path, partition_x);
83+
file->file_format = FileFormatType::kPuffin;
84+
file->referenced_data_file = referenced_data_file;
85+
file->content_offset = content_offset;
86+
file->content_size_in_bytes = 10;
87+
return file;
88+
}
89+
90+
void CommitFileA() {
91+
ICEBERG_UNWRAP_OR_FAIL(auto fast_append, table_->NewFastAppend());
92+
fast_append->AppendFile(file_a_);
93+
EXPECT_THAT(fast_append->Commit(), IsOk());
94+
EXPECT_THAT(table_->Refresh(), IsOk());
95+
}
96+
97+
void SetTableFormatVersion(int8_t format_version) {
98+
table_->metadata()->format_version = format_version;
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+
107+
TEST_F(RowDeltaTest, AddRowsCommitsAppendOperation) {
108+
std::shared_ptr<RowDelta> row_delta;
109+
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
110+
row_delta->AddRows(file_a_);
111+
112+
EXPECT_THAT(row_delta->Commit(), IsOk());
113+
114+
EXPECT_THAT(table_->Refresh(), IsOk());
115+
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
116+
EXPECT_EQ(snapshot->Operation(), std::make_optional(DataOperation::kAppend));
117+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedDataFiles), "1");
118+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedRecords), "100");
119+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedFileSize), "1024");
120+
}
121+
122+
TEST_F(RowDeltaTest, AddDeletesCommitsDeleteOperation) {
123+
auto delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
124+
/*partition_x=*/1L);
125+
126+
std::shared_ptr<RowDelta> row_delta;
127+
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
128+
row_delta->AddDeletes(delete_file);
129+
130+
EXPECT_THAT(row_delta->Commit(), IsOk());
131+
132+
EXPECT_THAT(table_->Refresh(), IsOk());
133+
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
134+
EXPECT_EQ(snapshot->Operation(), std::make_optional(DataOperation::kDelete));
135+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedDeleteFiles), "1");
136+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedPosDeleteFiles), "1");
137+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedPosDeletes), "7");
138+
}
139+
140+
TEST_F(RowDeltaTest, RemoveRowsCommitsOverwriteOperation) {
141+
CommitFileA();
142+
143+
std::shared_ptr<RowDelta> row_delta;
144+
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
145+
row_delta->RemoveRows(file_a_);
146+
147+
EXPECT_THAT(row_delta->Commit(), IsOk());
148+
149+
EXPECT_THAT(table_->Refresh(), IsOk());
150+
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
151+
EXPECT_EQ(snapshot->Operation(), std::make_optional(DataOperation::kOverwrite));
152+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kDeletedDataFiles), "1");
153+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kDeletedRecords), "100");
154+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kRemovedFileSize), "1024");
155+
}
156+
157+
TEST_F(RowDeltaTest, RemoveRowsAndAddDeletesCommitsDeleteOperation) {
158+
CommitFileA();
159+
160+
auto delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
161+
/*partition_x=*/1L);
162+
163+
std::shared_ptr<RowDelta> row_delta;
164+
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
165+
row_delta->RemoveRows(file_a_);
166+
row_delta->AddDeletes(delete_file);
167+
168+
EXPECT_THAT(row_delta->Commit(), IsOk());
169+
170+
EXPECT_THAT(table_->Refresh(), IsOk());
171+
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
172+
EXPECT_EQ(snapshot->Operation(), std::make_optional(DataOperation::kDelete));
173+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kDeletedDataFiles), "1");
174+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedDeleteFiles), "1");
175+
}
176+
177+
TEST_F(RowDeltaTest, AddRowsAndRemoveDeletesCommitsAppendOperation) {
178+
auto delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
179+
/*partition_x=*/1L);
180+
{
181+
std::shared_ptr<RowDelta> row_delta;
182+
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
183+
row_delta->AddDeletes(delete_file);
184+
EXPECT_THAT(row_delta->Commit(), IsOk());
185+
EXPECT_THAT(table_->Refresh(), IsOk());
186+
}
187+
188+
std::shared_ptr<RowDelta> row_delta;
189+
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
190+
row_delta->AddRows(file_a_);
191+
row_delta->RemoveDeletes(delete_file);
192+
193+
EXPECT_THAT(row_delta->Commit(), IsOk());
194+
195+
EXPECT_THAT(table_->Refresh(), IsOk());
196+
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
197+
EXPECT_EQ(snapshot->Operation(), std::make_optional(DataOperation::kAppend));
198+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedDataFiles), "1");
199+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kRemovedDeleteFiles), "1");
200+
}
201+
202+
TEST_F(RowDeltaTest, CannotRemoveReferencedDataFile) {
203+
CommitFileA();
204+
205+
std::shared_ptr<RowDelta> row_delta;
206+
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
207+
std::vector<std::string> referenced_files{file_a_->file_path};
208+
row_delta->ValidateDataFilesExist(referenced_files);
209+
row_delta->RemoveRows(file_a_);
210+
211+
auto result = row_delta->Commit();
212+
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
213+
EXPECT_THAT(result, HasErrorMessage("Cannot delete data files"));
214+
EXPECT_THAT(result, HasErrorMessage(file_a_->file_path));
215+
}
216+
217+
TEST_F(RowDeltaTest, AddDeleteFileForRemovedDataFileCommitsDeleteOperation) {
218+
CommitFileA();
219+
220+
auto delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
221+
/*partition_x=*/1L);
222+
delete_file->referenced_data_file = file_a_->file_path;
223+
224+
std::shared_ptr<RowDelta> row_delta;
225+
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
226+
row_delta->RemoveRows(file_a_);
227+
row_delta->AddDeletes(delete_file);
228+
229+
EXPECT_THAT(row_delta->Commit(), IsOk());
230+
231+
EXPECT_THAT(table_->Refresh(), IsOk());
232+
ICEBERG_UNWRAP_OR_FAIL(auto snapshot, table_->current_snapshot());
233+
EXPECT_EQ(snapshot->Operation(), std::make_optional(DataOperation::kDelete));
234+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kDeletedDataFiles), "1");
235+
EXPECT_EQ(snapshot->summary.at(SnapshotSummaryFields::kAddedDeleteFiles), "1");
236+
}
237+
238+
TEST_F(RowDeltaTest, ValidateDeletedFilesAllowsMissingRowsOnEmptyTable) {
239+
std::shared_ptr<RowDelta> row_delta;
240+
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
241+
row_delta->ValidateDeletedFiles();
242+
row_delta->RemoveRows(file_a_);
243+
244+
EXPECT_THAT(row_delta->Commit(), IsOk());
245+
}
246+
247+
TEST_F(RowDeltaTest, ValidateDeletedFilesAllowsMissingDeletesOnEmptyTable) {
248+
auto delete_file = MakeDeleteFile("/delete/file_a_pos_deletes.parquet",
249+
/*partition_x=*/1L);
250+
251+
std::shared_ptr<RowDelta> row_delta;
252+
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
253+
row_delta->ValidateDeletedFiles();
254+
row_delta->RemoveDeletes(delete_file);
255+
256+
EXPECT_THAT(row_delta->Commit(), IsOk());
257+
}
258+
259+
TEST_F(RowDeltaTest, AddDeletionVectorValidatesConcurrentDVs) {
260+
CommitFileA();
261+
ICEBERG_UNWRAP_OR_FAIL(auto starting_snapshot, table_->current_snapshot());
262+
SetTableFormatVersion(3);
263+
264+
auto concurrent_dv =
265+
MakeDeletionVector("/delete/concurrent-dv-a.puffin", file_a_->file_path,
266+
/*partition_x=*/1L, /*content_offset=*/0);
267+
std::shared_ptr<RowDelta> concurrent_delta;
268+
ICEBERG_UNWRAP_OR_FAIL(concurrent_delta, table_->NewRowDelta());
269+
concurrent_delta->AddDeletes(concurrent_dv);
270+
EXPECT_THAT(concurrent_delta->Commit(), IsOk());
271+
EXPECT_THAT(table_->Refresh(), IsOk());
272+
SetTableFormatVersion(3);
273+
274+
auto dv = MakeDeletionVector("/delete/dv-a.puffin", file_a_->file_path,
275+
/*partition_x=*/1L, /*content_offset=*/10);
276+
std::shared_ptr<RowDelta> row_delta;
277+
ICEBERG_UNWRAP_OR_FAIL(row_delta, table_->NewRowDelta());
278+
row_delta->ValidateFromSnapshot(starting_snapshot->snapshot_id);
279+
row_delta->AddDeletes(dv);
280+
281+
auto result = row_delta->Commit();
282+
EXPECT_THAT(result, IsError(ErrorKind::kValidationFailed));
283+
EXPECT_THAT(result, HasErrorMessage("Found concurrently added DV"));
284+
EXPECT_THAT(result, HasErrorMessage(file_a_->file_path));
285+
}
286+
287+
} // namespace iceberg

src/iceberg/transaction.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "iceberg/update/expire_snapshots.h"
3636
#include "iceberg/update/fast_append.h"
3737
#include "iceberg/update/pending_update.h"
38+
#include "iceberg/update/row_delta.h"
3839
#include "iceberg/update/set_snapshot.h"
3940
#include "iceberg/update/snapshot_manager.h"
4041
#include "iceberg/update/snapshot_update.h"
@@ -478,6 +479,13 @@ Result<std::shared_ptr<FastAppend>> Transaction::NewFastAppend() {
478479
return fast_append;
479480
}
480481

482+
Result<std::shared_ptr<RowDelta>> Transaction::NewRowDelta() {
483+
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<RowDelta> row_delta,
484+
RowDelta::Make(ctx_->table->name().name, ctx_));
485+
ICEBERG_RETURN_UNEXPECTED(AddUpdate(row_delta));
486+
return row_delta;
487+
}
488+
481489
Result<std::shared_ptr<UpdateStatistics>> Transaction::NewUpdateStatistics() {
482490
ICEBERG_ASSIGN_OR_RAISE(std::shared_ptr<UpdateStatistics> update_statistics,
483491
UpdateStatistics::Make(ctx_));

src/iceberg/transaction.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ class ICEBERG_EXPORT Transaction : public std::enable_shared_from_this<Transacti
106106
/// \brief Create a new FastAppend to append data files and commit the changes.
107107
Result<std::shared_ptr<FastAppend>> NewFastAppend();
108108

109+
/// \brief Create a new RowDelta to add rows and row-level deletes.
110+
Result<std::shared_ptr<RowDelta>> NewRowDelta();
111+
109112
/// \brief Create a new SnapshotManager to manage snapshots.
110113
Result<std::shared_ptr<SnapshotManager>> NewSnapshotManager();
111114

src/iceberg/type_fwd.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ class TransactionContext;
224224
class ExpireSnapshots;
225225
class FastAppend;
226226
class PendingUpdate;
227+
class RowDelta;
227228
class SetSnapshot;
228229
class SnapshotManager;
229230
class SnapshotUpdate;

0 commit comments

Comments
 (0)