| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| #include "iceberg/data/delete_loader.h" |
| |
| #include <cstring> |
| #include <span> |
| #include <string> |
| #include <vector> |
| |
| #include <nanoarrow/nanoarrow.h> |
| |
| #include "iceberg/arrow/nanoarrow_status_internal.h" |
| #include "iceberg/arrow_c_data_guard_internal.h" |
| #include "iceberg/deletes/position_delete_index.h" |
| #include "iceberg/deletes/position_delete_range_consumer.h" |
| #include "iceberg/file_reader.h" |
| #include "iceberg/manifest/manifest_entry.h" |
| #include "iceberg/metadata_columns.h" |
| #include "iceberg/result.h" |
| #include "iceberg/row/arrow_array_wrapper.h" |
| #include "iceberg/schema.h" |
| #include "iceberg/util/macros.h" |
| #include "iceberg/util/struct_like_set.h" |
| |
| namespace iceberg { |
| |
| namespace { |
| |
| /// \brief Build the projection schema for reading position delete files. |
| std::shared_ptr<Schema> PosDeleteSchema() { |
| return std::make_shared<Schema>(std::vector<SchemaField>{ |
| MetadataColumns::kDeleteFilePath, |
| MetadataColumns::kDeleteFilePos, |
| }); |
| } |
| |
| /// \brief Open a delete file with the given projection schema. |
| Result<std::unique_ptr<Reader>> OpenDeleteFile(const DataFile& file, |
| std::shared_ptr<Schema> projection, |
| const std::shared_ptr<FileIO>& io) { |
| ReaderOptions options{ |
| .path = file.file_path, |
| .length = static_cast<size_t>(file.file_size_in_bytes), |
| .io = io, |
| .projection = std::move(projection), |
| }; |
| return ReaderFactoryRegistry::Open(file.file_format, options); |
| } |
| |
| /// Raw `int64` values buffer (offset-adjusted). Skips the validity bitmap: |
| /// `kDeleteFilePos` is required by the V2 spec. |
| const int64_t* Int64ValuesBuffer(const ArrowArrayView* view) { |
| return view->buffer_views[1].data.as_int64 + view->offset; |
| } |
| |
| /// String-equals at `row_idx` via nanoarrow's unsafe direct-buffer access. |
| /// Skips the validity bitmap: `kDeleteFilePath` is required by the V2 spec. |
| bool StringEquals(const ArrowArrayView* view, int64_t row_idx, std::string_view target) { |
| ArrowStringView sv = ArrowArrayViewGetStringUnsafe(view, row_idx); |
| if (static_cast<size_t>(sv.size_bytes) != target.size()) { |
| return false; |
| } |
| if (target.empty()) { |
| return true; |
| } |
| return sv.data != nullptr && std::memcmp(sv.data, target.data(), target.size()) == 0; |
| } |
| |
| } // namespace |
| |
| DeleteLoader::DeleteLoader(std::shared_ptr<FileIO> io) : io_(std::move(io)) {} |
| |
| DeleteLoader::~DeleteLoader() = default; |
| |
| Status DeleteLoader::LoadPositionDelete(const DataFile& file, PositionDeleteIndex& index, |
| std::string_view data_file_path) const { |
| // TODO(gangwu): push down path filter to open the file. |
| ICEBERG_ASSIGN_OR_RAISE(auto reader, OpenDeleteFile(file, PosDeleteSchema(), io_)); |
| |
| ICEBERG_ASSIGN_OR_RAISE(auto arrow_schema, reader->Schema()); |
| internal::ArrowSchemaGuard schema_guard(&arrow_schema); |
| |
| // Reused across batches; reads child buffers directly to avoid the |
| // per-row `Scalar` dispatch in `ArrowArrayStructLike`. |
| ArrowArrayView array_view; |
| internal::ArrowArrayViewGuard view_guard(&array_view); |
| ArrowError error; |
| ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR( |
| ArrowArrayViewInitFromSchema(&array_view, &arrow_schema, &error), error); |
| |
| // Fast path when the writer's `referenced_data_file` hint matches our |
| // target: skip the path column, hand `pos_data` straight to |
| // `ForEachPositionDelete`. Trusts the hint -- spec-compliant writers |
| // only set it when all rows share one data file. |
| const bool use_referenced_data_file_fast_path = |
| file.referenced_data_file.has_value() && |
| file.referenced_data_file.value() == data_file_path; |
| |
| // Filter-path staging buffer; reused across batches via `clear()`. |
| std::vector<int64_t> positions; |
| // Scratch buffer for `ForEachPositionDelete`'s bulk dispatch path; |
| // reused across batches and across both routing branches. |
| std::vector<uint32_t> bulk_scratch; |
| |
| while (true) { |
| ICEBERG_ASSIGN_OR_RAISE(auto batch_opt, reader->Next()); |
| if (!batch_opt.has_value()) break; |
| |
| auto& batch = batch_opt.value(); |
| internal::ArrowArrayGuard batch_guard(&batch); |
| |
| ICEBERG_NANOARROW_RETURN_UNEXPECTED_WITH_ERROR( |
| ArrowArrayViewSetArray(&array_view, &batch, &error), error); |
| |
| const int64_t length = batch.length; |
| if (length <= 0) { |
| continue; |
| } |
| |
| // Child indices must match `PosDeleteSchema()`: 0 = file_path, 1 = pos. |
| const ArrowArrayView* path_view = array_view.children[0]; |
| const ArrowArrayView* pos_view = array_view.children[1]; |
| |
| // V2 spec marks pos and file_path as required (NOT NULL). The direct |
| // buffer access below skips the validity bitmap, so a non-compliant |
| // batch would silently corrupt the index. Fail fast instead. |
| if (ArrowArrayViewComputeNullCount(pos_view) != 0 || |
| ArrowArrayViewComputeNullCount(path_view) != 0) { |
| return InvalidArrowData( |
| "position delete file has null values in required pos/file_path columns"); |
| } |
| |
| const int64_t* pos_data = Int64ValuesBuffer(pos_view); |
| |
| if (use_referenced_data_file_fast_path) { |
| ForEachPositionDelete(std::span<const int64_t>(pos_data, length), index, |
| bulk_scratch); |
| continue; |
| } |
| |
| positions.clear(); |
| if (positions.capacity() < static_cast<size_t>(length)) { |
| positions.reserve(static_cast<size_t>(length)); |
| } |
| for (int64_t i = 0; i < length; ++i) { |
| if (StringEquals(path_view, i, data_file_path)) { |
| positions.push_back(pos_data[i]); |
| } |
| } |
| ForEachPositionDelete(positions, index, bulk_scratch); |
| } |
| |
| return reader->Close(); |
| } |
| |
| Status DeleteLoader::LoadDV(const DataFile& file, PositionDeleteIndex& index) const { |
| return NotSupported("Loading deletion vectors is not yet supported"); |
| } |
| |
| Result<PositionDeleteIndex> DeleteLoader::LoadPositionDeletes( |
| std::span<const std::shared_ptr<DataFile>> delete_files, |
| std::string_view data_file_path) const { |
| PositionDeleteIndex index; |
| |
| for (const auto& file : delete_files) { |
| if (file->referenced_data_file.has_value() && |
| file->referenced_data_file.value() != data_file_path) { |
| continue; |
| } |
| |
| if (file->IsDeletionVector()) { |
| ICEBERG_RETURN_UNEXPECTED(LoadDV(*file, index)); |
| continue; |
| } |
| |
| ICEBERG_PRECHECK(file->content == DataFile::Content::kPositionDeletes, |
| "Expected position delete file but got content type {}", |
| ToString(file->content)); |
| |
| ICEBERG_RETURN_UNEXPECTED(LoadPositionDelete(*file, index, data_file_path)); |
| } |
| |
| return index; |
| } |
| |
| Result<std::unique_ptr<UncheckedStructLikeSet>> DeleteLoader::LoadEqualityDeletes( |
| std::span<const std::shared_ptr<DataFile>> delete_files, |
| const StructType& equality_type) const { |
| auto eq_set = std::make_unique<UncheckedStructLikeSet>(equality_type); |
| |
| std::shared_ptr<Schema> projection = equality_type.ToSchema(); |
| |
| for (const auto& file : delete_files) { |
| ICEBERG_PRECHECK(file->content == DataFile::Content::kEqualityDeletes, |
| "Expected equality delete file but got content type {}", |
| static_cast<int>(file->content)); |
| |
| ICEBERG_ASSIGN_OR_RAISE(auto reader, OpenDeleteFile(*file, projection, io_)); |
| ICEBERG_ASSIGN_OR_RAISE(auto arrow_schema, reader->Schema()); |
| internal::ArrowSchemaGuard schema_guard(&arrow_schema); |
| |
| while (true) { |
| ICEBERG_ASSIGN_OR_RAISE(auto batch_opt, reader->Next()); |
| if (!batch_opt.has_value()) break; |
| |
| auto& batch = batch_opt.value(); |
| internal::ArrowArrayGuard batch_guard(&batch); |
| |
| ICEBERG_ASSIGN_OR_RAISE( |
| auto row, ArrowArrayStructLike::Make(arrow_schema, batch, /*row_index=*/0)); |
| |
| for (int64_t i = 0; i < batch.length; ++i) { |
| if (i > 0) { |
| ICEBERG_RETURN_UNEXPECTED(row->Reset(i)); |
| } |
| ICEBERG_RETURN_UNEXPECTED(eq_set->Insert(*row)); |
| } |
| } |
| |
| ICEBERG_RETURN_UNEXPECTED(reader->Close()); |
| } |
| |
| return eq_set; |
| } |
| |
| } // namespace iceberg |