blob: 48a4430ade6826571be6659f60968059a8e7c171 [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/format/parquet/file_reader_wrapper.h"
#include <algorithm>
#include <cassert>
#include <cstddef>
#include "arrow/io/interfaces.h"
#include "arrow/record_batch.h"
#include "arrow/util/range.h"
#include "fmt/format.h"
#include "paimon/common/utils/arrow/arrow_utils.h"
#include "paimon/format/parquet/column_index_filter.h"
#include "paimon/format/parquet/page_filtered_row_group_reader.h"
#include "paimon/format/parquet/parquet_format_defs.h"
#include "paimon/macros.h"
#include "parquet/arrow/reader.h"
#include "parquet/arrow/schema.h"
#include "parquet/file_reader.h"
#include "parquet/metadata.h"
#include "parquet/page_index.h"
namespace paimon::parquet {
namespace {
// Merge overlapping or adjacent ReadRanges into a minimal set of non-overlapping ranges.
// PreBufferRanges requires non-overlapping ranges, so this is necessary when combining
// ranges from multiple sources (page-level ranges, column chunk ranges, etc.).
std::vector<::arrow::io::ReadRange> MergeOverlappingRanges(
std::vector<::arrow::io::ReadRange> ranges) {
if (ranges.empty()) {
return ranges;
}
// Sort by offset
std::sort(ranges.begin(), ranges.end(),
[](const ::arrow::io::ReadRange& a, const ::arrow::io::ReadRange& b) {
return a.offset < b.offset;
});
std::vector<::arrow::io::ReadRange> merged;
merged.push_back(ranges[0]);
for (size_t i = 1; i < ranges.size(); ++i) {
auto& last = merged.back();
const auto& curr = ranges[i];
// Check if current range overlaps or is adjacent to the last merged range
int64_t last_end = last.offset + last.length;
if (curr.offset <= last_end) {
// Merge: extend the last range if current extends beyond it
int64_t curr_end = curr.offset + curr.length;
if (curr_end > last_end) {
last.length = curr_end - last.offset;
}
} else {
// No overlap, add as new range
merged.push_back(curr);
}
}
return merged;
}
} // namespace
Result<std::unique_ptr<FileReaderWrapper>> FileReaderWrapper::Create(
std::unique_ptr<::parquet::arrow::FileReader>&& file_reader, int64_t batch_size,
std::shared_ptr<::arrow::MemoryPool> pool) {
try {
if (file_reader == nullptr) {
return Status::Invalid("file reader wrapper create failed. file reader is nullptr");
}
std::vector<std::pair<uint64_t, uint64_t>> all_row_group_ranges;
auto meta_data = file_reader->parquet_reader()->metadata();
// prepare [start_row_idx, end_row_idx) for all row groups
uint64_t start_row_idx = 0;
for (int32_t i = 0; i < meta_data->num_row_groups(); i++) {
uint64_t end_row_idx = start_row_idx + meta_data->RowGroup(i)->num_rows();
all_row_group_ranges.emplace_back(start_row_idx, end_row_idx);
start_row_idx = end_row_idx;
}
uint64_t num_rows = file_reader->parquet_reader()->metadata()->num_rows();
if (start_row_idx != num_rows) {
assert(false);
return Status::Invalid(fmt::format(
"unexpected error. row group ranges not match with num rows {}", num_rows));
}
std::vector<int32_t> columns_indices =
arrow::internal::Iota(file_reader->parquet_reader()->metadata()->num_columns());
auto file_reader_wrapper = std::unique_ptr<FileReaderWrapper>(new FileReaderWrapper(
std::move(file_reader), all_row_group_ranges, num_rows, batch_size, pool));
std::vector<TargetRowGroup> all_target_row_groups;
for (int32_t i = 0; i < file_reader_wrapper->GetNumberOfRowGroups(); i++) {
all_target_row_groups.emplace_back(/*rg_index=*/i, /*is_partially_matched=*/false,
/*ranges=*/RowRanges());
}
PAIMON_RETURN_NOT_OK(
file_reader_wrapper->PrepareForReadingLazy(all_target_row_groups, columns_indices));
return file_reader_wrapper;
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("FileReaderWrapper::Create")
}
FileReaderWrapper::~FileReaderWrapper() {
WaitForPendingPreBuffer();
}
Result<std::shared_ptr<arrow::Schema>> FileReaderWrapper::GetSchema() const {
try {
std::shared_ptr<arrow::Schema> file_schema;
PAIMON_RETURN_NOT_OK_FROM_ARROW(file_reader_->GetSchema(&file_schema));
return file_schema;
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("FileReaderWrapper::GetSchema")
}
Status FileReaderWrapper::Close() {
try {
if (batch_reader_) {
PAIMON_RETURN_NOT_OK_FROM_ARROW(batch_reader_->Close());
}
return Status::OK();
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("FileReaderWrapper::Close")
}
FileReaderWrapper::FileReaderWrapper(
std::unique_ptr<::parquet::arrow::FileReader>&& file_reader,
const std::vector<std::pair<uint64_t, uint64_t>>& all_row_group_ranges, uint64_t num_rows,
int64_t batch_size, std::shared_ptr<::arrow::MemoryPool> pool)
: file_reader_(std::move(file_reader)),
all_row_group_ranges_(all_row_group_ranges),
pool_(std::move(pool)),
batch_size_(batch_size),
num_rows_(num_rows) {}
void FileReaderWrapper::WaitForPendingPreBuffer() {
if (!prebuffered_ranges_.empty() && file_reader_) {
// Wait for all outstanding PreBuffer async reads to complete before destruction.
// Without this, JindoSDK async pread callbacks may fire after the underlying
// buffers and memory pool are freed, causing use-after-free crashes.
auto status =
file_reader_->parquet_reader()->WhenBufferedRanges(prebuffered_ranges_).status();
(void)status; // Best-effort; ignore errors during cleanup
prebuffered_ranges_.clear();
}
}
void FileReaderWrapper::AdvanceToNextRowGroup() {
current_row_group_idx_++;
// Skip row groups excluded by read range.
while (current_row_group_idx_ < target_row_groups_.size() &&
target_row_groups_[current_row_group_idx_].IsExcludedByReadRange()) {
current_row_group_idx_++;
}
if (current_row_group_idx_ >= target_row_groups_.size()) {
next_row_to_read_ = num_rows_;
} else {
next_row_to_read_ =
all_row_group_ranges_[target_row_groups_[current_row_group_idx_].GetRowGroupIndex()]
.first;
}
}
Status FileReaderWrapper::SeekToRow(uint64_t row_number) {
try {
current_page_filtered_reader_.reset();
filtered_global_offset_ = 0;
for (uint64_t i = 0; i < target_row_groups_.size(); i++) {
if (target_row_groups_[i].IsExcludedByReadRange()) {
continue;
}
int32_t rg_id = target_row_groups_[i].GetRowGroupIndex();
uint64_t rg_start = all_row_group_ranges_[rg_id].first;
uint64_t rg_end = all_row_group_ranges_[rg_id].second;
if (row_number > rg_start && row_number < rg_end) {
return Status::Invalid(
fmt::format("seek to row failed. row number {} should not be in the middle of "
"readable range",
row_number));
}
if (rg_start >= row_number) {
current_row_group_idx_ = i;
next_row_to_read_ = rg_start;
// Rebuild batch_reader_ for non-page-filtered RGs at/after seek position.
std::vector<int32_t> fully_matched_indices;
for (uint64_t j = i; j < target_row_groups_.size(); j++) {
if (!target_row_groups_[j].IsExcludedByReadRange() &&
!target_row_groups_[j].IsPartiallyMatched()) {
fully_matched_indices.push_back(target_row_groups_[j].GetRowGroupIndex());
}
}
if (!fully_matched_indices.empty()) {
PAIMON_RETURN_NOT_OK_FROM_ARROW(file_reader_->GetRecordBatchReader(
fully_matched_indices, target_column_indices_, &batch_reader_));
} else {
batch_reader_.reset();
}
return Status::OK();
}
}
next_row_to_read_ = num_rows_;
current_row_group_idx_ = target_row_groups_.size();
return Status::OK();
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("FileReaderWrapper::SeekToRow")
}
Result<std::shared_ptr<arrow::RecordBatch>> FileReaderWrapper::NextPageFiltered() {
int32_t rg_id = target_row_groups_[current_row_group_idx_].GetRowGroupIndex();
// Construct the per-RG streaming reader on demand.
if (!current_page_filtered_reader_) {
const auto& target_rg = target_row_groups_[current_row_group_idx_];
auto row_group_page_index_reader = GetRowGroupPageIndexReader(rg_id);
auto page_ranges = PageFilteredRowGroupReader::ComputePageRanges(
target_rg, target_column_indices_, row_group_page_index_reader,
file_reader_->parquet_reader());
bool pre_buffered = !prebuffered_ranges_.empty();
int64_t max_chunksize = batch_size_ > 0 ? batch_size_ : std::numeric_limits<int64_t>::max();
PAIMON_ASSIGN_OR_RAISE(
current_page_filtered_reader_,
PageFilteredRowGroupReader::ReadFilteredRowGroup(
target_rg, target_column_indices_, file_reader_->properties().cache_options(),
pre_buffered, page_ranges, max_chunksize, row_group_page_index_reader, pool_,
file_reader_.get()));
current_filtered_row_ranges_ = target_rg.GetRowRanges();
current_filtered_rg_start_ = all_row_group_ranges_[rg_id].first;
filtered_global_offset_ = 0;
}
std::shared_ptr<arrow::RecordBatch> record_batch;
PAIMON_RETURN_NOT_OK_FROM_ARROW(current_page_filtered_reader_->ReadNext(&record_batch));
if (record_batch) {
auto original_row =
current_filtered_row_ranges_.MapFilteredIndexToOriginalRow(filtered_global_offset_);
previous_first_row_ = original_row.has_value() ? current_filtered_rg_start_ +
static_cast<uint64_t>(*original_row)
: current_filtered_rg_start_;
filtered_global_offset_ += record_batch->num_rows();
// Advance to the next row that survives filtering, or to the row group end when the
// filtered ranges are exhausted, so that next_row_to_read_ tracks the streaming position.
auto next_original_row =
current_filtered_row_ranges_.MapFilteredIndexToOriginalRow(filtered_global_offset_);
next_row_to_read_ =
next_original_row.has_value()
? current_filtered_rg_start_ + static_cast<uint64_t>(*next_original_row)
: all_row_group_ranges_[rg_id].second;
return record_batch;
}
// RG exhausted — reset and advance.
current_page_filtered_reader_.reset();
filtered_global_offset_ = 0;
AdvanceToNextRowGroup();
return std::shared_ptr<arrow::RecordBatch>();
}
Result<std::shared_ptr<arrow::RecordBatch>> FileReaderWrapper::NextFullyMatched() {
if (!batch_reader_) {
return std::shared_ptr<arrow::RecordBatch>();
}
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::RecordBatch> record_batch,
batch_reader_->Next());
if (!record_batch) {
return std::shared_ptr<arrow::RecordBatch>();
}
// Large binary columns (exceed 2GB) may split at different row boundaries. TableBatchReader
// aligns their chunks by slicing columns, which may leave non-zero child offsets.
PAIMON_ASSIGN_OR_RAISE(record_batch,
ArrowUtils::NormalizeRecordBatchOffsets(record_batch, pool_.get()));
int32_t rg_id = target_row_groups_[current_row_group_idx_].GetRowGroupIndex();
uint64_t rg_end = all_row_group_ranges_[rg_id].second;
int64_t num_rows = record_batch->num_rows();
previous_first_row_ = next_row_to_read_;
if (next_row_to_read_ + num_rows < rg_end) {
next_row_to_read_ += num_rows;
} else if (next_row_to_read_ + num_rows == rg_end) {
AdvanceToNextRowGroup();
} else {
return Status::Invalid(
fmt::format("Next failed. next_row_to_read {} + num_rows {} exceeds row group end {}",
next_row_to_read_, num_rows, rg_end));
}
return record_batch;
}
std::shared_ptr<::parquet::RowGroupPageIndexReader> FileReaderWrapper::GetRowGroupPageIndexReader(
int32_t row_group_index) {
auto cached = row_group_page_index_readers_.find(row_group_index);
if (cached != row_group_page_index_readers_.end()) {
return cached->second;
}
std::shared_ptr<::parquet::RowGroupPageIndexReader> row_group_page_index_reader;
auto page_index_reader = GetPageIndexReader();
if (page_index_reader) {
row_group_page_index_reader = page_index_reader->RowGroup(row_group_index);
}
// To avoid OOM, limit the number of row group page index readers cached in memory.
constexpr int32_t kMaxRowGroupPageIndexReaders = 1024;
if (row_group_page_index_readers_.size() < kMaxRowGroupPageIndexReaders) {
row_group_page_index_readers_.emplace(row_group_index, row_group_page_index_reader);
}
return row_group_page_index_reader;
}
Result<std::shared_ptr<arrow::RecordBatch>> FileReaderWrapper::Next() {
try {
if (PAIMON_UNLIKELY(!reader_initialized_)) {
PAIMON_RETURN_NOT_OK(PrepareForReading(target_row_groups_, target_column_indices_));
}
while (current_row_group_idx_ < target_row_groups_.size()) {
bool is_partially_matched =
target_row_groups_[current_row_group_idx_].IsPartiallyMatched();
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::RecordBatch> batch,
is_partially_matched ? NextPageFiltered() : NextFullyMatched());
if (batch) {
return batch;
} else if (!is_partially_matched) {
// Null from fully-matched path means batch_reader_ is globally exhausted.
break;
}
// current_row_group_idx_ has been advanced in NextPageFiltered() or NextFullyMatched(),
// loop to try next RG.
}
previous_first_row_ = next_row_to_read_;
return std::shared_ptr<arrow::RecordBatch>();
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("FileReaderWrapper::Next")
}
Result<std::vector<std::pair<uint64_t, uint64_t>>> FileReaderWrapper::GetRowGroupRanges(
const std::set<int32_t>& row_group_indices) const {
std::vector<std::pair<uint64_t, uint64_t>> row_group_ranges;
for (auto row_group_index : row_group_indices) {
if (static_cast<size_t>(row_group_index) >= all_row_group_ranges_.size()) {
return Status::Invalid(fmt::format("row group index {} is out of bound {}",
row_group_index, all_row_group_ranges_.size()));
}
row_group_ranges.push_back(all_row_group_ranges_[row_group_index]);
}
return row_group_ranges;
}
Status FileReaderWrapper::PrepareForReadingLazy(
const std::vector<TargetRowGroup>& target_row_groups,
const std::vector<int32_t>& column_indices) {
target_row_groups_ = target_row_groups;
target_column_indices_ = column_indices;
reader_initialized_ = false;
return Status::OK();
}
std::vector<::arrow::io::ReadRange> FileReaderWrapper::CollectPreBufferRanges(
const std::vector<int32_t>& column_indices) {
std::vector<::arrow::io::ReadRange> ranges;
auto file_metadata = file_reader_->parquet_reader()->metadata();
for (const auto& trg : target_row_groups_) {
if (trg.IsExcludedByReadRange()) continue;
if (trg.IsPartiallyMatched()) {
// Page-filtered RGs: only matching page byte ranges.
auto row_group_page_index_reader = GetRowGroupPageIndexReader(trg.GetRowGroupIndex());
auto page_ranges = PageFilteredRowGroupReader::ComputePageRanges(
trg, column_indices, row_group_page_index_reader, file_reader_->parquet_reader());
ranges.insert(ranges.end(), std::make_move_iterator(page_ranges.begin()),
std::make_move_iterator(page_ranges.end()));
} else {
// Fully-matched RGs: entire column chunk ranges.
auto rg_metadata = file_metadata->RowGroup(trg.GetRowGroupIndex());
for (int32_t col_idx : column_indices) {
auto col_chunk = rg_metadata->ColumnChunk(col_idx);
int64_t offset = col_chunk->data_page_offset();
if (col_chunk->has_dictionary_page() && col_chunk->dictionary_page_offset() > 0 &&
offset > col_chunk->dictionary_page_offset()) {
offset = col_chunk->dictionary_page_offset();
}
ranges.push_back({offset, col_chunk->total_compressed_size()});
}
}
}
return ranges;
}
void FileReaderWrapper::DispatchPreBuffer(std::vector<::arrow::io::ReadRange> ranges) {
const auto& cache_opts = file_reader_->properties().cache_options();
::arrow::io::IOContext io_ctx(pool_.get());
auto merged_ranges = MergeOverlappingRanges(std::move(ranges));
try {
file_reader_->parquet_reader()->PreBufferRanges(merged_ranges, io_ctx, cache_opts);
prebuffered_ranges_ = std::move(merged_ranges);
} catch (const std::exception&) {
prebuffered_ranges_.clear();
}
}
Status FileReaderWrapper::PrepareForReading(const std::vector<TargetRowGroup>& target_row_groups,
const std::vector<int32_t>& column_indices) {
try {
target_row_groups_ = target_row_groups;
target_column_indices_ = column_indices;
// Partition into fully-matched and page-filtered row groups, skipping excluded ones.
std::vector<int32_t> fully_matched_row_groups;
uint64_t active_count = 0;
for (const auto& trg : target_row_groups_) {
if (trg.IsExcludedByReadRange()) {
continue;
}
active_count++;
if (!trg.IsPartiallyMatched()) {
fully_matched_row_groups.push_back(trg.GetRowGroupIndex());
}
}
bool has_partially_matched = fully_matched_row_groups.size() != active_count;
WaitForPendingPreBuffer();
// TODO(Yonghao Fang): Neither Paimon nor Arrow manage the size and lifecycle of prebuffered
// caches. So when a lot of row is needed, there is possibility of OOM due to too much
// prebuffering. Also, DispatchPreBuffer will drop previous prebuffered ranges by
// GetRecordBatchReader, which cause IO wastes.
// Create standard reader for fully-matched row groups.
if (!fully_matched_row_groups.empty()) {
PAIMON_RETURN_NOT_OK_FROM_ARROW(file_reader_->GetRecordBatchReader(
fully_matched_row_groups, column_indices, &batch_reader_));
} else {
batch_reader_.reset();
}
// When page-filtered RGs exist, issue a single PreBuffer covering both kinds.
// Otherwise GetRecordBatchReader already issued PreBuffer internally.
if (has_partially_matched) {
auto all_ranges = CollectPreBufferRanges(column_indices);
DispatchPreBuffer(std::move(all_ranges));
}
// Reset read state. Find the first non-excluded row group.
uint64_t first_active_idx = 0;
while (first_active_idx < target_row_groups_.size() &&
target_row_groups_[first_active_idx].IsExcludedByReadRange()) {
first_active_idx++;
}
if (first_active_idx >= target_row_groups_.size()) {
next_row_to_read_ = num_rows_;
} else {
next_row_to_read_ =
all_row_group_ranges_[target_row_groups_[first_active_idx].GetRowGroupIndex()]
.first;
}
previous_first_row_ = std::numeric_limits<uint64_t>::max();
current_row_group_idx_ = first_active_idx;
reader_initialized_ = true;
return Status::OK();
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("FileReaderWrapper::PrepareForReading")
}
Status FileReaderWrapper::ApplyReadRanges(
const std::vector<std::pair<uint64_t, uint64_t>>& read_ranges) {
if (read_ranges.empty()) {
for (auto& trg : target_row_groups_) {
trg.SetExcludedByReadRange(true);
}
reader_initialized_ = false;
return Status::OK();
}
// Build a set of row group indices whose range matches one of the read ranges.
std::set<int32_t> matching_rg_indices;
for (const auto& read_range : read_ranges) {
for (size_t i = 0; i < all_row_group_ranges_.size(); i++) {
if (all_row_group_ranges_[i] == read_range) {
matching_rg_indices.insert(static_cast<int32_t>(i));
}
}
}
// Mark each target row group as excluded or not based on the matching set.
for (auto& trg : target_row_groups_) {
trg.SetExcludedByReadRange(matching_rg_indices.count(trg.GetRowGroupIndex()) == 0);
}
reader_initialized_ = false;
return Status::OK();
}
std::shared_ptr<::parquet::PageIndexReader> FileReaderWrapper::GetPageIndexReader() {
try {
return file_reader_->parquet_reader()->GetPageIndexReader();
} catch (...) {
// Page index is optional; degrade gracefully if the metadata read throws.
return nullptr;
}
}
Result<RowRanges> FileReaderWrapper::CalculateFilteredRowRanges(
int32_t row_group_index, const std::shared_ptr<Predicate>& predicate,
const std::map<std::string, int32_t>& column_name_to_index) {
try {
auto meta_data = file_reader_->parquet_reader()->metadata();
int64_t row_count = meta_data->RowGroup(row_group_index)->num_rows();
if (!predicate) {
return RowRanges::CreateSingle(row_count);
}
return ColumnIndexFilter::CalculateRowRanges(predicate,
GetRowGroupPageIndexReader(row_group_index),
column_name_to_index, row_count);
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("FileReaderWrapper::CalculateFilteredRowRanges")
}
} // namespace paimon::parquet