2020#include " iceberg/parquet/parquet_reader.h"
2121
2222#include < algorithm>
23+ #include < limits>
2324#include < numeric>
25+ #include < utility>
2426#include < variant>
2527#include < vector>
2628
@@ -276,7 +278,6 @@ class ParquetReader::Impl {
276278 return InvalidArgument (" Projected schema is required by Parquet reader" );
277279 }
278280
279- split_ = options.split ;
280281 read_schema_ = options.projection ;
281282
282283 // Prepare reader properties
@@ -297,6 +298,7 @@ class ParquetReader::Impl {
297298 ICEBERG_ARROW_ASSIGN_OR_RETURN (
298299 reader_, ::parquet::arrow::FileReader::Make (pool_, std::move (file_reader),
299300 arrow_reader_properties));
301+ ICEBERG_RETURN_UNEXPECTED (PlanRowGroups (options));
300302
301303 // Project read schema onto the Parquet file schema
302304 ICEBERG_ASSIGN_OR_RAISE (projection_, BuildProjection (reader_.get (), *read_schema_));
@@ -388,45 +390,70 @@ class ParquetReader::Impl {
388390 }
389391
390392 private:
391- Status InitReadContext () {
392- context_ = std::make_unique<ReadContext>();
393-
394- // Row group pruning based on the split
393+ Status PlanRowGroups (const ReaderOptions& options) {
395394 // TODO(gangwu): add row group filtering based on zone map, bloom filter, etc.
396395 auto metadata = reader_->parquet_reader ()->metadata ();
397- std::vector<int > row_group_indices;
398- if (split_.has_value ()) {
396+ if (options.split .has_value ()) {
397+ const auto & split = *options.split ;
398+ ICEBERG_ARROW_ASSIGN_OR_RETURN (auto file_size, input_stream_->GetSize ());
399+ int64_t row_count = 0 ;
399400 for (int i = 0 ; i < metadata->num_row_groups (); ++i) {
400- auto row_group_offset = metadata->RowGroup (i)->file_offset ();
401- if (row_group_offset >= split_->offset &&
402- row_group_offset < split_->offset + split_->length ) {
403- row_group_indices.push_back (i);
404- } else if (row_group_offset >= split_->offset + split_->length ) {
405- break ;
401+ auto row_group = metadata->RowGroup (i);
402+ // Empty row groups have no pages to assign to a split.
403+ if (row_group->num_rows () == 0 ) {
404+ continue ;
405+ }
406+ // Like Java Iceberg, a split reads the row groups whose midpoints it contains.
407+ auto midpoint = RowGroupMidpoint (*row_group, file_size);
408+ if (!midpoint.has_value ()) {
409+ return Invalid (
410+ " Cannot read Parquet split {} (start={}, length={}), row "
411+ " group {}: {}" ,
412+ options.path , split.offset , split.length , i, midpoint.error ().message );
413+ }
414+ // Subtraction avoids overflow for a caller-supplied unbounded split length.
415+ if (std::cmp_greater_equal (*midpoint, split.offset ) &&
416+ static_cast <uint64_t >(*midpoint) - split.offset < split.length ) {
417+ row_group_indices_.push_back (i);
418+ }
419+ if (row_group->num_rows () < 0 ||
420+ row_group->num_rows () > std::numeric_limits<int64_t >::max () - row_count) {
421+ return Invalid (" Invalid Parquet row count in file {}, row group {}" ,
422+ options.path , i);
406423 }
424+ row_count += row_group->num_rows ();
407425 }
408- } else {
409- row_group_indices .resize (metadata->num_row_groups ());
410- std::iota (row_group_indices .begin (), row_group_indices .end (), 0 ); // NOLINT
426+ } else if (metadata-> num_row_groups () > 0 ) {
427+ row_group_indices_ .resize (metadata->num_row_groups ());
428+ std::iota (row_group_indices_ .begin (), row_group_indices_ .end (), 0 ); // NOLINT
411429 }
430+ return {};
431+ }
412432
433+ Status InitReadContext () {
434+ // Build locally so initialization can be retried without consuming any rows.
435+ auto context = std::make_unique<ReadContext>();
413436 // Create the record batch reader, including for empty splits to preserve the schema.
414- auto column_indices = SelectedColumnIndices (projection_);
415437 ICEBERG_ARROW_ASSIGN_OR_RETURN (
416- context_->record_batch_reader_ ,
417- reader_->GetRecordBatchReader (row_group_indices, column_indices));
438+ context->record_batch_reader_ ,
439+ reader_->GetRecordBatchReader (row_group_indices_,
440+ SelectedColumnIndices (projection_)));
418441
419442 if (NeedsRowPositions (*read_schema_, metadata_context_)) {
420- ICEBERG_ASSIGN_OR_RAISE (context_->row_number_reader_ ,
421- RowNumberReader::Make (*metadata, row_group_indices, pool_));
443+ auto metadata = reader_->parquet_reader ()->metadata ();
444+ // Share the batch reader's selection so positions remain file-relative even
445+ // when a batch spans nonconsecutive row groups.
446+ ICEBERG_ASSIGN_OR_RAISE (
447+ context->row_number_reader_ ,
448+ RowNumberReader::Make (*metadata, row_group_indices_, pool_));
422449 }
423450
424451 // Build the output Arrow schema from the projected Iceberg schema. This schema is the
425452 // target of ProjectRecordBatch, so it must describe the projected schema rather than
426453 // the schema of the file.
427454 ArrowSchema arrow_schema;
428455 ICEBERG_RETURN_UNEXPECTED (ToArrowSchema (*read_schema_, &arrow_schema));
429- ICEBERG_ARROW_ASSIGN_OR_RETURN (context_ ->output_arrow_schema_ ,
456+ ICEBERG_ARROW_ASSIGN_OR_RETURN (context ->output_arrow_schema_ ,
430457 ::arrow::ImportSchema (&arrow_schema));
431458
432459 // Align the output schema with the arrays the reader actually produces. The reader's
@@ -437,18 +464,18 @@ class ParquetReader::Impl {
437464 // 3. Mixed list and large_list types in files with stored schemas
438465 // For each projected field, we use the reader's actual type. For missing fields
439466 // (columns not in the file), we apply the configured use_large_list preference.
440- context_ ->output_arrow_schema_ = AlignOutputSchemaToReaderSchema (
441- context_ ->output_arrow_schema_ , context_ ->record_batch_reader_ ->schema (),
467+ context ->output_arrow_schema_ = AlignOutputSchemaToReaderSchema (
468+ context ->output_arrow_schema_ , context ->record_batch_reader_ ->schema (),
442469 projection_, use_large_list_);
443-
470+ context_ = std::move (context);
444471 return {};
445472 }
446473
447474 private:
448475 // TODO(gangwu): make memory pool configurable
449476 ::arrow::MemoryPool* pool_ = ::arrow::default_memory_pool();
450- // The split to read from the Parquet file .
451- std::optional<Split> split_ ;
477+ // Row groups in footer order, shared by the batch and row-number readers .
478+ std::vector< int > row_group_indices_ ;
452479 // Whether to read list columns as large_list (64-bit offsets).
453480 bool use_large_list_ = false ;
454481 // Schema to read from the Parquet file.
0 commit comments