Skip to content

Commit 75544f7

Browse files
authored
feat(parquet): skip page headers of unselected pages via OffsetIndex-based direct read plan (#167)
* fix(parquet): skip reading page header for unneeded pages * style: clang tidy * fix
1 parent 4ad64a9 commit 75544f7

4 files changed

Lines changed: 940 additions & 72 deletions

File tree

‎cmake_modules/arrow.diff‎

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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(&current_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(&current_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

Comments
 (0)