blob: 6ca3106b39904889add6ea7be3938b33b55f0998 [file]
/*
* 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 "paimon/common/reader/blob_fallback_batch_reader.h"
#include <algorithm>
#include <string_view>
#include <utility>
#include "arrow/api.h"
#include "arrow/c/bridge.h"
#include "fmt/format.h"
#include "paimon/common/data/blob_defs.h"
#include "paimon/common/data/blob_utils.h"
#include "paimon/common/metrics/metrics_impl.h"
#include "paimon/common/reader/reader_utils.h"
#include "paimon/common/table/special_fields.h"
#include "paimon/common/utils/arrow/mem_utils.h"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/checked_cast.h"
namespace paimon {
Result<std::unique_ptr<BlobFallbackBatchReader>> BlobFallbackBatchReader::Create(
std::vector<std::vector<Segment>>&& sequence_groups,
const std::shared_ptr<arrow::Schema>& read_schema, int32_t read_batch_size,
const std::shared_ptr<MemoryPool>& pool) {
if (sequence_groups.size() < 2) {
return Status::Invalid(
"Blob fallback needs at least two sequence groups; a single group should be read "
"sequentially.");
}
if (read_schema == nullptr) {
return Status::Invalid("Blob fallback read schema cannot be nullptr.");
}
if (read_batch_size <= 0) {
return Status::Invalid(fmt::format(
"Blob fallback read batch size '{}' should be larger than zero", read_batch_size));
}
int32_t blob_field_idx = -1;
for (int32_t i = 0; i < read_schema->num_fields(); i++) {
if (BlobUtils::IsBlobField(read_schema->field(i))) {
if (blob_field_idx != -1) {
return Status::Invalid(
"Blob fallback read schema should contain exactly one blob field.");
}
blob_field_idx = i;
}
}
if (blob_field_idx == -1) {
return Status::Invalid("Blob fallback read schema should contain a blob field.");
}
int32_t row_id_field_idx = read_schema->GetFieldIndex(SpecialFields::RowId().Name());
int32_t seq_num_field_idx = read_schema->GetFieldIndex(SpecialFields::SequenceNumber().Name());
std::vector<GroupCursor> groups;
groups.reserve(sequence_groups.size());
for (auto& segments : sequence_groups) {
if (segments.empty()) {
return Status::Invalid("Blob fallback sequence group should not be empty.");
}
for (const auto& segment : segments) {
if (segment.reader == nullptr && segment.gap_selected_ranges.empty()) {
return Status::Invalid(
"Blob fallback gap segment should cover at least one selected row id.");
}
}
GroupCursor cursor;
cursor.segments = std::move(segments);
groups.push_back(std::move(cursor));
}
return std::unique_ptr<BlobFallbackBatchReader>(
new BlobFallbackBatchReader(std::move(groups), read_schema, blob_field_idx,
row_id_field_idx, seq_num_field_idx, read_batch_size, pool));
}
BlobFallbackBatchReader::BlobFallbackBatchReader(std::vector<GroupCursor>&& groups,
const std::shared_ptr<arrow::Schema>& read_schema,
int32_t blob_field_idx, int32_t row_id_field_idx,
int32_t seq_num_field_idx, int32_t read_batch_size,
const std::shared_ptr<MemoryPool>& pool)
: groups_(std::move(groups)),
read_schema_(read_schema),
blob_field_idx_(blob_field_idx),
row_id_field_idx_(row_id_field_idx),
seq_num_field_idx_(seq_num_field_idx),
read_batch_size_(read_batch_size),
arrow_pool_(GetArrowPool(pool)) {}
Result<int64_t> BlobFallbackBatchReader::FillWindow(size_t group_idx, int64_t want,
std::vector<Chunk>* chunks) {
GroupCursor& cursor = groups_[group_idx];
int64_t collected = 0;
while (collected < want) {
if (!cursor.pending.empty()) {
const std::shared_ptr<arrow::StructArray>& front = cursor.pending.front();
int64_t available = front->length() - cursor.pending_pos;
int64_t take = std::min(available, want - collected);
chunks->push_back(Chunk{front, cursor.pending_pos, take, {}});
cursor.pending_pos += take;
collected += take;
if (cursor.pending_pos == front->length()) {
cursor.pending.pop_front();
cursor.pending_pos = 0;
}
continue;
}
if (cursor.segment_idx >= cursor.segments.size()) {
// group exhausted; only the first group may define a shorter window
break;
}
Segment& segment = cursor.segments[cursor.segment_idx];
if (segment.reader == nullptr) {
// gap segment: all rows are placeholders, stepped range by range so the row ids
// stay available for all-placeholder rows
if (cursor.gap_range_idx >= segment.gap_selected_ranges.size()) {
cursor.segment_idx++;
cursor.gap_range_idx = 0;
cursor.gap_range_pos = 0;
continue;
}
const Range& range = segment.gap_selected_ranges[cursor.gap_range_idx];
int64_t remaining = range.Count() - cursor.gap_range_pos;
int64_t take = std::min(remaining, want - collected);
Chunk chunk{nullptr, 0, take, {}};
if (row_id_field_idx_ >= 0) {
chunk.gap_row_ids.reserve(take);
for (int64_t k = 0; k < take; k++) {
chunk.gap_row_ids.push_back(range.from + cursor.gap_range_pos + k);
}
}
chunks->push_back(std::move(chunk));
cursor.gap_range_pos += take;
collected += take;
if (cursor.gap_range_pos == range.Count()) {
cursor.gap_range_idx++;
cursor.gap_range_pos = 0;
}
continue;
}
PAIMON_ASSIGN_OR_RAISE(ReadBatchWithBitmap batch_with_bitmap,
segment.reader->NextBatchWithBitmap());
if (BatchReader::IsEofBatch(batch_with_bitmap)) {
cursor.segment_idx++;
continue;
}
auto& [read_batch, bitmap] = batch_with_bitmap;
auto& [c_array, c_schema] = read_batch;
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> src_array,
arrow::ImportArray(c_array.get(), c_schema.get()));
PAIMON_ASSIGN_OR_RAISE(arrow::ArrayVector selected_array_vec,
ReaderUtils::GenerateFilteredArrayVector(src_array, bitmap));
for (const auto& selected_array : selected_array_vec) {
if (selected_array->length() == 0) {
continue;
}
if (!selected_array || selected_array->type_id() != arrow::Type::STRUCT) {
return Status::Invalid("Blob fallback expects file readers to emit struct arrays.");
}
auto struct_array = checked_pointer_cast<arrow::StructArray>(selected_array);
cursor.pending.push_back(std::move(struct_array));
}
}
return collected;
}
Result<std::vector<bool>> BlobFallbackBatchReader::ComputePlaceholderFlags(
const std::vector<Chunk>& chunks, int64_t row_count) const {
std::vector<bool> flags(row_count, false);
int64_t pos = 0;
for (const auto& chunk : chunks) {
if (chunk.array == nullptr) {
// gap rows stand for placeholders
std::fill(flags.begin() + pos, flags.begin() + pos + chunk.length, true);
} else {
std::shared_ptr<arrow::Array> blob_col = chunk.array->field(blob_field_idx_);
if (!blob_col || blob_col->type_id() != arrow::Type::LARGE_BINARY) {
return Status::Invalid(fmt::format(
"Blob fallback expects the blob column to be large binary, but got {}",
blob_col ? blob_col->type()->ToString() : "null"));
}
auto binary_col = checked_pointer_cast<arrow::LargeBinaryArray>(blob_col);
for (int64_t k = 0; k < chunk.length; k++) {
int64_t idx = chunk.offset + k;
if (binary_col->IsNull(idx)) {
continue;
}
std::string_view value = binary_col->GetView(idx);
if (BlobDefs::IsPlaceholderSentinel(value.data(), value.size())) {
flags[pos + k] = true;
}
}
}
pos += chunk.length;
}
return flags;
}
Result<std::shared_ptr<arrow::Array>> BlobFallbackBatchReader::AssembleRowIdRun(
const std::vector<Chunk>& chunks, int64_t run_start, int64_t run_end) const {
arrow::ArrayVector pieces;
int64_t pos = 0;
for (const auto& chunk : chunks) {
int64_t overlap_start = std::max(run_start, pos);
int64_t overlap_end = std::min(run_end, pos + chunk.length);
if (overlap_start < overlap_end) {
if (chunk.array != nullptr) {
std::shared_ptr<arrow::Array> column = chunk.array->field(row_id_field_idx_);
pieces.push_back(column->Slice(chunk.offset + (overlap_start - pos),
overlap_end - overlap_start));
} else {
arrow::Int64Builder builder(arrow_pool_.get());
for (int64_t r = overlap_start; r < overlap_end; r++) {
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Append(chunk.gap_row_ids[r - pos]));
}
std::shared_ptr<arrow::Array> piece;
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Finish(&piece));
pieces.push_back(std::move(piece));
}
}
pos += chunk.length;
if (pos >= run_end) {
break;
}
}
if (pieces.size() == 1 && pieces[0]->offset() == 0) {
return pieces[0];
}
// Concatenate flattens non-zero offsets left by Slice, so the exported batch honors the
// zero-offset BatchReader contract.
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> concat_array,
arrow::Concatenate(pieces, arrow_pool_.get()));
return concat_array;
}
Result<std::shared_ptr<arrow::Array>> BlobFallbackBatchReader::AssembleColumn(
int32_t field_idx, const std::vector<int32_t>& group_choice,
const std::vector<std::vector<Chunk>>& group_chunks) const {
const auto row_count = static_cast<int64_t>(group_choice.size());
arrow::ArrayVector pieces;
int64_t run_start = 0;
while (run_start < row_count) {
const int32_t group = group_choice[run_start];
int64_t run_end = run_start + 1;
while (run_end < row_count && group_choice[run_end] == group) {
run_end++;
}
if (group < 0) {
// placeholder in every layer: the blob degrades to null; the row keeps its row id
// (taken from the newest group, which steps in lockstep), reports -1 as its
// sequence number, and returns null for every other field
if (field_idx == row_id_field_idx_) {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> row_id_piece,
AssembleRowIdRun(group_chunks[0], run_start, run_end));
pieces.push_back(std::move(row_id_piece));
} else if (field_idx == seq_num_field_idx_) {
arrow::Int64Scalar seq_scalar(-1);
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
std::shared_ptr<arrow::Array> seq_piece,
arrow::MakeArrayFromScalar(seq_scalar, run_end - run_start, arrow_pool_.get()));
pieces.push_back(std::move(seq_piece));
} else {
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
std::shared_ptr<arrow::Array> null_piece,
arrow::MakeArrayOfNull(read_schema_->field(field_idx)->type(),
run_end - run_start, arrow_pool_.get()));
pieces.push_back(std::move(null_piece));
}
} else {
int64_t pos = 0;
for (const auto& chunk : group_chunks[group]) {
int64_t overlap_start = std::max(run_start, pos);
int64_t overlap_end = std::min(run_end, pos + chunk.length);
if (overlap_start < overlap_end) {
if (chunk.array == nullptr) {
return Status::Invalid(
"Unexpected: a gap row was chosen as a blob fallback result.");
}
std::shared_ptr<arrow::Array> column = chunk.array->field(field_idx);
pieces.push_back(column->Slice(chunk.offset + (overlap_start - pos),
overlap_end - overlap_start));
}
pos += chunk.length;
if (pos >= run_end) {
break;
}
}
}
run_start = run_end;
}
if (pieces.size() == 1 && pieces[0]->offset() == 0) {
return pieces[0];
}
// Concatenate flattens non-zero offsets left by Slice, so the exported batch honors the
// zero-offset BatchReader contract.
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> concat_array,
arrow::Concatenate(pieces, arrow_pool_.get()));
return concat_array;
}
Result<BatchReader::ReadBatch> BlobFallbackBatchReader::NextBatch() {
if (closed_) {
return Status::Invalid("blob fallback batch reader is closed");
}
std::vector<std::vector<Chunk>> group_chunks(groups_.size());
// the first (newest) group defines the window; the others must step in lockstep
PAIMON_ASSIGN_OR_RAISE(int64_t row_count, FillWindow(0, read_batch_size_, &group_chunks[0]));
for (size_t g = 1; g < groups_.size(); g++) {
// ask for one row even when the first group is exhausted, so that a longer group is
// reported as a misalignment instead of silently truncating the read
const int64_t want = std::max<int64_t>(row_count, 1);
PAIMON_ASSIGN_OR_RAISE(int64_t got, FillWindow(g, want, &group_chunks[g]));
if (got != row_count) {
return Status::Invalid(fmt::format(
"All sequence groups of a blob fallback read should have the same number of "
"rows: group {} yielded {} rows in a window of {}",
g, got, row_count));
}
}
if (row_count == 0) {
return BatchReader::MakeEofBatch();
}
std::vector<std::vector<bool>> placeholder_flags(groups_.size());
for (size_t g = 0; g < groups_.size(); g++) {
PAIMON_ASSIGN_OR_RAISE(placeholder_flags[g],
ComputePlaceholderFlags(group_chunks[g], row_count));
}
// per row, the first group in max-sequence order with a real entry wins; -1 means the row is
// a placeholder in every group
std::vector<int32_t> group_choice(row_count, -1);
for (int64_t r = 0; r < row_count; r++) {
for (size_t g = 0; g < groups_.size(); g++) {
if (!placeholder_flags[g][r]) {
group_choice[r] = static_cast<int32_t>(g);
break;
}
}
}
arrow::ArrayVector columns;
columns.reserve(read_schema_->num_fields());
for (int32_t field_idx = 0; field_idx < read_schema_->num_fields(); field_idx++) {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> column,
AssembleColumn(field_idx, group_choice, group_chunks));
columns.push_back(std::move(column));
}
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::StructArray> target_array,
arrow::StructArray::Make(columns, read_schema_->fields()));
std::unique_ptr<ArrowArray> c_array = std::make_unique<ArrowArray>();
std::unique_ptr<ArrowSchema> c_schema = std::make_unique<ArrowSchema>();
PAIMON_RETURN_NOT_OK_FROM_ARROW(
arrow::ExportArray(*target_array, c_array.get(), c_schema.get()));
return std::make_pair(std::move(c_array), std::move(c_schema));
}
Result<BatchReader::ReadBatchWithBitmap> BlobFallbackBatchReader::NextBatchWithBitmap() {
PAIMON_ASSIGN_OR_RAISE(ReadBatch batch, NextBatch());
if (BatchReader::IsEofBatch(batch)) {
return BatchReader::MakeEofBatchWithBitmap();
}
return ReaderUtils::AddAllValidBitmap(std::move(batch));
}
void BlobFallbackBatchReader::Close() {
for (auto& group : groups_) {
group.pending.clear();
for (auto& segment : group.segments) {
if (segment.reader) {
segment.reader->Close();
}
}
}
closed_ = true;
}
std::shared_ptr<Metrics> BlobFallbackBatchReader::GetReaderMetrics() const {
auto metrics = std::make_shared<MetricsImpl>();
for (const auto& group : groups_) {
for (const auto& segment : group.segments) {
if (segment.reader) {
auto reader_metrics = segment.reader->GetReaderMetrics();
if (reader_metrics) {
metrics->Merge(reader_metrics);
}
}
}
}
return metrics;
}
} // namespace paimon