Skip to content

Commit 9dedafb

Browse files
author
xuan.zhao
committed
feat(puffin): support deletion-vector-v1 blob read/write
1 parent 3c9d13d commit 9dedafb

17 files changed

Lines changed: 1403 additions & 23 deletions

src/iceberg/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ set(ICEBERG_DATA_SOURCES
188188
data/data_writer.cc
189189
data/delete_filter.cc
190190
data/delete_loader.cc
191+
data/deletion_vector_writer.cc
191192
data/equality_delete_writer.cc
192193
data/file_scan_task_reader.cc
193194
data/position_delete_writer.cc

src/iceberg/data/delete_loader.cc

Lines changed: 98 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@
1919

2020
#include "iceberg/data/delete_loader.h"
2121

22+
#include <cstdint>
2223
#include <cstring>
24+
#include <limits>
2325
#include <span>
2426
#include <string>
27+
#include <utility>
2528
#include <vector>
2629

2730
#include <nanoarrow/nanoarrow.h>
@@ -30,12 +33,14 @@
3033
#include "iceberg/arrow_c_data_guard_internal.h"
3134
#include "iceberg/deletes/position_delete_index.h"
3235
#include "iceberg/deletes/position_delete_range_consumer.h"
36+
#include "iceberg/file_io.h"
3337
#include "iceberg/file_reader.h"
3438
#include "iceberg/manifest/manifest_entry.h"
3539
#include "iceberg/metadata_columns.h"
3640
#include "iceberg/result.h"
3741
#include "iceberg/row/arrow_array_wrapper.h"
3842
#include "iceberg/schema.h"
43+
#include "iceberg/util/content_file_util.h"
3944
#include "iceberg/util/macros.h"
4045
#include "iceberg/util/struct_like_set.h"
4146

@@ -89,10 +94,11 @@ DeleteLoader::DeleteLoader(std::shared_ptr<FileIO> io) : io_(std::move(io)) {}
8994

9095
DeleteLoader::~DeleteLoader() = default;
9196

92-
Status DeleteLoader::LoadPositionDelete(const DataFile& file, PositionDeleteIndex& index,
97+
Status DeleteLoader::LoadPositionDelete(const std::shared_ptr<DataFile>& file,
98+
PositionDeleteIndex& index,
9399
std::string_view data_file_path) const {
94100
// TODO(gangwu): push down path filter to open the file.
95-
ICEBERG_ASSIGN_OR_RAISE(auto reader, OpenDeleteFile(file, PosDeleteSchema(), io_));
101+
ICEBERG_ASSIGN_OR_RAISE(auto reader, OpenDeleteFile(*file, PosDeleteSchema(), io_));
96102

97103
ICEBERG_ASSIGN_OR_RAISE(auto arrow_schema, reader->Schema());
98104
internal::ArrowSchemaGuard schema_guard(&arrow_schema);
@@ -110,15 +116,19 @@ Status DeleteLoader::LoadPositionDelete(const DataFile& file, PositionDeleteInde
110116
// `ForEachPositionDelete`. Trusts the hint -- spec-compliant writers
111117
// only set it when all rows share one data file.
112118
const bool use_referenced_data_file_fast_path =
113-
file.referenced_data_file.has_value() &&
114-
file.referenced_data_file.value() == data_file_path;
119+
file->referenced_data_file.has_value() &&
120+
file->referenced_data_file.value() == data_file_path;
115121

116122
// Filter-path staging buffer; reused across batches via `clear()`.
117123
std::vector<int64_t> positions;
118124
// Scratch buffer for `ForEachPositionDelete`'s bulk dispatch path;
119125
// reused across batches and across both routing branches.
120126
std::vector<uint32_t> bulk_scratch;
121127

128+
// Whether any position for the target data file came from this file, so the
129+
// source delete file is recorded once.
130+
bool contributed = false;
131+
122132
while (true) {
123133
ICEBERG_ASSIGN_OR_RAISE(auto batch_opt, reader->Next());
124134
if (!batch_opt.has_value()) break;
@@ -150,6 +160,9 @@ Status DeleteLoader::LoadPositionDelete(const DataFile& file, PositionDeleteInde
150160
const int64_t* pos_data = Int64ValuesBuffer(pos_view);
151161

152162
if (use_referenced_data_file_fast_path) {
163+
// Reaching here means length > 0 (empty batches are skipped above), so
164+
// this file contributes positions for the target data file.
165+
contributed = true;
153166
ForEachPositionDelete(std::span<const int64_t>(pos_data, length), index,
154167
bulk_scratch);
155168
continue;
@@ -164,37 +177,105 @@ Status DeleteLoader::LoadPositionDelete(const DataFile& file, PositionDeleteInde
164177
positions.push_back(pos_data[i]);
165178
}
166179
}
180+
if (!positions.empty()) {
181+
contributed = true;
182+
}
167183
ForEachPositionDelete(positions, index, bulk_scratch);
168184
}
169185

170-
return reader->Close();
186+
ICEBERG_RETURN_UNEXPECTED(reader->Close());
187+
188+
// Record the source delete file so callers (e.g. DeletionVectorWriter) can
189+
// report file-scoped position deletes as rewritten.
190+
if (contributed) {
191+
index.AddDeleteFile(file);
192+
}
193+
return {};
171194
}
172195

173-
Status DeleteLoader::LoadDV(const DataFile& file, PositionDeleteIndex& index) const {
174-
return NotSupported("Loading deletion vectors is not yet supported");
196+
Status DeleteLoader::LoadDV(const std::shared_ptr<DataFile>& file,
197+
PositionDeleteIndex& index,
198+
std::string_view data_file_path) const {
199+
// A deletion vector must reference exactly one data file; without it the
200+
// caller cannot know which data file the positions apply to.
201+
ICEBERG_PRECHECK(file->referenced_data_file.has_value(),
202+
"Deletion vector requires referenced_data_file: {}", file->file_path);
203+
204+
// The DV must reference the data file being read. A mismatch means the caller
205+
// grouped delete files incorrectly; failing here avoids silently applying the
206+
// wrong DV or hiding bad metadata behind an empty index.
207+
ICEBERG_PRECHECK(file->referenced_data_file.value() == data_file_path,
208+
"Deletion vector references {}, not the requested data file {}",
209+
file->referenced_data_file.value(), data_file_path);
210+
211+
// For deletion vectors, content_offset and content_size_in_bytes point directly
212+
// at the DV blob bytes within the Puffin file and are required by the spec.
213+
ICEBERG_PRECHECK(
214+
file->content_offset.has_value() && file->content_size_in_bytes.has_value(),
215+
"Deletion vector requires content_offset and content_size_in_bytes: {}",
216+
file->file_path);
217+
218+
const int64_t offset = file->content_offset.value();
219+
const int64_t length = file->content_size_in_bytes.value();
220+
ICEBERG_PRECHECK(offset >= 0 && length >= 0,
221+
"Invalid deletion vector offset/length: offset={}, length={}", offset,
222+
length);
223+
ICEBERG_PRECHECK(length <= std::numeric_limits<int32_t>::max(),
224+
"Cannot read deletion vector larger than 2GB: {}", length);
225+
226+
ICEBERG_ASSIGN_OR_RAISE(auto input_file, io_->NewInputFile(file->file_path));
227+
ICEBERG_ASSIGN_OR_RAISE(auto stream, input_file->Open());
228+
229+
std::vector<std::byte> bytes(static_cast<size_t>(length));
230+
ICEBERG_RETURN_UNEXPECTED(stream->ReadFully(offset, bytes));
231+
ICEBERG_RETURN_UNEXPECTED(stream->Close());
232+
233+
std::span<const uint8_t> blob(reinterpret_cast<const uint8_t*>(bytes.data()),
234+
bytes.size());
235+
// Deserialize validates the blob length and cardinality against `file` and
236+
// retains it as the source delete file.
237+
ICEBERG_ASSIGN_OR_RAISE(auto dv, PositionDeleteIndex::Deserialize(blob, file));
238+
239+
index.Merge(dv);
240+
return {};
175241
}
176242

177243
Result<PositionDeleteIndex> DeleteLoader::LoadPositionDeletes(
178244
std::span<const std::shared_ptr<DataFile>> delete_files,
179245
std::string_view data_file_path) const {
246+
for (const auto& file : delete_files) {
247+
ICEBERG_PRECHECK(file != nullptr, "Delete file must not be null");
248+
}
249+
180250
PositionDeleteIndex index;
181251

252+
// A single deletion vector replaces all other deletes for a data file, so it
253+
// is read through the dedicated DV path and validated against the requested
254+
// data file.
255+
if (ContentFileUtil::ContainsSingleDV(delete_files)) {
256+
ICEBERG_RETURN_UNEXPECTED(LoadDV(delete_files.front(), index, data_file_path));
257+
return index;
258+
}
259+
260+
// Otherwise all entries must be position delete files. A DV must not be mixed
261+
// with position deletes: once a DV applies, readers ignore matching position
262+
// deletes, so a mixed list means the caller grouped delete files incorrectly.
182263
for (const auto& file : delete_files) {
264+
ICEBERG_PRECHECK(!file->IsDeletionVector(),
265+
"Deletion vector cannot be mixed with position delete files: {}",
266+
file->file_path);
267+
ICEBERG_PRECHECK(file->content == DataFile::Content::kPositionDeletes,
268+
"Expected position delete file but got content type {}",
269+
ToString(file->content));
270+
271+
// A file-scoped position delete for another data file has nothing for the
272+
// target path; skip it as an optimization.
183273
if (file->referenced_data_file.has_value() &&
184274
file->referenced_data_file.value() != data_file_path) {
185275
continue;
186276
}
187277

188-
if (file->IsDeletionVector()) {
189-
ICEBERG_RETURN_UNEXPECTED(LoadDV(*file, index));
190-
continue;
191-
}
192-
193-
ICEBERG_PRECHECK(file->content == DataFile::Content::kPositionDeletes,
194-
"Expected position delete file but got content type {}",
195-
ToString(file->content));
196-
197-
ICEBERG_RETURN_UNEXPECTED(LoadPositionDelete(*file, index, data_file_path));
278+
ICEBERG_RETURN_UNEXPECTED(LoadPositionDelete(file, index, data_file_path));
198279
}
199280

200281
return index;

src/iceberg/data/delete_loader.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,13 @@ class ICEBERG_DATA_EXPORT DeleteLoader {
6767

6868
private:
6969
/// \brief Load a single position delete file into the index.
70-
Status LoadPositionDelete(const DataFile& file, PositionDeleteIndex& index,
70+
Status LoadPositionDelete(const std::shared_ptr<DataFile>& file,
71+
PositionDeleteIndex& index,
7172
std::string_view data_file_path) const;
7273

7374
/// \brief Load a single deletion vector file into the index.
74-
Status LoadDV(const DataFile& file, PositionDeleteIndex& index) const;
75+
Status LoadDV(const std::shared_ptr<DataFile>& file, PositionDeleteIndex& index,
76+
std::string_view data_file_path) const;
7577

7678
std::shared_ptr<FileIO> io_;
7779
};

0 commit comments

Comments
 (0)