@@ -741,3 +741,212 @@ diff --git a/cpp/src/arrow/io/interfaces.h b/cpp/src/arrow/io/interfaces.h
741741 Status TransferColumnData(::parquet::internal::RecordReader* reader,
742742 const std::shared_ptr<::arrow::Field>& value_field,
743743 const ColumnDescriptor* descr, ::arrow::MemoryPool* pool,
744+ diff --git a/cpp/src/parquet/column_reader.h b/cpp/src/parquet/column_reader.h
745+ --- a/cpp/src/parquet/column_reader.h
746+ +++ b/cpp/src/parquet/column_reader.h
747+ @@ -76,6 +76,18 @@ struct PARQUET_EXPORT DataPageStats {
748+ std::optional<int32_t> num_rows;
749+ };
750+
751+ + /// \brief Identifies a data page that PageReader should read directly.
752+ + ///
753+ + /// The offset is relative to the beginning of the column chunk stream passed to
754+ + /// PageReader::Open. The compressed size includes both the serialized page header
755+ + /// and the compressed page body. The ordinal is the original data page ordinal in
756+ + /// the column chunk and does not include the dictionary page.
757+ + struct PARQUET_EXPORT DataPageReadPlanEntry {
758+ + int32_t page_ordinal;
759+ + int64_t offset;
760+ + int32_t compressed_page_size;
761+ + };
762+ +
763+ class PARQUET_EXPORT LevelDecoder {
764+ public:
765+ LevelDecoder();
766+ @@ -147,9 +159,21 @@ class PARQUET_EXPORT PageReader {
767+ // ApplicationVersion::HasCorrectStatistics().
768+ // \note API EXPERIMENTAL
769+ void set_data_page_filter(DataPageFilter data_page_filter) {
770+ + if (data_page_read_plan_enabled_) {
771+ + throw ParquetException(
772+ + "data_page_filter and data_page_read_plan cannot be enabled together");
773+ + }
774+ data_page_filter_ = std::move(data_page_filter);
775+ }
776+
777+ + /// Configure PageReader to jump directly to selected data pages before reading
778+ + /// their headers. `first_data_page_offset` and each entry offset are relative to
779+ + /// the beginning of the column chunk stream. Dictionary pages before
780+ + /// `first_data_page_offset` are still read normally.
781+ + // \note API EXPERIMENTAL
782+ + void set_data_page_read_plan(int64_t first_data_page_offset,
783+ + std::vector<DataPageReadPlanEntry> data_pages);
784+ +
785+ // @returns: shared_ptr<Page>(nullptr) on EOS, std::shared_ptr<Page>
786+ // containing new Page otherwise
787+ //
788+ @@ -162,6 +186,11 @@ class PARQUET_EXPORT PageReader {
789+ protected:
790+ // Callback that decides if we should skip a page or not.
791+ DataPageFilter data_page_filter_;
792+ +
793+ + bool data_page_read_plan_enabled_ = false;
794+ + int64_t first_data_page_offset_ = 0;
795+ + std::vector<DataPageReadPlanEntry> data_page_read_plan_;
796+ + size_t next_data_page_ = 0;
797+ };
798+
799+ class PARQUET_EXPORT ColumnReader {
800+ diff --git a/cpp/src/parquet/column_reader.cc b/cpp/src/parquet/column_reader.cc
801+ --- a/cpp/src/parquet/column_reader.cc
802+ +++ b/cpp/src/parquet/column_reader.cc
803+ @@ -207,6 +207,39 @@ ReaderProperties default_reader_properties() {
804+ return default_reader_properties;
805+ }
806+
807+ + void PageReader::set_data_page_read_plan(
808+ + int64_t first_data_page_offset,
809+ + std::vector<DataPageReadPlanEntry> data_pages) {
810+ + if (data_page_filter_) {
811+ + throw ParquetException(
812+ + "data_page_filter and data_page_read_plan cannot be enabled together");
813+ + }
814+ + if (first_data_page_offset < 0) {
815+ + throw ParquetException("Invalid negative first data page offset");
816+ + }
817+ +
818+ + int64_t previous_end = first_data_page_offset;
819+ + int32_t previous_ordinal = -1;
820+ + for (const auto& page : data_pages) {
821+ + int64_t page_end;
822+ + if (page.page_ordinal < 0 || page.offset < first_data_page_offset ||
823+ + page.compressed_page_size <= 0 ||
824+ + AddWithOverflow(page.offset, page.compressed_page_size, &page_end)) {
825+ + throw ParquetException("Invalid data page read plan entry");
826+ + }
827+ + if (page.offset < previous_end || page.page_ordinal <= previous_ordinal) {
828+ + throw ParquetException("Data page read plan entries must be ordered");
829+ + }
830+ + previous_end = page_end;
831+ + previous_ordinal = page.page_ordinal;
832+ + }
833+ +
834+ + data_page_read_plan_enabled_ = true;
835+ + first_data_page_offset_ = first_data_page_offset;
836+ + data_page_read_plan_ = std::move(data_pages);
837+ + next_data_page_ = 0;
838+ + }
839+ +
840+ namespace {
841+
842+ // Extracts encoded statistics from V1 and V2 data page headers
843+ @@ -430,9 +463,43 @@ std::shared_ptr<Page> SerializedPageReader::NextPage() {
844+
845+ // Loop here because there may be unhandled page types that we skip until
846+ // finding a page that we do know what to do with
847+ - while (seen_num_values_ < total_num_values_) {
848+ + while (data_page_read_plan_enabled_ || seen_num_values_ < total_num_values_) {
849+ + const DataPageReadPlanEntry* planned_data_page = nullptr;
850+ + uint32_t page_header_limit = max_page_header_size_;
851+ +
852+ + if (data_page_read_plan_enabled_) {
853+ + if (next_data_page_ >= data_page_read_plan_.size()) {
854+ + return nullptr;
855+ + }
856+ +
857+ + PARQUET_ASSIGN_OR_THROW(int64_t current_position, stream_->Tell());
858+ + if (current_position < first_data_page_offset_) {
859+ + page_header_limit = static_cast<uint32_t>(std::min<int64_t>(
860+ + page_header_limit, first_data_page_offset_ - current_position));
861+ + } else {
862+ + planned_data_page = &data_page_read_plan_[next_data_page_];
863+ + if (current_position > planned_data_page->offset) {
864+ + throw ParquetException("Data page read plan points behind stream position");
865+ + }
866+ + PARQUET_THROW_NOT_OK(
867+ + stream_->Advance(planned_data_page->offset - current_position));
868+ + PARQUET_ASSIGN_OR_THROW(int64_t target_position, stream_->Tell());
869+ + if (target_position != planned_data_page->offset) {
870+ + throw ParquetException("Failed to seek to planned data page");
871+ + }
872+ + page_ordinal_ = planned_data_page->page_ordinal;
873+ + page_header_limit = static_cast<uint32_t>(std::min<int64_t>(
874+ + page_header_limit, planned_data_page->compressed_page_size));
875+ + }
876+ + }
877+ +
878+ + if (page_header_limit == 0) {
879+ + throw ParquetException("No bytes available for page header");
880+ + }
881+ +
882+ uint32_t header_size = 0;
883+ - uint32_t allowed_page_size = kDefaultPageHeaderSize;
884+ + uint32_t allowed_page_size =
885+ + std::min<uint32_t>(kDefaultPageHeaderSize, page_header_limit);
886+
887+ // Page headers can be very large because of page statistics
888+ // We try to deserialize a larger buffer progressively
889+ @@ -458,11 +525,12 @@ std::shared_ptr<Page> SerializedPageReader::NextPage() {
890+ // Failed to deserialize. Double the allowed page header size and try again
891+ std::stringstream ss;
892+ ss << e.what();
893+ - allowed_page_size *= 2;
894+ - if (allowed_page_size > max_page_header_size_) {
895+ + if (allowed_page_size >= page_header_limit) {
896+ ss << "Deserializing page header failed.\n";
897+ throw ParquetException(ss.str());
898+ }
899+ + allowed_page_size =
900+ + std::min<uint32_t>(allowed_page_size * 2, page_header_limit);
901+ }
902+ }
903+ // Advance the stream offset
904+ @@ -474,6 +542,20 @@ std::shared_ptr<Page> SerializedPageReader::NextPage() {
905+ throw ParquetException("Invalid page header");
906+ }
907+
908+ + const PageType::type page_type = LoadEnumSafe(¤t_page_header_.type);
909+ + if (planned_data_page != nullptr) {
910+ + if (page_type != PageType::DATA_PAGE && page_type != PageType::DATA_PAGE_V2) {
911+ + throw ParquetException("Data page read plan points to a non-data page");
912+ + }
913+ + int64_t total_compressed_size;
914+ + if (AddWithOverflow(static_cast<int64_t>(header_size),
915+ + static_cast<int64_t>(compressed_len),
916+ + &total_compressed_size) ||
917+ + total_compressed_size != planned_data_page->compressed_page_size) {
918+ + throw ParquetException("Planned data page size does not match page header");
919+ + }
920+ + }
921+ +
922+ EncodedStatistics data_page_statistics;
923+ if (ShouldSkipPage(&data_page_statistics)) {
924+ PARQUET_THROW_NOT_OK(stream_->Advance(compressed_len));
925+ @@ -494,8 +576,6 @@ std::shared_ptr<Page> SerializedPageReader::NextPage() {
926+ ParquetException::EofException(ss.str());
927+ }
928+
929+ - const PageType::type page_type = LoadEnumSafe(¤t_page_header_.type);
930+ -
931+ if (properties_.page_checksum_verification() && current_page_header_.__isset.crc &&
932+ PageCanUseChecksum(page_type)) {
933+ // verify crc
934+ @@ -534,6 +614,9 @@ std::shared_ptr<Page> SerializedPageReader::NextPage() {
935+ LoadEnumSafe(&dict_header.encoding),
936+ is_sorted);
937+ } else if (page_type == PageType::DATA_PAGE) {
938+ + if (planned_data_page != nullptr) {
939+ + ++next_data_page_;
940+ + }
941+ ++page_ordinal_;
942+ const format::DataPageHeader& header = current_page_header_.data_page_header;
943+ page_buffer =
944+ @@ -545,6 +628,9 @@ std::shared_ptr<Page> SerializedPageReader::NextPage() {
945+ LoadEnumSafe(&header.repetition_level_encoding), uncompressed_len,
946+ std::move(data_page_statistics));
947+ } else if (page_type == PageType::DATA_PAGE_V2) {
948+ + if (planned_data_page != nullptr) {
949+ + ++next_data_page_;
950+ + }
951+ ++page_ordinal_;
952+ const format::DataPageHeaderV2& header = current_page_header_.data_page_header_v2;
0 commit comments