|
| 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, software |
| 13 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | + * See the License for the specific language governing permissions and |
| 16 | + * limitations under the License. |
| 17 | + */ |
| 18 | + |
| 19 | +#include "paimon/format/blob/blob_file_batch_reader.h" |
| 20 | + |
| 21 | +#include <algorithm> |
| 22 | +#include <future> |
| 23 | +#include <numeric> |
| 24 | + |
| 25 | +#include "arrow/api.h" |
| 26 | +#include "arrow/array/builder_dict.h" |
| 27 | +#include "arrow/array/builder_nested.h" |
| 28 | +#include "arrow/c/bridge.h" |
| 29 | +#include "arrow/util/bit_util.h" |
| 30 | +#include "fmt/format.h" |
| 31 | +#include "paimon/common/data/blob_utils.h" |
| 32 | +#include "paimon/common/executor/future.h" |
| 33 | +#include "paimon/common/io/offset_input_stream.h" |
| 34 | +#include "paimon/common/metrics/metrics_impl.h" |
| 35 | +#include "paimon/common/utils/arrow/mem_utils.h" |
| 36 | +#include "paimon/common/utils/arrow/status_utils.h" |
| 37 | +#include "paimon/common/utils/delta_varint_compressor.h" |
| 38 | +#include "paimon/common/utils/stream_utils.h" |
| 39 | +#include "paimon/data/blob.h" |
| 40 | + |
| 41 | +namespace paimon::blob { |
| 42 | + |
| 43 | +Result<std::unique_ptr<BlobFileBatchReader>> BlobFileBatchReader::Create( |
| 44 | + const std::shared_ptr<InputStream>& input_stream, int32_t batch_size, bool blob_as_descriptor, |
| 45 | + const std::shared_ptr<MemoryPool>& pool) { |
| 46 | + if (input_stream == nullptr) { |
| 47 | + return Status::Invalid("blob file batch reader create failed: input stream is nullptr"); |
| 48 | + } |
| 49 | + if (batch_size <= 0) { |
| 50 | + return Status::Invalid(fmt::format( |
| 51 | + "blob file batch reader create failed: read batch size '{}' should be larger than zero", |
| 52 | + batch_size)); |
| 53 | + } |
| 54 | + |
| 55 | + PAIMON_ASSIGN_OR_RAISE(uint64_t file_size, input_stream->Length()); |
| 56 | + PAIMON_RETURN_NOT_OK( |
| 57 | + input_stream->Seek(file_size - BlobDefs::kBlobFileHeaderLength, FS_SEEK_SET)); |
| 58 | + int8_t header[BlobDefs::kBlobFileHeaderLength]; |
| 59 | + PAIMON_ASSIGN_OR_RAISE( |
| 60 | + int32_t actual_size, |
| 61 | + input_stream->Read(reinterpret_cast<char*>(header), BlobDefs::kBlobFileHeaderLength)); |
| 62 | + if (actual_size != BlobDefs::kBlobFileHeaderLength) { |
| 63 | + return Status::Invalid( |
| 64 | + fmt::format("actual read size {} not match with expect header length {}", actual_size, |
| 65 | + BlobDefs::kBlobFileHeaderLength)); |
| 66 | + } |
| 67 | + int8_t version = header[4]; |
| 68 | + if (version != BlobDefs::kFileVersion) { |
| 69 | + return Status::Invalid(fmt::format( |
| 70 | + "create blob format reader failed. unsupported blob file version: {}", version)); |
| 71 | + } |
| 72 | + int32_t index_length = GetIndexLength(header, 0); |
| 73 | + PAIMON_RETURN_NOT_OK(input_stream->Seek( |
| 74 | + file_size - BlobDefs::kBlobFileHeaderLength - index_length, FS_SEEK_SET)); |
| 75 | + std::vector<char> index_bytes(index_length, '\0'); |
| 76 | + PAIMON_ASSIGN_OR_RAISE(actual_size, input_stream->Read(index_bytes.data(), index_length)); |
| 77 | + if (actual_size != index_length) { |
| 78 | + return Status::Invalid( |
| 79 | + fmt::format("actual read size {} not match with expect index length {}", actual_size, |
| 80 | + index_length)); |
| 81 | + } |
| 82 | + PAIMON_ASSIGN_OR_RAISE(const std::vector<int64_t> blob_lengths, |
| 83 | + DeltaVarintCompressor::Decompress(index_bytes)); |
| 84 | + |
| 85 | + std::vector<int64_t> blob_offsets; |
| 86 | + blob_offsets.reserve(blob_lengths.size()); |
| 87 | + int64_t offset = 0; |
| 88 | + for (const auto& blob_length : blob_lengths) { |
| 89 | + blob_offsets.push_back(offset); |
| 90 | + // Null blobs (bin_length == -1) don't occupy file space |
| 91 | + if (blob_length >= 0) { |
| 92 | + offset += blob_length; |
| 93 | + } |
| 94 | + } |
| 95 | + PAIMON_ASSIGN_OR_RAISE(std::string file_path, input_stream->GetUri()); |
| 96 | + auto reader = std::unique_ptr<BlobFileBatchReader>(new BlobFileBatchReader( |
| 97 | + input_stream, file_path, blob_lengths, blob_offsets, batch_size, blob_as_descriptor, pool)); |
| 98 | + return reader; |
| 99 | +} |
| 100 | + |
| 101 | +BlobFileBatchReader::BlobFileBatchReader(const std::shared_ptr<InputStream>& input_stream, |
| 102 | + const std::string& file_path, |
| 103 | + const std::vector<int64_t>& blob_lengths, |
| 104 | + const std::vector<int64_t>& blob_offsets, |
| 105 | + int32_t batch_size, bool blob_as_descriptor, |
| 106 | + const std::shared_ptr<MemoryPool>& pool) |
| 107 | + : input_stream_(input_stream), |
| 108 | + file_path_(file_path), |
| 109 | + all_blob_lengths_(blob_lengths), |
| 110 | + all_blob_offsets_(blob_offsets), |
| 111 | + target_blob_lengths_(blob_lengths), |
| 112 | + target_blob_offsets_(blob_offsets), |
| 113 | + batch_size_(batch_size), |
| 114 | + blob_as_descriptor_(blob_as_descriptor), |
| 115 | + pool_(pool), |
| 116 | + arrow_pool_(GetArrowPool(pool_)), |
| 117 | + metrics_(std::make_shared<MetricsImpl>()) { |
| 118 | + target_blob_row_indexes_.resize(target_blob_lengths_.size()); |
| 119 | + std::iota(target_blob_row_indexes_.begin(), target_blob_row_indexes_.end(), 0); |
| 120 | +} |
| 121 | + |
| 122 | +Status BlobFileBatchReader::SetReadSchema(::ArrowSchema* read_schema, |
| 123 | + const std::shared_ptr<Predicate>& predicate, |
| 124 | + const std::optional<RoaringBitmap32>& selection_bitmap) { |
| 125 | + if (!read_schema) { |
| 126 | + return Status::Invalid("SetReadSchema failed: read schema cannot be nullptr"); |
| 127 | + } |
| 128 | + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Schema> arrow_schema, |
| 129 | + arrow::ImportSchema(read_schema)); |
| 130 | + if (arrow_schema->num_fields() != 1) { |
| 131 | + return Status::Invalid( |
| 132 | + fmt::format("read schema field number {} is not 1", arrow_schema->num_fields())); |
| 133 | + } |
| 134 | + if (!BlobUtils::IsBlobField(arrow_schema->field(0))) { |
| 135 | + return Status::Invalid( |
| 136 | + fmt::format("field {} is not BLOB", arrow_schema->field(0)->ToString())); |
| 137 | + } |
| 138 | + if (selection_bitmap != std::nullopt) { |
| 139 | + int32_t cardinality = selection_bitmap->Cardinality(); |
| 140 | + std::vector<int64_t> new_lengths(cardinality); |
| 141 | + std::vector<int64_t> new_offsets(cardinality); |
| 142 | + std::vector<uint64_t> new_row_indexes(cardinality); |
| 143 | + |
| 144 | + PAIMON_ASSIGN_OR_RAISE(uint64_t total_rows, GetNumberOfRows()); |
| 145 | + RoaringBitmap32::Iterator iterator(*selection_bitmap); |
| 146 | + for (int32_t i = 0; i < cardinality; i++) { |
| 147 | + int32_t row_index = *iterator; |
| 148 | + if (static_cast<size_t>(row_index) >= total_rows) { |
| 149 | + return Status::Invalid(fmt::format( |
| 150 | + "row index {} is out of bound of total row number {}", row_index, total_rows)); |
| 151 | + } |
| 152 | + ++iterator; |
| 153 | + new_lengths[i] = all_blob_lengths_[row_index]; |
| 154 | + new_offsets[i] = all_blob_offsets_[row_index]; |
| 155 | + new_row_indexes[i] = row_index; |
| 156 | + } |
| 157 | + target_blob_lengths_ = new_lengths; |
| 158 | + target_blob_offsets_ = new_offsets; |
| 159 | + target_blob_row_indexes_ = new_row_indexes; |
| 160 | + } |
| 161 | + target_type_ = arrow::struct_(arrow_schema->fields()); |
| 162 | + current_pos_ = 0; |
| 163 | + previous_batch_first_row_number_ = std::numeric_limits<uint64_t>::max(); |
| 164 | + |
| 165 | + return Status::OK(); |
| 166 | +} |
| 167 | + |
| 168 | +Result<std::shared_ptr<arrow::Buffer>> BlobFileBatchReader::NextBlobOffsets( |
| 169 | + int32_t rows_to_read) const { |
| 170 | + arrow::TypedBufferBuilder<int64_t> buffer_builder(arrow_pool_.get()); |
| 171 | + PAIMON_RETURN_NOT_OK_FROM_ARROW(buffer_builder.Reserve(rows_to_read + 1)); |
| 172 | + PAIMON_RETURN_NOT_OK_FROM_ARROW(buffer_builder.Append(0)); |
| 173 | + int64_t data_length = 0; |
| 174 | + for (int32_t k = 0; k < rows_to_read; ++k) { |
| 175 | + const size_t i = current_pos_ + k; |
| 176 | + // Null blobs contribute zero bytes to content |
| 177 | + if (!IsTargetNull(i)) { |
| 178 | + data_length += GetTargetContentLength(i); |
| 179 | + } |
| 180 | + PAIMON_RETURN_NOT_OK_FROM_ARROW(buffer_builder.Append(data_length)); |
| 181 | + } |
| 182 | + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Buffer> offset_buffer, |
| 183 | + buffer_builder.Finish()); |
| 184 | + return offset_buffer; |
| 185 | +} |
| 186 | + |
| 187 | +Result<std::shared_ptr<arrow::Buffer>> BlobFileBatchReader::NextBlobContents( |
| 188 | + int32_t rows_to_read) const { |
| 189 | + int64_t total_length = 0; |
| 190 | + for (int32_t k = 0; k < rows_to_read; ++k) { |
| 191 | + const size_t i = current_pos_ + k; |
| 192 | + if (!IsTargetNull(i)) { |
| 193 | + total_length += GetTargetContentLength(i); |
| 194 | + } |
| 195 | + } |
| 196 | + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Buffer> data_buffer, |
| 197 | + arrow::AllocateBuffer(total_length, arrow_pool_.get())); |
| 198 | + uint8_t* buffer = data_buffer->mutable_data(); |
| 199 | + for (int32_t k = 0; k < rows_to_read; ++k) { |
| 200 | + const size_t i = current_pos_ + k; |
| 201 | + if (IsTargetNull(i)) { |
| 202 | + continue; |
| 203 | + } |
| 204 | + int64_t offset = GetTargetContentOffset(i); |
| 205 | + int64_t length = GetTargetContentLength(i); |
| 206 | + PAIMON_RETURN_NOT_OK(ReadBlobContentAt(offset, length, buffer)); |
| 207 | + buffer += length; |
| 208 | + } |
| 209 | + return data_buffer; |
| 210 | +} |
| 211 | + |
| 212 | +Result<std::shared_ptr<arrow::Buffer>> BlobFileBatchReader::BuildNullBitmap( |
| 213 | + int32_t rows_to_read) const { |
| 214 | + bool has_null = false; |
| 215 | + for (int32_t k = 0; k < rows_to_read; ++k) { |
| 216 | + if (IsTargetNull(current_pos_ + k)) { |
| 217 | + has_null = true; |
| 218 | + break; |
| 219 | + } |
| 220 | + } |
| 221 | + if (!has_null) { |
| 222 | + return std::shared_ptr<arrow::Buffer>(); |
| 223 | + } |
| 224 | + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Buffer> null_bitmap, |
| 225 | + arrow::AllocateBitmap(rows_to_read, arrow_pool_.get())); |
| 226 | + // Initialize all bits to 1 (valid), then clear bits for null rows |
| 227 | + memset(null_bitmap->mutable_data(), 0xFF, null_bitmap->size()); |
| 228 | + for (int32_t k = 0; k < rows_to_read; ++k) { |
| 229 | + if (IsTargetNull(current_pos_ + k)) { |
| 230 | + arrow::bit_util::ClearBit(null_bitmap->mutable_data(), k); |
| 231 | + } |
| 232 | + } |
| 233 | + return null_bitmap; |
| 234 | +} |
| 235 | + |
| 236 | +Result<std::shared_ptr<arrow::Array>> BlobFileBatchReader::BuildContentArray( |
| 237 | + int32_t rows_to_read) const { |
| 238 | + PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> value_offsets, |
| 239 | + NextBlobOffsets(rows_to_read)); |
| 240 | + PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> data, NextBlobContents(rows_to_read)); |
| 241 | + PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Buffer> child_null_bitmap, |
| 242 | + BuildNullBitmap(rows_to_read)); |
| 243 | + |
| 244 | + auto large_binary_array = std::make_shared<arrow::LargeBinaryArray>(rows_to_read, value_offsets, |
| 245 | + data, child_null_bitmap); |
| 246 | + std::vector<std::shared_ptr<arrow::ArrayData>> child_data; |
| 247 | + child_data.emplace_back(large_binary_array->data()); |
| 248 | + std::shared_ptr<arrow::ArrayData> struct_array_data = |
| 249 | + arrow::ArrayData::Make(target_type_, large_binary_array->length(), {nullptr}, child_data); |
| 250 | + return std::make_shared<arrow::StructArray>(struct_array_data); |
| 251 | +} |
| 252 | + |
| 253 | +Result<std::shared_ptr<arrow::Array>> BlobFileBatchReader::BuildTargetArray( |
| 254 | + int32_t rows_to_read) const { |
| 255 | + std::shared_ptr<arrow::Array> blob_array; |
| 256 | + if (!blob_as_descriptor_) { |
| 257 | + return BuildContentArray(rows_to_read); |
| 258 | + } |
| 259 | + // For descriptor mode, build using StructBuilder to handle nulls properly |
| 260 | + PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::unique_ptr<arrow::ArrayBuilder> array_builder, |
| 261 | + arrow::MakeBuilder(target_type_, arrow_pool_.get())); |
| 262 | + auto builder = dynamic_cast<arrow::StructBuilder*>(array_builder.get()); |
| 263 | + if (builder == nullptr) { |
| 264 | + return Status::Invalid("cast to struct builder failed"); |
| 265 | + } |
| 266 | + auto field_builder = dynamic_cast<arrow::LargeBinaryBuilder*>(builder->field_builder(0)); |
| 267 | + if (field_builder == nullptr) { |
| 268 | + return Status::Invalid("cast to large binary builder failed"); |
| 269 | + } |
| 270 | + for (int32_t k = 0; k < rows_to_read; ++k) { |
| 271 | + const size_t i = current_pos_ + k; |
| 272 | + PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Append()); |
| 273 | + if (IsTargetNull(i)) { |
| 274 | + PAIMON_RETURN_NOT_OK_FROM_ARROW(field_builder->AppendNull()); |
| 275 | + } else { |
| 276 | + int64_t offset = GetTargetContentOffset(i); |
| 277 | + int64_t length = GetTargetContentLength(i); |
| 278 | + PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<Blob> blob, |
| 279 | + Blob::FromPath(file_path_, offset, length)); |
| 280 | + auto descriptor = blob->ToDescriptor(pool_); |
| 281 | + PAIMON_RETURN_NOT_OK_FROM_ARROW( |
| 282 | + field_builder->Append(descriptor->data(), descriptor->size())); |
| 283 | + } |
| 284 | + } |
| 285 | + std::shared_ptr<arrow::Array> array; |
| 286 | + PAIMON_RETURN_NOT_OK_FROM_ARROW(builder->Finish(&array)); |
| 287 | + return array; |
| 288 | +} |
| 289 | + |
| 290 | +Result<BatchReader::ReadBatch> BlobFileBatchReader::NextBatch() { |
| 291 | + if (closed_) { |
| 292 | + return Status::Invalid("blob file batch reader is closed"); |
| 293 | + } |
| 294 | + if (target_type_ == nullptr) { |
| 295 | + return Status::Invalid("target type is nullptr, call SetReadSchema first"); |
| 296 | + } |
| 297 | + if (current_pos_ >= target_blob_lengths_.size()) { |
| 298 | + PAIMON_ASSIGN_OR_RAISE(previous_batch_first_row_number_, GetNumberOfRows()); |
| 299 | + return BatchReader::MakeEofBatch(); |
| 300 | + } |
| 301 | + int32_t left_rows = target_blob_lengths_.size() - current_pos_; |
| 302 | + int32_t rows_to_read = std::min(left_rows, batch_size_); |
| 303 | + PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> blob_array, |
| 304 | + BuildTargetArray(rows_to_read)); |
| 305 | + std::unique_ptr<ArrowArray> c_array = std::make_unique<ArrowArray>(); |
| 306 | + std::unique_ptr<ArrowSchema> c_schema = std::make_unique<ArrowSchema>(); |
| 307 | + PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportArray(*blob_array, c_array.get(), c_schema.get())); |
| 308 | + previous_batch_first_row_number_ = target_blob_row_indexes_[current_pos_]; |
| 309 | + current_pos_ += rows_to_read; |
| 310 | + return make_pair(std::move(c_array), std::move(c_schema)); |
| 311 | +} |
| 312 | + |
| 313 | +Status BlobFileBatchReader::ReadBlobContentAt(const int64_t offset, const int64_t length, |
| 314 | + uint8_t* content) const { |
| 315 | + PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<OffsetInputStream> offset_input_stream, |
| 316 | + OffsetInputStream::Create(input_stream_, length, offset)); |
| 317 | + return StreamUtils::ReadAsyncFully(std::move(offset_input_stream), |
| 318 | + reinterpret_cast<char*>(content)); |
| 319 | +} |
| 320 | + |
| 321 | +int32_t BlobFileBatchReader::GetIndexLength(const int8_t* bytes, int32_t offset) { |
| 322 | + return static_cast<int32_t>( |
| 323 | + (static_cast<uint32_t>(static_cast<uint8_t>(bytes[offset + 3])) << 24) | |
| 324 | + (static_cast<uint32_t>(static_cast<uint8_t>(bytes[offset + 2])) << 16) | |
| 325 | + (static_cast<uint32_t>(static_cast<uint8_t>(bytes[offset + 1])) << 8) | |
| 326 | + static_cast<uint32_t>(static_cast<uint8_t>(bytes[offset]))); |
| 327 | +} |
| 328 | + |
| 329 | +// Note: blob file has no self-describing schema, use read schema instead. |
| 330 | +Result<std::unique_ptr<::ArrowSchema>> BlobFileBatchReader::GetFileSchema() const { |
| 331 | + return Status::NotImplemented("blob file has no self-describing file schema"); |
| 332 | +} |
| 333 | + |
| 334 | +} // namespace paimon::blob |
0 commit comments