blob: c0cd40e19d809e0402f39bbfb725a08e466ba3a2 [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/parquet_file_batch_reader.h"
#include <algorithm>
#include <cstddef>
#include <unordered_map>
#include "arrow/acero/options.h"
#include "arrow/array/array_nested.h"
#include "arrow/c/abi.h"
#include "arrow/c/bridge.h"
#include "arrow/compute/api.h"
#include "arrow/dataset/dataset.h"
#include "arrow/dataset/file_base.h"
#include "arrow/dataset/file_parquet.h"
#include "arrow/dataset/type_fwd.h"
#include "arrow/io/caching.h"
#include "arrow/io/interfaces.h"
#include "arrow/record_batch.h"
#include "arrow/type.h"
#include "arrow/util/range.h"
#include "arrow/util/thread_pool.h"
#include "fmt/format.h"
#include "paimon/common/metrics/metrics_impl.h"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/options_utils.h"
#include "paimon/common/utils/scope_guard.h"
#include "paimon/common/utils/string_utils.h"
#include "paimon/core/schema/arrow_schema_validator.h"
#include "paimon/core/utils/nested_projection_utils.h"
#include "paimon/format/parquet/parquet_field_id_converter.h"
#include "paimon/format/parquet/parquet_format_defs.h"
#include "paimon/format/parquet/parquet_schema_util.h"
#include "paimon/format/parquet/parquet_timestamp_converter.h"
#include "paimon/format/parquet/predicate_converter.h"
#include "paimon/reader/batch_reader.h"
#include "paimon/utils/roaring_bitmap32.h"
#include "parquet/arrow/reader.h"
#include "parquet/properties.h"
namespace arrow {
class MemoryPool;
} // namespace arrow
namespace paimon {
class Predicate;
} // namespace paimon
namespace paimon::parquet {
namespace {
// LIST/MAP do not support pruning fields from their nested value types, but physical and
// logical leaf types may still differ (for example, Parquet reports LTZ timestamps as UTC
// while Paimon exposes them in the local timezone). Compare only the nested projection shape
// here so those representation differences are handled by the normal cast path.
bool HasSameNestedProjectionShape(const std::shared_ptr<arrow::DataType>& read_type,
const std::shared_ptr<arrow::DataType>& file_type) {
const bool read_is_nested = ArrowSchemaValidator::IsNestedType(read_type);
const bool file_is_nested = ArrowSchemaValidator::IsNestedType(file_type);
if (!read_is_nested || !file_is_nested) {
if (read_is_nested || file_is_nested) {
return false;
}
// ParquetTimestampConverter explicitly supports timestamp unit and timezone
// conversion after reading. Other atomic type differences remain unsupported here.
if (read_type->id() == arrow::Type::TIMESTAMP &&
file_type->id() == arrow::Type::TIMESTAMP) {
const auto& read_timestamp = static_cast<const arrow::TimestampType&>(*read_type);
const auto& file_timestamp = static_cast<const arrow::TimestampType&>(*file_type);
return read_timestamp.unit() == file_timestamp.unit() ||
(file_timestamp.unit() == arrow::TimeUnit::MILLI &&
read_timestamp.unit() == arrow::TimeUnit::SECOND);
}
return read_type->Equals(file_type);
}
if (read_type->id() != file_type->id()) {
return false;
}
switch (file_type->id()) {
case arrow::Type::STRUCT: {
if (read_type->num_fields() != file_type->num_fields()) {
return false;
}
for (int32_t i = 0; i < file_type->num_fields(); ++i) {
const auto& read_child = read_type->field(i);
const auto& file_child = file_type->field(i);
if (read_child->name() != file_child->name() ||
!HasSameNestedProjectionShape(read_child->type(), file_child->type())) {
return false;
}
}
return true;
}
case arrow::Type::LIST: {
const auto& read_list = static_cast<const arrow::ListType&>(*read_type);
const auto& file_list = static_cast<const arrow::ListType&>(*file_type);
return HasSameNestedProjectionShape(read_list.value_type(), file_list.value_type());
}
case arrow::Type::MAP: {
const auto& read_map = static_cast<const arrow::MapType&>(*read_type);
const auto& file_map = static_cast<const arrow::MapType&>(*file_type);
return HasSameNestedProjectionShape(read_map.key_type(), file_map.key_type()) &&
HasSameNestedProjectionShape(read_map.item_type(), file_map.item_type());
}
default:
return false;
}
}
} // namespace
ParquetFileBatchReader::ParquetFileBatchReader(
std::shared_ptr<arrow::io::RandomAccessFile>&& input_stream,
std::unique_ptr<FileReaderWrapper>&& reader, const std::map<std::string, std::string>& options,
const std::shared_ptr<arrow::MemoryPool>& arrow_pool,
std::shared_ptr<std::atomic<uint64_t>> storage_read_bytes)
: options_(options),
arrow_pool_(arrow_pool),
input_stream_(std::move(input_stream)),
reader_(std::move(reader)),
metrics_(std::make_shared<MetricsImpl>()),
storage_read_bytes_(std::move(storage_read_bytes)),
logger_(Logger::GetLogger("ParquetFileBatchReader")) {}
Result<std::unique_ptr<ParquetFileBatchReader>> ParquetFileBatchReader::Create(
std::shared_ptr<arrow::io::RandomAccessFile>&& input_stream,
const std::map<std::string, std::string>& options, int32_t batch_size,
std::shared_ptr<::parquet::FileMetaData> file_metadata,
std::shared_ptr<std::atomic<uint64_t>> storage_read_bytes,
const std::shared_ptr<arrow::MemoryPool>& pool) {
try {
assert(input_stream);
PAIMON_ASSIGN_OR_RAISE(::parquet::ReaderProperties reader_properties,
CreateReaderProperties(pool, options));
PAIMON_ASSIGN_OR_RAISE(::parquet::ArrowReaderProperties arrow_reader_properties,
CreateArrowReaderProperties(pool, options, batch_size));
::parquet::arrow::FileReaderBuilder file_reader_builder;
PAIMON_RETURN_NOT_OK_FROM_ARROW(
file_reader_builder.Open(input_stream, reader_properties, std::move(file_metadata)));
std::unique_ptr<::parquet::arrow::FileReader> file_reader;
PAIMON_RETURN_NOT_OK_FROM_ARROW(file_reader_builder.memory_pool(pool.get())
->properties(arrow_reader_properties)
->Build(&file_reader));
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<FileReaderWrapper> reader,
FileReaderWrapper::Create(std::move(file_reader),
static_cast<int64_t>(batch_size), pool));
auto parquet_file_batch_reader = std::unique_ptr<ParquetFileBatchReader>(
new ParquetFileBatchReader(std::move(input_stream), std::move(reader), options, pool,
std::move(storage_read_bytes)));
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<::ArrowSchema> file_schema,
parquet_file_batch_reader->GetFileSchema());
PAIMON_RETURN_NOT_OK(parquet_file_batch_reader->SetReadSchema(
file_schema.get(), /*predicate=*/nullptr, /*selection_bitmap=*/std::nullopt));
return parquet_file_batch_reader;
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("ParquetFileBatchReader::Create")
}
Result<std::unique_ptr<::ArrowSchema>> ParquetFileBatchReader::GetFileSchema() const {
try {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema> file_schema, reader_->GetSchema());
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema> new_schema,
ParquetFieldIdConverter::GetPaimonIdsFromParquetIds(file_schema));
PAIMON_ASSIGN_OR_RAISE(
std::shared_ptr<arrow::DataType> new_type,
ParquetTimestampConverter::AdjustTimezone(arrow::struct_(new_schema->fields())));
auto c_schema = std::make_unique<::ArrowSchema>();
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportType(*new_type, c_schema.get()));
return c_schema;
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("ParquetFileBatchReader::GetFileSchema")
}
Status ParquetFileBatchReader::SetReadSchema(
::ArrowSchema* schema, const std::shared_ptr<Predicate>& predicate,
const std::optional<RoaringBitmap32>& selection_bitmap) {
try {
if (!schema) {
return Status::Invalid("SetReadSchema failed: read schema cannot be nullptr");
}
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Schema> read_schema,
arrow::ImportSchema(schema));
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema> file_schema, reader_->GetSchema());
// Recursively match read_schema against file_schema by field names.
// STRUCT supports sub-field projection; LIST/MAP require exact type match.
PAIMON_ASSIGN_OR_RAISE(std::vector<int32_t> column_indices,
ComputeNestedColumnIndices(read_schema, file_schema));
// Build column name to index map for page-level filtering.
// We still need the full per-top-level-field leaf indices for predicate pushdown.
std::unordered_map<std::string, std::vector<int32_t>> field_index_map;
int32_t flat_idx = 0;
for (const auto& field : file_schema->fields()) {
std::vector<int32_t> leaf_indices;
FlattenSchema(field->type(), &flat_idx, &leaf_indices);
field_index_map[field->name()] = leaf_indices;
}
TargetRowGroups target_row_groups =
TargetRowGroup::MakeForAllRowGroups(reader_->GetAllRowGroupRanges());
PAIMON_ASSIGN_OR_RAISE(
bool enable_page_index_filter,
OptionsUtils::GetValueFromMap<bool>(options_, PARQUET_READ_ENABLE_PAGE_INDEX_FILTER,
DEFAULT_PARQUET_READ_ENABLE_PAGE_INDEX_FILTER));
if (predicate) {
PAIMON_ASSIGN_OR_RAISE(
target_row_groups,
FilterRowGroupsByPredicate(predicate, file_schema, target_row_groups));
}
if (selection_bitmap) {
PAIMON_ASSIGN_OR_RAISE(
target_row_groups,
FilterRowGroupsByBitmap(selection_bitmap.value(), target_row_groups));
if (enable_page_index_filter) {
// To decide which strategy to use, "trim" or "coalesce". "Coalesce" By default.
PAIMON_ASSIGN_OR_RAISE(
std::string strategy,
OptionsUtils::GetValueFromMap<std::string>(
options_, PARQUET_READ_BITMAP_ROW_RANGE_REFINING_STRATEGY,
DEFAULT_PARQUET_READ_BITMAP_STRATEGY));
if (strategy == "trim") {
PAIMON_ASSIGN_OR_RAISE(
target_row_groups,
RefineRowRangesByTrimming(selection_bitmap.value(), target_row_groups,
column_indices));
} else if (strategy == "coalesce") {
PAIMON_ASSIGN_OR_RAISE(
target_row_groups,
RefineRowRangesByCoalescing(selection_bitmap.value(), target_row_groups));
} else {
return Status::Invalid(
fmt::format("Invalid row range refining strategy :{}, valid strategies "
"are: trim, coalesce",
strategy));
}
}
}
// Apply page-level filtering after bitmap pruning so we don't read page index
// pages for row groups that the bitmap already excluded.
// If no predicate is provided, skip page-level filtering
if (predicate && !target_row_groups.empty()) {
if (enable_page_index_filter) {
// Build column name to index map for page-level filtering.
// For leaf columns, indices[0] is the correct leaf column index in Parquet.
// For nested types (struct/list/map), FlattenSchema produces multiple leaf indices,
// but predicate pushdown only targets leaf columns with simple types, so indices[0]
// is always the correct single leaf index for predicate evaluation.
std::map<std::string, int32_t> column_name_to_index;
for (const auto& [name, indices] : field_index_map) {
if (!indices.empty()) {
column_name_to_index[name] = indices[0];
}
}
PAIMON_ASSIGN_OR_RAISE(
target_row_groups,
FilterRowGroupsByPageIndex(predicate, column_name_to_index, target_row_groups));
}
}
read_data_type_ = arrow::struct_(read_schema->fields());
metrics_->SetCounter(ParquetMetrics::READ_ROW_GROUPS_TOTAL,
reader_->GetNumberOfRowGroups());
metrics_->SetCounter(ParquetMetrics::READ_ROW_GROUPS_AFTER_FILTER,
target_row_groups.size());
PAIMON_RETURN_NOT_OK(UpdateAllTargetRowRanges(target_row_groups));
PAIMON_RETURN_NOT_OK(reader_->PrepareForReadingLazy(target_row_groups, column_indices));
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("ParquetFileBatchReader::SetReadSchema")
return Status::OK();
}
Result<TargetRowGroups> ParquetFileBatchReader::FilterRowGroupsByPredicate(
const std::shared_ptr<Predicate>& predicate, const std::shared_ptr<arrow::Schema> file_schema,
const TargetRowGroups& src_row_groups) const {
if (!predicate) {
return Status::Invalid("cannot pushdown an empty predicate");
}
// convert paimon predicate to arrow expression
PAIMON_ASSIGN_OR_RAISE(
uint32_t predicate_node_count_limit,
OptionsUtils::GetValueFromMap<uint32_t>(options_, PARQUET_READ_PREDICATE_NODE_COUNT_LIMIT,
DEFAULT_PARQUET_READ_PREDICATE_NODE_COUNT_LIMIT));
PAIMON_ASSIGN_OR_RAISE(arrow::compute::Expression expr,
PredicateConverter::Convert(predicate, predicate_node_count_limit));
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(arrow::Expression bind_expr, expr.Bind(*file_schema));
// prepare file source
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(int64_t file_length, input_stream_->GetSize());
auto file_source = arrow::dataset::FileSource(input_stream_, /*size=*/file_length);
// filter row group by arrow expression and row group meta
auto parquet_file_format = std::make_shared<arrow::dataset::ParquetFileFormat>();
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
std::shared_ptr<arrow::dataset::ParquetFileFragment> file_fragment,
parquet_file_format->MakeFragment(
file_source, /*partition_expression=*/PredicateConverter::AlwaysTrue(),
/*physical_schema=*/nullptr,
/*row_groups=*/TargetRowGroup::GetRowGroupIndices(src_row_groups)));
PAIMON_RETURN_NOT_OK_FROM_ARROW(
file_fragment->EnsureCompleteMetadata(reader_->GetFileReader()));
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(arrow::dataset::FragmentVector target_fragments,
file_fragment->SplitByRowGroup(bind_expr));
TargetRowGroups target_row_groups;
target_row_groups.reserve(src_row_groups.size());
for (const auto& fragment : target_fragments) {
auto parquet_fragment = dynamic_cast<arrow::dataset::ParquetFileFragment*>(fragment.get());
if (!parquet_fragment) {
return Status::Invalid("cannot cast to ParquetFileFragment in ParquetFileBatchReader");
}
for (auto rg_index : parquet_fragment->row_groups()) {
for (const auto& row_group : src_row_groups) {
if (row_group.GetRowGroupIndex() == rg_index) {
target_row_groups.emplace_back(row_group);
break;
}
}
}
}
return target_row_groups;
}
Result<TargetRowGroups> ParquetFileBatchReader::FilterRowGroupsByBitmap(
const RoaringBitmap32& bitmap, const TargetRowGroups& src_row_groups) const {
if (bitmap.IsEmpty()) {
return Status::Invalid("cannot push down an empty bitmap to ParquetFileBatchReader");
}
const auto& all_row_group_ranges = reader_->GetAllRowGroupRanges();
TargetRowGroups target_row_groups;
for (const auto& row_group : src_row_groups) {
int32_t row_group_idx = row_group.GetRowGroupIndex();
if (static_cast<size_t>(row_group_idx) >= all_row_group_ranges.size()) {
return Status::Invalid(
fmt::format("src row group {} not in row group meta", row_group_idx));
}
// half open interval [start_row_idx, end_row_idx)
const auto& [start_row_idx, end_row_idx] = all_row_group_ranges[row_group_idx];
if (!bitmap.ContainsAny(start_row_idx, end_row_idx)) {
continue;
}
target_row_groups.emplace_back(row_group);
}
return target_row_groups;
}
RowRanges ParquetFileBatchReader::CoalesceNearbyRanges(const RowRanges& input,
uint64_t hole_size_limit) {
if (input.IsEmpty()) {
return RowRanges();
}
const auto& ranges = input.GetRanges();
RowRanges result;
int64_t merge_start = ranges.front().from;
int64_t merge_end = ranges.front().to;
for (size_t i = 1; i < ranges.size(); ++i) {
// Gap between [merge_start, merge_end] and [ranges[i].from, ranges[i].to]
int64_t gap = ranges[i].from - merge_end - 1;
if (static_cast<uint64_t>(gap) > hole_size_limit) {
result.Add(RowRanges::Range(merge_start, merge_end));
merge_start = ranges[i].from;
}
merge_end = ranges[i].to;
}
result.Add(RowRanges::Range(merge_start, merge_end));
return result;
}
RowRanges ParquetFileBatchReader::BitmapToContiguousRanges(const RoaringBitmap32& bitmap,
uint64_t start_row, uint64_t end_row) {
RowRanges ranges;
if (bitmap.IsEmpty() || start_row >= end_row) {
return ranges;
}
auto it = bitmap.EqualOrLarger(static_cast<int32_t>(start_row));
const auto end = bitmap.End();
if (it == end || static_cast<uint64_t>(*it) >= end_row) {
return ranges;
}
auto run_start = static_cast<int64_t>(*it);
auto prev = run_start;
for (++it; it != end; ++it) {
auto current = static_cast<int64_t>(*it);
if (current >= static_cast<int64_t>(end_row)) {
break;
}
if (current != prev + 1) {
ranges.Add(RowRanges::Range(run_start - start_row, prev - start_row));
run_start = current;
}
prev = current;
}
ranges.Add(RowRanges::Range(run_start - start_row, prev - start_row));
return ranges;
}
Result<TargetRowGroups> ParquetFileBatchReader::RefineRowRangesByCoalescing(
const RoaringBitmap32& bitmap, const TargetRowGroups& src_row_groups) const {
PAIMON_ASSIGN_OR_RAISE(const uint64_t hole_size_limit,
OptionsUtils::GetValueFromMap<uint64_t>(
options_, PARQUET_READ_ROW_RANGES_COALESCE_HOLE_SIZE_LIMIT,
DEFAULT_PARQUET_READ_ROW_RANGES_COALESCE_HOLE_SIZE_LIMIT));
const auto& all_row_group_ranges = reader_->GetAllRowGroupRanges();
TargetRowGroups target_row_groups;
target_row_groups.reserve(src_row_groups.size());
for (const auto& row_group : src_row_groups) {
int32_t rg_index = row_group.GetRowGroupIndex();
uint64_t rg_start_row = all_row_group_ranges[rg_index].first;
uint64_t rg_end_row = all_row_group_ranges[rg_index].second;
// Step 1: bitmap -> contiguous ranges (relative to row group start).
// Step 2: coalesce ranges with small gaps to reduce range count.
RowRanges contiguous = BitmapToContiguousRanges(bitmap, rg_start_row, rg_end_row);
RowRanges coalesced = CoalesceNearbyRanges(contiguous, hole_size_limit);
auto rg_row_count = static_cast<int64_t>(rg_end_row - rg_start_row);
if (coalesced.IsEmpty()) {
continue;
}
if (coalesced.RowCount() == rg_row_count) {
target_row_groups.emplace_back(row_group);
} else {
target_row_groups.emplace_back(rg_index, true, std::move(coalesced));
}
}
return target_row_groups;
}
Result<TargetRowGroups> ParquetFileBatchReader::RefineRowRangesByTrimming(
const RoaringBitmap32& bitmap, const TargetRowGroups& src_row_groups,
const std::vector<int32_t>& column_indices) const {
auto page_index_reader = reader_->GetPageIndexReader();
if (!page_index_reader) {
return src_row_groups;
}
TargetRowGroups target_row_groups;
target_row_groups.reserve(src_row_groups.size());
for (const auto& row_group : src_row_groups) {
auto filtered = TrimRowGroupPageRanges(bitmap, row_group, column_indices);
if (!filtered.GetRowRanges().IsEmpty()) {
target_row_groups.emplace_back(std::move(filtered));
}
}
return target_row_groups;
}
TargetRowGroup ParquetFileBatchReader::TrimRowGroupPageRanges(
const RoaringBitmap32& bitmap, const TargetRowGroup& row_group,
const std::vector<int32_t>& column_indices) const {
int32_t row_group_idx = row_group.GetRowGroupIndex();
auto rg_page_index_reader = reader_->GetRowGroupPageIndexReader(row_group_idx);
if (!rg_page_index_reader) {
return row_group;
}
const auto& all_row_group_ranges = reader_->GetAllRowGroupRanges();
uint64_t rg_start_row = all_row_group_ranges[row_group_idx].first;
uint64_t rg_row_count = all_row_group_ranges[row_group_idx].second - rg_start_row;
RowRanges row_ranges = row_group.GetRowRanges();
for (int32_t col_index : column_indices) {
auto offset_index = rg_page_index_reader->GetOffsetIndex(col_index);
if (!offset_index) {
continue;
}
auto page_ranges = ComputeColumnPageRanges(bitmap, offset_index->page_locations(),
rg_start_row, rg_row_count);
row_ranges = RowRanges::Intersection(row_ranges, page_ranges);
}
if (row_ranges.RowCount() == static_cast<int64_t>(rg_row_count)) {
return row_group;
} else {
return TargetRowGroup(row_group_idx, true, std::move(row_ranges));
}
}
RowRanges ParquetFileBatchReader::ComputeColumnPageRanges(
const RoaringBitmap32& bitmap, const std::vector<::parquet::PageLocation>& page_locations,
uint64_t rg_start_row, uint64_t rg_row_count) {
RowRanges page_row_ranges;
for (size_t page_idx = 0; page_idx < page_locations.size(); ++page_idx) {
// half open interval [first_row, last_row)
auto first_row = page_locations[page_idx].first_row_index;
auto last_row = page_idx + 1 < page_locations.size()
? page_locations[page_idx + 1].first_row_index
: rg_row_count;
if (!bitmap.ContainsAny(rg_start_row + first_row, rg_start_row + last_row)) {
continue;
}
// closed interval [range_start_row, range_end_row]
auto range_start_row = bitmap.NextValue(rg_start_row + first_row);
auto range_end_row = bitmap.PreviousValue(rg_start_row + last_row);
if (!range_start_row.has_value() || !range_end_row.has_value()) {
continue;
}
page_row_ranges.Add(
Range(range_start_row.value() - rg_start_row, range_end_row.value() - rg_start_row));
}
return page_row_ranges;
}
// Uses page-level column index statistics to filter row groups and store per-row-group
// RowRanges for true page-level skipping. A row group is excluded if ALL its pages are
// determined to not match the predicate. For partially matched row groups, RowRanges
// are stored for page-level filtering during reading.
Result<TargetRowGroups> ParquetFileBatchReader::FilterRowGroupsByPageIndex(
const std::shared_ptr<Predicate>& predicate,
const std::map<std::string, int32_t>& column_name_to_index,
const TargetRowGroups& src_row_groups) const {
if (!predicate) {
return src_row_groups;
}
auto page_index_reader = reader_->GetPageIndexReader();
if (!page_index_reader) {
PAIMON_LOG_DEBUG(logger_,
"Page index not available in file, skipping page-level filtering (%s)",
PARQUET_WRITE_ENABLE_PAGE_INDEX);
return src_row_groups;
}
auto file_metadata = reader_->GetFileReader()->parquet_reader()->metadata();
TargetRowGroups target_row_groups;
for (const auto& row_group : src_row_groups) {
int32_t row_group_idx = row_group.GetRowGroupIndex();
auto result =
reader_->CalculateFilteredRowRanges(row_group_idx, predicate, column_name_to_index);
if (!result.ok()) {
target_row_groups.emplace_back(row_group);
continue;
}
const auto& row_ranges = result.value();
if (!row_ranges.IsEmpty()) {
int64_t rg_row_count = file_metadata->RowGroup(row_group_idx)->num_rows();
auto intersection = row_group.IsPartiallyMatched()
? RowRanges::Intersection(row_group.GetRowRanges(), row_ranges)
: row_ranges;
if (intersection.IsEmpty()) {
continue;
}
if (intersection.RowCount() < rg_row_count) {
target_row_groups.emplace_back(row_group_idx, true, intersection);
} else {
target_row_groups.emplace_back(row_group);
}
}
}
return target_row_groups;
}
Result<BatchReader::ReadBatch> ParquetFileBatchReader::NextBatch() {
try {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::RecordBatch> batch, reader_->Next());
if (batch == nullptr) {
row_mapping_.clear();
return BatchReader::MakeEofBatch();
}
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
batch->ToStructArray());
PAIMON_RETURN_NOT_OK_FROM_ARROW(array->Validate());
PAIMON_ASSIGN_OR_RAISE(bool need_cast, ParquetTimestampConverter::NeedCastArrayForTimestamp(
array->type(), read_data_type_));
if (need_cast) {
PAIMON_ASSIGN_OR_RAISE(array, ParquetTimestampConverter::CastArrayForTimestamp(
array, read_data_type_, arrow_pool_));
}
PAIMON_ASSIGN_OR_RAISE(need_cast, ParquetTimestampConverter::NeedCastArrayForTimestamp(
array->type(), read_data_type_));
if (need_cast) {
return Status::Invalid(fmt::format(
"unexpected: in parquet, after CastArrayForTimestamp, output type {} not "
"equal with read schema {}",
array->type()->ToString(), read_data_type_->ToString()));
}
PAIMON_RETURN_NOT_OK(GenerateRowMapping(array->length()));
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(*array, c_array.get(), c_schema.get()));
read_rows_ += array->length();
read_batch_count_++;
metrics_->SetCounter(ParquetMetrics::READ_ROWS, read_rows_);
metrics_->SetCounter(ParquetMetrics::READ_BATCH_COUNT, read_batch_count_);
return make_pair(std::move(c_array), std::move(c_schema));
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("ParquetFileBatchReader::NextBatch")
}
Result<std::vector<std::pair<uint64_t, uint64_t>>> ParquetFileBatchReader::GenReadRanges(
bool* need_prefetch) const {
try {
*need_prefetch = true;
return reader_->GetAllRowGroupRanges();
}
PAIMON_PARQUET_CATCH_AND_RETURN_STATUS("ParquetFileBatchReader::GenReadRanges")
}
Result<::parquet::ReaderProperties> ParquetFileBatchReader::CreateReaderProperties(
const std::shared_ptr<arrow::MemoryPool>& pool,
const std::map<std::string, std::string>& options) {
::parquet::ReaderProperties reader_properties;
// TODO(jinli.zjw): set more ReaderProperties (compare with java)
PAIMON_ASSIGN_OR_RAISE(
bool enable_pre_buffer,
OptionsUtils::GetValueFromMap<bool>(options, PARQUET_READ_ENABLE_PRE_BUFFER, true));
if (enable_pre_buffer) {
reader_properties.enable_buffered_stream();
} else {
reader_properties.disable_buffered_stream();
}
return reader_properties;
}
Result<::parquet::ArrowReaderProperties> ParquetFileBatchReader::CreateArrowReaderProperties(
const std::shared_ptr<arrow::MemoryPool>& pool,
const std::map<std::string, std::string>& options, int32_t batch_size) {
PAIMON_ASSIGN_OR_RAISE(
uint32_t executor_thread_count,
OptionsUtils::GetValueFromMap<uint32_t>(options, PARQUET_READ_EXECUTOR_THREAD_COUNT,
DEFAULT_PARQUET_READ_EXECUTOR_THREAD_COUNT));
::parquet::ArrowReaderProperties arrow_reader_props;
// TODO(jinli.zjw): set more ArrowReaderProperties (compare with java)
PAIMON_ASSIGN_OR_RAISE(
bool enable_pre_buffer,
OptionsUtils::GetValueFromMap<bool>(options, PARQUET_READ_ENABLE_PRE_BUFFER, true));
arrow_reader_props.set_pre_buffer(enable_pre_buffer);
arrow_reader_props.set_batch_size(static_cast<int64_t>(batch_size));
if (executor_thread_count != 0) {
PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::SetCpuThreadPoolCapacity(executor_thread_count));
arrow_reader_props.set_use_threads(true);
} else {
arrow_reader_props.set_use_threads(false);
}
PAIMON_ASSIGN_OR_RAISE(bool cache_lazy, OptionsUtils::GetValueFromMap<bool>(
options, PARQUET_READ_CACHE_OPTION_LAZY, false));
PAIMON_ASSIGN_OR_RAISE(
int64_t cache_prefetch_limit,
OptionsUtils::GetValueFromMap<int64_t>(options, PARQUET_READ_CACHE_OPTION_PREFETCH_LIMIT,
DEFAULT_PARQUET_READ_CACHE_OPTION_PREFETCH_LIMIT));
PAIMON_ASSIGN_OR_RAISE(
int64_t cache_hole_size_limit,
OptionsUtils::GetValueFromMap<int64_t>(options, PARQUET_READ_CACHE_OPTION_HOLE_SIZE_LIMIT,
DEFAULT_PARQUET_READ_CACHE_OPTION_HOLE_SIZE_LIMIT));
PAIMON_ASSIGN_OR_RAISE(
int64_t cache_range_size_limit,
OptionsUtils::GetValueFromMap<int64_t>(options, PARQUET_READ_CACHE_OPTION_RANGE_SIZE_LIMIT,
DEFAULT_PARQUET_READ_CACHE_OPTION_RANGE_SIZE_LIMIT));
if (cache_hole_size_limit < 0) {
return Status::Invalid(fmt::format("{} must be non-negative, but was {}",
PARQUET_READ_CACHE_OPTION_HOLE_SIZE_LIMIT,
cache_hole_size_limit));
}
if (cache_range_size_limit <= cache_hole_size_limit) {
return Status::Invalid(fmt::format("{} must be greater than {}, but was {} <= {}",
PARQUET_READ_CACHE_OPTION_RANGE_SIZE_LIMIT,
PARQUET_READ_CACHE_OPTION_HOLE_SIZE_LIMIT,
cache_range_size_limit, cache_hole_size_limit));
}
auto cache_option = arrow::io::CacheOptions::Defaults();
cache_option.lazy = cache_lazy;
cache_option.prefetch_limit = cache_prefetch_limit;
cache_option.hole_size_limit = cache_hole_size_limit;
cache_option.range_size_limit = cache_range_size_limit;
arrow_reader_props.set_cache_options(cache_option);
return arrow_reader_props;
}
// Nested column index computation
Status ParquetFileBatchReader::CollectLeafIndices(const std::shared_ptr<arrow::DataType>& read_type,
const std::shared_ptr<arrow::DataType>& file_type,
int32_t* leaf_index,
std::vector<int32_t>* indices) {
if (file_type->id() == arrow::Type::STRUCT) {
for (const auto& file_child : file_type->fields()) {
std::shared_ptr<arrow::Field> read_child = nullptr;
for (const auto& candidate : read_type->fields()) {
if (candidate->name() == file_child->name()) {
read_child = candidate;
break;
}
}
if (read_child) {
PAIMON_RETURN_NOT_OK(CollectLeafIndices(read_child->type(), file_child->type(),
leaf_index, indices));
} else {
SkipLeafIndices(file_child->type(), leaf_index);
}
}
} else if (file_type->id() == arrow::Type::LIST) {
// Keep behavior aligned with ORC path: list/map inner partial projection
// is currently unsupported and should fail-fast.
if (!HasSameNestedProjectionShape(read_type, file_type)) {
return Status::Invalid(fmt::format(
"Parquet does not support partial projection inside list/map: src {} vs target {}",
file_type->ToString(), read_type->ToString()));
}
const auto& read_list = static_cast<const arrow::ListType&>(*read_type);
const auto& file_list = static_cast<const arrow::ListType&>(*file_type);
PAIMON_RETURN_NOT_OK(CollectLeafIndices(read_list.value_type(), file_list.value_type(),
leaf_index, indices));
} else if (file_type->id() == arrow::Type::MAP) {
if (!HasSameNestedProjectionShape(read_type, file_type)) {
return Status::Invalid(fmt::format(
"Parquet does not support partial projection inside list/map: src {} vs target {}",
file_type->ToString(), read_type->ToString()));
}
const auto& read_map = static_cast<const arrow::MapType&>(*read_type);
const auto& file_map = static_cast<const arrow::MapType&>(*file_type);
PAIMON_RETURN_NOT_OK(
CollectLeafIndices(read_map.key_type(), file_map.key_type(), leaf_index, indices));
PAIMON_RETURN_NOT_OK(
CollectLeafIndices(read_map.item_type(), file_map.item_type(), leaf_index, indices));
} else {
// Leaf column — collect its index.
indices->push_back((*leaf_index)++);
}
return Status::OK();
}
void ParquetFileBatchReader::SkipLeafIndices(const std::shared_ptr<arrow::DataType>& file_type,
int32_t* leaf_index) {
if (file_type->id() == arrow::Type::STRUCT || file_type->id() == arrow::Type::LIST ||
file_type->id() == arrow::Type::MAP) {
for (int32_t i = 0; i < file_type->num_fields(); i++) {
SkipLeafIndices(file_type->field(i)->type(), leaf_index);
}
} else {
(*leaf_index)++;
}
}
Result<std::vector<int32_t>> ParquetFileBatchReader::ComputeNestedColumnIndices(
const std::shared_ptr<arrow::Schema>& read_schema,
const std::shared_ptr<arrow::Schema>& file_schema) {
std::vector<int32_t> indices;
std::vector<int32_t> file_field_leaf_starts;
file_field_leaf_starts.reserve(file_schema->num_fields());
int32_t file_leaf_index = 0;
for (const auto& file_field : file_schema->fields()) {
file_field_leaf_starts.push_back(file_leaf_index);
SkipLeafIndices(file_field->type(), &file_leaf_index);
}
const auto& file_fields = file_schema->fields();
for (const auto& read_field : read_schema->fields()) {
int32_t file_field_idx = -1;
for (int32_t i = 0; i < static_cast<int32_t>(file_fields.size()); ++i) {
if (file_fields[i]->name() == read_field->name()) {
file_field_idx = i;
break;
}
}
if (file_field_idx < 0) {
return Status::Invalid(
fmt::format("Field '{}' in read schema does not exist in parquet file schema",
read_field->name()));
}
int32_t leaf_index = file_field_leaf_starts[file_field_idx];
PAIMON_RETURN_NOT_OK(CollectLeafIndices(
read_field->type(), file_fields[file_field_idx]->type(), &leaf_index, &indices));
}
return indices;
}
Status ParquetFileBatchReader::UpdateAllTargetRowRanges(
const std::vector<TargetRowGroup>& target_row_groups) {
row_mapping_.clear();
auto all_row_group_ranges = reader_->GetAllRowGroupRanges();
RowRanges all_ranges;
for (const auto& target_row_group : target_row_groups) {
auto row_group_idx = target_row_group.GetRowGroupIndex();
for (const auto& range : target_row_group.GetRowRanges().GetRanges()) {
all_ranges.Add(Range(all_row_group_ranges[row_group_idx].first + range.from,
all_row_group_ranges[row_group_idx].first + range.to));
}
}
all_row_ranges_ = std::move(all_ranges);
return Status::OK();
}
Status ParquetFileBatchReader::GenerateRowMapping(int64_t batch_length) {
const std::vector<Range>& all_ranges = all_row_ranges_.GetRanges();
PAIMON_ASSIGN_OR_RAISE(int64_t batch_start_row, reader_->GetPreviousBatchFirstRowNumber());
auto cur_range_it =
std::upper_bound(all_ranges.begin(), all_ranges.end(), batch_start_row,
[](int64_t value, const Range& r) { return value < r.from; });
if (cur_range_it == all_ranges.begin()) {
return Status::Invalid("No range found!");
}
--cur_range_it;
if (batch_start_row < cur_range_it->from || batch_start_row > cur_range_it->to) {
return Status::Invalid(
fmt::format("Batch start row {} is not in the current range [{}, {}]!", batch_start_row,
cur_range_it->from, cur_range_it->to));
}
std::vector<uint64_t> row_mapping;
row_mapping.reserve(batch_length);
int64_t global_row = batch_start_row;
for (int64_t i = 0; i < batch_length; ++i) {
if (global_row > cur_range_it->to) {
++cur_range_it;
if (cur_range_it == all_ranges.end()) {
return Status::Invalid("Batch length exceeds the total row ranges!");
}
global_row = cur_range_it->from;
}
row_mapping.push_back(global_row);
global_row++;
}
row_mapping_ = std::move(row_mapping);
return Status::OK();
}
} // namespace paimon::parquet