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>
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
9095DeleteLoader::~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
177243Result<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;
0 commit comments