Skip to content

Commit ddc8d63

Browse files
authored
feat(format): introduce blob file format (#98)
1 parent 337fccc commit ddc8d63

16 files changed

Lines changed: 2263 additions & 0 deletions
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
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

Comments
 (0)