| /* |
| * 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 <map> |
| #include <string> |
| |
| #include "arrow/api.h" |
| #include "arrow/array/builder_binary.h" |
| #include "arrow/array/builder_nested.h" |
| #include "arrow/array/builder_primitive.h" |
| #include "arrow/c/abi.h" |
| #include "arrow/c/bridge.h" |
| #include "arrow/io/caching.h" |
| #include "arrow/memory_pool.h" |
| #include "gtest/gtest.h" |
| #include "paimon/common/utils/arrow/arrow_input_stream_adapter.h" |
| #include "paimon/common/utils/arrow/mem_utils.h" |
| #include "paimon/common/utils/arrow/status_utils.h" |
| #include "paimon/common/utils/checked_cast.h" |
| #include "paimon/common/utils/path_util.h" |
| #include "paimon/format/parquet/parquet_field_id_converter.h" |
| #include "paimon/format/parquet/parquet_format_defs.h" |
| #include "paimon/format/parquet/parquet_format_writer.h" |
| #include "paimon/fs/file_system.h" |
| #include "paimon/fs/local/local_file_system.h" |
| #include "paimon/memory/memory_pool.h" |
| #include "paimon/record_batch.h" |
| #include "paimon/testing/utils/testharness.h" |
| #include "parquet/arrow/reader.h" |
| #include "parquet/properties.h" |
| |
| namespace arrow { |
| class Array; |
| } // namespace arrow |
| |
| namespace paimon::parquet::test { |
| |
| class FileReaderWrapperTest : public ::testing::Test { |
| public: |
| void SetUp() override { |
| dir_ = paimon::test::UniqueTestDirectory::Create(); |
| ASSERT_TRUE(dir_); |
| fs_ = std::make_shared<LocalFileSystem>(); |
| pool_ = GetDefaultPool(); |
| arrow_pool_ = GetArrowPool(pool_); |
| batch_size_ = 512; |
| } |
| void TearDown() override {} |
| |
| std::pair<std::shared_ptr<arrow::Schema>, std::shared_ptr<arrow::DataType>> PrepareArrowSchema() |
| const { |
| auto string_field = arrow::field( |
| "col1", arrow::utf8(), |
| arrow::KeyValueMetadata::Make({ParquetFieldIdConverter::PARQUET_FIELD_ID}, {"0"})); |
| auto int_field = arrow::field( |
| "col2", arrow::int32(), |
| arrow::KeyValueMetadata::Make({ParquetFieldIdConverter::PARQUET_FIELD_ID}, {"1"})); |
| auto bool_field = arrow::field( |
| "col3", arrow::boolean(), |
| arrow::KeyValueMetadata::Make({ParquetFieldIdConverter::PARQUET_FIELD_ID}, {"2"})); |
| auto struct_type = arrow::struct_({string_field, int_field, bool_field}); |
| return std::make_pair( |
| arrow::schema(arrow::FieldVector({string_field, int_field, bool_field})), struct_type); |
| } |
| |
| std::shared_ptr<arrow::Array> PrepareArray(const std::shared_ptr<arrow::DataType>& data_type, |
| int32_t record_batch_size, |
| int32_t offset = 0) const { |
| arrow::StructBuilder struct_builder( |
| data_type, arrow::default_memory_pool(), |
| {std::make_shared<arrow::StringBuilder>(), std::make_shared<arrow::Int32Builder>(), |
| std::make_shared<arrow::BooleanBuilder>()}); |
| auto string_builder = checked_cast<arrow::StringBuilder*>(struct_builder.field_builder(0)); |
| auto int_builder = checked_cast<arrow::Int32Builder*>(struct_builder.field_builder(1)); |
| auto bool_builder = checked_cast<arrow::BooleanBuilder*>(struct_builder.field_builder(2)); |
| for (int32_t i = 0 + offset; i < record_batch_size + offset; ++i) { |
| EXPECT_TRUE(struct_builder.Append().ok()); |
| EXPECT_TRUE(string_builder->Append("str_" + std::to_string(i)).ok()); |
| if (i % 3 == 0) { |
| // test null |
| EXPECT_TRUE(int_builder->AppendNull().ok()); |
| } else { |
| EXPECT_TRUE(int_builder->Append(i).ok()); |
| } |
| EXPECT_TRUE(bool_builder->Append(static_cast<bool>(i % 2)).ok()); |
| } |
| std::shared_ptr<arrow::Array> array; |
| EXPECT_TRUE(struct_builder.Finish(&array).ok()); |
| return array; |
| } |
| |
| void AddRecordBatchOnce(const std::shared_ptr<ParquetFormatWriter>& format_writer, |
| const std::shared_ptr<arrow::DataType>& struct_type, |
| int32_t record_batch_size, int32_t offset) const { |
| auto array = PrepareArray(struct_type, record_batch_size, offset); |
| auto arrow_array = std::make_unique<ArrowArray>(); |
| ASSERT_TRUE(arrow::ExportArray(*array, arrow_array.get()).ok()); |
| auto batch = std::make_shared<RecordBatch>( |
| /*partition=*/std::map<std::string, std::string>(), /*bucket=*/-1, |
| /*row_kinds=*/std::vector<RecordBatch::RowKind>(), arrow_array.get()); |
| ASSERT_OK(format_writer->AddBatch(batch->GetData())); |
| } |
| |
| Result<std::unique_ptr<FileReaderWrapper>> PrepareReaderWrapper( |
| const std::string& file_path, int64_t wrapper_batch_size = 0) { |
| PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<InputStream> in, fs_->Open(file_path)); |
| PAIMON_ASSIGN_OR_RAISE(int64_t file_length, in->Length()); |
| auto input_stream = std::make_unique<ArrowInputStreamAdapter>(in, file_length, arrow_pool_); |
| ::parquet::arrow::FileReaderBuilder file_reader_builder; |
| ::parquet::ReaderProperties reader_properties; |
| reader_properties.enable_buffered_stream(); |
| PAIMON_RETURN_NOT_OK_FROM_ARROW( |
| file_reader_builder.Open(std::move(input_stream), reader_properties)); |
| |
| ::parquet::ArrowReaderProperties arrow_reader_props; |
| arrow_reader_props.set_pre_buffer(true); |
| arrow_reader_props.set_batch_size(static_cast<int64_t>(batch_size_)); |
| arrow_reader_props.set_use_threads(true); |
| arrow_reader_props.set_cache_options(arrow::io::CacheOptions::Defaults()); |
| std::unique_ptr<::parquet::arrow::FileReader> file_reader; |
| PAIMON_RETURN_NOT_OK_FROM_ARROW(file_reader_builder.memory_pool(arrow_pool_.get()) |
| ->properties(arrow_reader_props) |
| ->Build(&file_reader)); |
| return FileReaderWrapper::Create(std::move(file_reader), wrapper_batch_size, arrow_pool_); |
| } |
| |
| void PrepareParquetFile(const std::string& file_path, int32_t row_count, |
| bool enable_page_index = false, int32_t write_batch_size = 10) { |
| auto schema_pair = PrepareArrowSchema(); |
| const auto& arrow_schema = schema_pair.first; |
| const auto& struct_type = schema_pair.second; |
| |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<OutputStream> out, |
| fs_->Create(file_path, /*overwrite=*/false)); |
| ::parquet::WriterProperties::Builder builder; |
| builder.write_batch_size(write_batch_size); |
| builder.max_row_group_length(1000); |
| builder.enable_store_decimal_as_integer(); |
| if (enable_page_index) { |
| builder.enable_write_page_index(); |
| builder.disable_dictionary(); |
| builder.data_pagesize(1); |
| } |
| auto writer_properties = builder.build(); |
| ASSERT_OK_AND_ASSIGN( |
| std::shared_ptr<ParquetFormatWriter> format_writer, |
| ParquetFormatWriter::Create(out, arrow_schema, writer_properties, |
| DEFAULT_PARQUET_WRITER_MAX_MEMORY_USE, arrow_pool_)); |
| |
| AddRecordBatchOnce(format_writer, struct_type, /*record_batch_size=*/row_count, |
| /*offset=*/0); |
| ASSERT_OK(format_writer->Flush()); |
| ASSERT_OK(format_writer->Finish()); |
| ASSERT_OK(out->Flush()); |
| ASSERT_OK(out->Close()); |
| } |
| |
| private: |
| std::unique_ptr<paimon::test::UniqueTestDirectory> dir_; |
| std::shared_ptr<FileSystem> fs_; |
| std::shared_ptr<MemoryPool> pool_; |
| std::shared_ptr<arrow::MemoryPool> arrow_pool_; |
| int32_t batch_size_; |
| }; |
| |
| TEST_F(FileReaderWrapperTest, EmptyFile) { |
| std::string file_path = PathUtil::JoinPath(dir_->Str(), "test.parquet"); |
| PrepareParquetFile(file_path, /*row_count=*/0); |
| ASSERT_OK_AND_ASSIGN(auto reader_wrapper, PrepareReaderWrapper(file_path)); |
| ASSERT_EQ(0, reader_wrapper->GetNumberOfRows()); |
| ASSERT_EQ(0, reader_wrapper->GetNumberOfRowGroups()); |
| ASSERT_EQ(std::numeric_limits<uint64_t>::max(), reader_wrapper->GetNextRowToRead()); |
| ASSERT_EQ(std::numeric_limits<uint64_t>::max(), |
| reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| ASSERT_OK_AND_ASSIGN(auto batch, reader_wrapper->Next()); |
| ASSERT_EQ(0, reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| ASSERT_EQ(0, reader_wrapper->GetNextRowToRead()); |
| ASSERT_TRUE(reader_wrapper->GetAllRowGroupRanges().empty()); |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::RecordBatch> record_batch, reader_wrapper->Next()); |
| ASSERT_FALSE(record_batch); |
| } |
| |
| TEST_F(FileReaderWrapperTest, NullFileReader) { |
| ASSERT_NOK_WITH_MSG(FileReaderWrapper::Create(nullptr, |
| /*batch_size=*/0, |
| /*pool=*/arrow_pool_), |
| "file reader wrapper create failed. file reader is nullptr"); |
| } |
| |
| TEST_F(FileReaderWrapperTest, Simple) { |
| std::string file_path = PathUtil::JoinPath(dir_->Str(), "test.parquet"); |
| PrepareParquetFile(file_path, /*row_count=*/5500); |
| ASSERT_OK_AND_ASSIGN(auto reader_wrapper, PrepareReaderWrapper(file_path)); |
| ASSERT_EQ(5500, reader_wrapper->GetNumberOfRows()); |
| ASSERT_EQ(6, reader_wrapper->GetNumberOfRowGroups()); |
| ASSERT_EQ(std::numeric_limits<uint64_t>::max(), reader_wrapper->GetNextRowToRead()); |
| ASSERT_EQ(std::numeric_limits<uint64_t>::max(), |
| reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| std::vector<std::pair<uint64_t, uint64_t>> expected_all_row_group_ranges = { |
| {0, 1000}, {1000, 2000}, {2000, 3000}, {3000, 4000}, {4000, 5000}, {5000, 5500}}; |
| ASSERT_EQ(expected_all_row_group_ranges, reader_wrapper->GetAllRowGroupRanges()); |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::RecordBatch> record_batch, reader_wrapper->Next()); |
| ASSERT_TRUE(record_batch); |
| ASSERT_EQ(512, record_batch->num_rows()); |
| ASSERT_EQ(512, reader_wrapper->GetNextRowToRead()); |
| ASSERT_EQ(0, reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| ASSERT_OK_AND_ASSIGN(record_batch, reader_wrapper->Next()); |
| ASSERT_TRUE(record_batch); |
| ASSERT_EQ(488, record_batch->num_rows()); |
| ASSERT_EQ(1000, reader_wrapper->GetNextRowToRead()); |
| ASSERT_EQ(512, reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| ASSERT_OK_AND_ASSIGN(record_batch, reader_wrapper->Next()); |
| ASSERT_TRUE(record_batch); |
| ASSERT_EQ(512, record_batch->num_rows()); |
| ASSERT_EQ(1512, reader_wrapper->GetNextRowToRead()); |
| ASSERT_EQ(1000, reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| ASSERT_NOK_WITH_MSG(reader_wrapper->SeekToRow(1001), |
| "should not be in the middle of readable range"); |
| ASSERT_OK(reader_wrapper->SeekToRow(1000)); |
| ASSERT_OK_AND_ASSIGN(record_batch, reader_wrapper->Next()); |
| ASSERT_TRUE(record_batch); |
| ASSERT_EQ(512, record_batch->num_rows()); |
| ASSERT_EQ(1512, reader_wrapper->GetNextRowToRead()); |
| ASSERT_EQ(1000, reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| |
| ASSERT_OK(reader_wrapper->SeekToRow(5600)); |
| ASSERT_EQ(5500, reader_wrapper->GetNextRowToRead()); |
| ASSERT_EQ(6, reader_wrapper->current_row_group_idx_); |
| ASSERT_EQ(1000, reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| ASSERT_OK_AND_ASSIGN(record_batch, reader_wrapper->Next()); |
| ASSERT_FALSE(record_batch); |
| ASSERT_EQ(5500, reader_wrapper->GetNextRowToRead()); |
| ASSERT_EQ(5500, reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| } |
| |
| /// Regression: when batch_size_ is 0 (the default) and a row group is consumed via |
| /// the page-filtered streaming path, we must not pass 0 to TableBatchReader::set_chunksize |
| /// — that would make ReadNext spin forever on zero-row batches. The wrapper now |
| /// translates 0 to int64_max so the reader produces one batch covering all matched rows. |
| TEST_F(FileReaderWrapperTest, PageFilteredZeroBatchSizeDoesNotHang) { |
| std::string file_path = PathUtil::JoinPath(dir_->Str(), "page_zero_batch.parquet"); |
| PrepareParquetFile(file_path, /*row_count=*/200, /*enable_page_index=*/true); |
| ASSERT_OK_AND_ASSIGN(auto reader_wrapper, PrepareReaderWrapper(file_path)); |
| ASSERT_EQ(1, reader_wrapper->GetNumberOfRowGroups()); |
| |
| // Inject a per-RG RowRanges to drive the page-filtered streaming path. Two non- |
| // contiguous ranges keep the test honest about RowRanges semantics; the actual |
| // numbers don't matter as long as their total falls inside the row group. |
| RowRanges rr({RowRanges::Range(0, 49), RowRanges::Range(100, 149)}); |
| |
| std::vector<int32_t> all_columns = {0, 1, 2}; |
| ASSERT_OK(reader_wrapper->PrepareForReading( |
| {TargetRowGroup(/*rg_index=*/0, /*is_partially_matched=*/true, /*ranges=*/rr)}, |
| all_columns)); |
| int64_t total = 0; |
| int64_t batch_count = 0; |
| while (true) { |
| ASSERT_OK_AND_ASSIGN(auto batch, reader_wrapper->Next()); |
| if (!batch) break; |
| total += batch->num_rows(); |
| ++batch_count; |
| ASSERT_LT(batch_count, 1000) << "Next() did not converge — likely an infinite loop"; |
| } |
| ASSERT_EQ(100, total); |
| ASSERT_GE(batch_count, 1); |
| } |
| |
| /// SeekToRow back to a previously-consumed page-filtered row group must rebuild the |
| /// per-RG streaming reader from row_group_row_ranges_ and re-yield the same rows. |
| /// The page-filter path holds no per-RG cache that consumption could destroy; the |
| /// reader is constructed on demand each time, mirroring Arrow's stateless |
| /// GetRecordBatchReader for the fully-matched path. |
| TEST_F(FileReaderWrapperTest, SeekBackToConsumedPageFilteredRowGroup) { |
| std::string file_path = PathUtil::JoinPath(dir_->Str(), "seek_back.parquet"); |
| // 2000 rows produces 2 row groups (max_row_group_length=1000) with page index enabled. |
| PrepareParquetFile(file_path, /*row_count=*/2000, /*enable_page_index=*/true); |
| ASSERT_OK_AND_ASSIGN(auto reader_wrapper, PrepareReaderWrapper(file_path)); |
| ASSERT_EQ(2, reader_wrapper->GetNumberOfRowGroups()); |
| |
| // Both RGs page-filtered. RowRanges are RG-local: RG0 keeps 40 rows, RG1 keeps 50. |
| std::map<int32_t, RowRanges> row_ranges_map; |
| row_ranges_map[0] = RowRanges(RowRanges::Range(10, 49)); |
| row_ranges_map[1] = RowRanges(RowRanges::Range(100, 149)); |
| |
| std::vector<int32_t> all_columns = {0, 1, 2}; |
| ASSERT_OK(reader_wrapper->PrepareForReading( |
| {TargetRowGroup(/*rg_index=*/0, /*is_partially_matched=*/true, |
| /*ranges=*/row_ranges_map[0]), |
| TargetRowGroup(/*rg_index=*/1, /*is_partially_matched=*/true, |
| /*ranges=*/row_ranges_map[1])}, |
| all_columns)); |
| |
| auto count_all_rows = [&](int64_t* out_total) { |
| int64_t total = 0; |
| while (true) { |
| auto next = reader_wrapper->Next(); |
| if (!next.ok()) return next.status(); |
| auto batch = std::move(next).value(); |
| if (!batch) break; |
| total += batch->num_rows(); |
| } |
| *out_total = total; |
| return Status::OK(); |
| }; |
| |
| int64_t first_total = 0; |
| ASSERT_OK(count_all_rows(&first_total)); |
| ASSERT_EQ(90, first_total); // 40 + 50 |
| |
| // Seek back to row 0 (start of RG0). The on-demand reader construction means RG0 |
| // is read again from scratch, producing the same 90 rows total. |
| ASSERT_OK(reader_wrapper->SeekToRow(0)); |
| |
| int64_t second_total = 0; |
| ASSERT_OK(count_all_rows(&second_total)); |
| ASSERT_EQ(90, second_total); |
| } |
| |
| /// When the page-level predicate matches more rows than the wrapper's batch_size, |
| /// the page-filtered streaming path must split the filtered rows across multiple |
| /// Next() calls. Pages are written 3 rows wide (write_batch_size=3 with |
| /// data_pagesize=1) so that filtered rows span multiple page-sized chunks; the |
| /// emitted batches must (a) sum to the RowRanges row count and (b) never exceed |
| /// the configured batch_size — TableBatchReader additionally caps each batch at |
| /// the underlying chunk boundary, which is fine as long as the cap holds. |
| TEST_F(FileReaderWrapperTest, PageFilteredRespectsBatchSize) { |
| constexpr int32_t kRowCount = 60; |
| constexpr int32_t kPageRowCount = 3; |
| constexpr int64_t kExpectedTotal = 30; |
| |
| std::string file_path = PathUtil::JoinPath(dir_->Str(), "page_split.parquet"); |
| PrepareParquetFile(file_path, kRowCount, /*enable_page_index=*/true, |
| /*write_batch_size=*/kPageRowCount); |
| |
| // Keep rows [0, 29] — the first 10 pages of the row group. |
| RowRanges rr({RowRanges::Range(0, kExpectedTotal - 1)}); |
| |
| for (int64_t batch_size : {int64_t{1}, int64_t{2}, int64_t{3}, int64_t{5}, int64_t{10}}) { |
| SCOPED_TRACE("batch_size=" + std::to_string(batch_size)); |
| ASSERT_OK_AND_ASSIGN(auto reader_wrapper, PrepareReaderWrapper(file_path, batch_size)); |
| ASSERT_OK(reader_wrapper->PrepareForReading( |
| {TargetRowGroup(/*rg_index=*/0, /*is_partially_matched=*/true, /*ranges=*/rr)}, |
| {0, 1, 2})); |
| |
| int64_t total = 0; |
| int64_t batch_count = 0; |
| while (true) { |
| ASSERT_OK_AND_ASSIGN(auto batch, reader_wrapper->Next()); |
| if (!batch) break; |
| ASSERT_GT(batch->num_rows(), 0); |
| ASSERT_LE(batch->num_rows(), batch_size); |
| total += batch->num_rows(); |
| ++batch_count; |
| } |
| ASSERT_EQ(kExpectedTotal, total); |
| const int64_t min_batches = (kExpectedTotal + batch_size - 1) / batch_size; |
| ASSERT_GE(batch_count, min_batches); |
| } |
| } |
| |
| /// While streaming a page-filtered row group, GetNextRowToRead() must report the next row that |
| /// survives filtering, and the row group end once the filtered ranges are exhausted. Reporting the |
| /// row group start for the whole row group makes callers believe the reader has not moved. |
| TEST_F(FileReaderWrapperTest, PageFilteredAdvancesNextRowToRead) { |
| std::string file_path = PathUtil::JoinPath(dir_->Str(), "page_next_row.parquet"); |
| // 2000 rows produces 2 row groups (max_row_group_length=1000) with page index enabled. |
| PrepareParquetFile(file_path, /*row_count=*/2000, /*enable_page_index=*/true); |
| ASSERT_OK_AND_ASSIGN(auto reader_wrapper, |
| PrepareReaderWrapper(file_path, /*wrapper_batch_size=*/7)); |
| ASSERT_EQ(2, reader_wrapper->GetNumberOfRowGroups()); |
| |
| // RowRanges are RG-local. RG0 keeps two non-contiguous stretches so that a batch can span the |
| // gap between them; RG1 keeps its first 20 rows. |
| RowRanges rg0_ranges({RowRanges::Range(10, 49), RowRanges::Range(100, 149)}); |
| RowRanges rg1_ranges(RowRanges::Range(0, 19)); |
| ASSERT_OK(reader_wrapper->PrepareForReading( |
| {TargetRowGroup(/*rg_index=*/0, /*is_partially_matched=*/true, /*ranges=*/rg0_ranges), |
| TargetRowGroup(/*rg_index=*/1, /*is_partially_matched=*/true, /*ranges=*/rg1_ranges)}, |
| /*column_indices=*/{0, 1, 2})); |
| |
| // Absolute row numbers the reader is expected to produce, in order. |
| std::vector<uint64_t> expected_rows; |
| for (uint64_t row = 10; row <= 49; ++row) { |
| expected_rows.push_back(row); |
| } |
| for (uint64_t row = 100; row <= 149; ++row) { |
| expected_rows.push_back(row); |
| } |
| for (uint64_t row = 1000; row <= 1019; ++row) { |
| expected_rows.push_back(row); |
| } |
| |
| size_t consumed = 0; |
| while (true) { |
| ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::RecordBatch> record_batch, |
| reader_wrapper->Next()); |
| if (!record_batch) { |
| break; |
| } |
| ASSERT_LT(consumed, expected_rows.size()); |
| ASSERT_EQ(expected_rows[consumed], |
| reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| consumed += record_batch->num_rows(); |
| ASSERT_LE(consumed, expected_rows.size()); |
| // RG0 ends exactly where RG1 starts, so the row group boundary is also covered by |
| // expected_rows; only the very last batch leaves the cursor at the file end. |
| uint64_t expected_next_row = |
| consumed < expected_rows.size() ? expected_rows[consumed] : 2000; |
| ASSERT_EQ(expected_next_row, reader_wrapper->GetNextRowToRead()); |
| } |
| ASSERT_EQ(expected_rows.size(), consumed); |
| } |
| |
| TEST_F(FileReaderWrapperTest, GetRowGroupRanges) { |
| std::string file_path = PathUtil::JoinPath(dir_->Str(), "test.parquet"); |
| PrepareParquetFile(file_path, /*row_count=*/5500); |
| ASSERT_OK_AND_ASSIGN(auto reader_wrapper, PrepareReaderWrapper(file_path)); |
| ASSERT_OK_AND_ASSIGN(auto ranges, reader_wrapper->GetRowGroupRanges({0, 3, 5})); |
| std::vector<std::pair<uint64_t, uint64_t>> expected_read_ranges = { |
| {0, 1000}, {3000, 4000}, {5000, 5500}}; |
| ASSERT_EQ(expected_read_ranges, ranges); |
| ASSERT_NOK_WITH_MSG(reader_wrapper->GetRowGroupRanges({0, 3, 6}), "out of bound"); |
| ASSERT_OK_AND_ASSIGN(ranges, reader_wrapper->GetRowGroupRanges({})); |
| ASSERT_TRUE(ranges.empty()); |
| } |
| |
| TEST_F(FileReaderWrapperTest, ApplyReadRanges) { |
| std::string file_path = PathUtil::JoinPath(dir_->Str(), "test.parquet"); |
| PrepareParquetFile(file_path, /*row_count=*/5500); |
| ASSERT_OK_AND_ASSIGN(auto reader_wrapper, PrepareReaderWrapper(file_path)); |
| |
| // Prepare with a subset of row groups: {0, 1, 2, 4, 5} |
| std::vector<TargetRowGroup> initial_targets = { |
| TargetRowGroup(/*rg_index=*/0, /*is_partially_matched=*/false, |
| /*ranges=*/RowRanges()), |
| TargetRowGroup(/*rg_index=*/1, /*is_partially_matched=*/false, |
| /*ranges=*/RowRanges()), |
| TargetRowGroup(/*rg_index=*/2, /*is_partially_matched=*/false, |
| /*ranges=*/RowRanges()), |
| TargetRowGroup(/*rg_index=*/4, /*is_partially_matched=*/false, |
| /*ranges=*/RowRanges()), |
| TargetRowGroup(/*rg_index=*/5, /*is_partially_matched=*/false, |
| /*ranges=*/RowRanges())}; |
| std::vector<int32_t> all_columns = {0, 1, 2}; |
| ASSERT_OK(reader_wrapper->PrepareForReadingLazy(initial_targets, all_columns)); |
| |
| // Apply read ranges that match RG 0, 3, 5. Only 0 and 5 are in initial targets. |
| std::vector<std::pair<uint64_t, uint64_t>> read_ranges = { |
| {0, 1000}, {3000, 4000}, {5000, 5500}}; |
| ASSERT_OK(reader_wrapper->ApplyReadRanges(read_ranges)); |
| |
| // Verify: reading should only produce rows from RG 0 (1000 rows) and RG 5 (500 rows). |
| int64_t total_rows = 0; |
| while (true) { |
| ASSERT_OK_AND_ASSIGN(auto batch, reader_wrapper->Next()); |
| if (!batch) { |
| break; |
| } |
| total_rows += batch->num_rows(); |
| } |
| ASSERT_EQ(1500, total_rows); |
| |
| // Apply empty read ranges should result in no data. |
| ASSERT_OK(reader_wrapper->PrepareForReadingLazy(initial_targets, all_columns)); |
| ASSERT_OK(reader_wrapper->ApplyReadRanges({})); |
| ASSERT_OK_AND_ASSIGN(auto batch, reader_wrapper->Next()); |
| ASSERT_FALSE(batch); |
| } |
| |
| TEST_F(FileReaderWrapperTest, ApplyReadRangesWiderSecondCall) { |
| std::string file_path = PathUtil::JoinPath(dir_->Str(), "test.parquet"); |
| PrepareParquetFile(file_path, /*row_count=*/5500); |
| ASSERT_OK_AND_ASSIGN(auto reader_wrapper, PrepareReaderWrapper(file_path)); |
| |
| // Prepare with row groups: {0, 1, 2, 4, 5} |
| std::vector<TargetRowGroup> initial_targets = { |
| TargetRowGroup(/*rg_index=*/0, /*is_partially_matched=*/false, |
| /*ranges=*/RowRanges()), |
| TargetRowGroup(/*rg_index=*/1, /*is_partially_matched=*/false, |
| /*ranges=*/RowRanges()), |
| TargetRowGroup(/*rg_index=*/2, /*is_partially_matched=*/false, |
| /*ranges=*/RowRanges()), |
| TargetRowGroup(/*rg_index=*/4, /*is_partially_matched=*/false, |
| /*ranges=*/RowRanges()), |
| TargetRowGroup(/*rg_index=*/5, /*is_partially_matched=*/false, |
| /*ranges=*/RowRanges())}; |
| std::vector<int32_t> all_columns = {0, 1, 2}; |
| ASSERT_OK(reader_wrapper->PrepareForReadingLazy(initial_targets, all_columns)); |
| |
| // First ApplyReadRanges: narrow to RG 0 only. |
| ASSERT_OK(reader_wrapper->ApplyReadRanges({{0, 1000}})); |
| |
| // Second ApplyReadRanges: widen to RG 0, 1, 2. Previously excluded RG 1, 2 should restore. |
| ASSERT_OK(reader_wrapper->ApplyReadRanges({{0, 1000}, {1000, 2000}, {2000, 3000}})); |
| |
| // Verify: reading should produce rows from RG 0 + 1 + 2 = 3000 rows. |
| int64_t total_rows = 0; |
| while (true) { |
| ASSERT_OK_AND_ASSIGN(auto batch, reader_wrapper->Next()); |
| if (!batch) break; |
| total_rows += batch->num_rows(); |
| } |
| ASSERT_EQ(3000, total_rows); |
| } |
| |
| TEST_F(FileReaderWrapperTest, PrepareForReading) { |
| std::string file_path = PathUtil::JoinPath(dir_->Str(), "test.parquet"); |
| PrepareParquetFile(file_path, /*row_count=*/5500); |
| ASSERT_OK_AND_ASSIGN(auto reader_wrapper, PrepareReaderWrapper(file_path)); |
| ASSERT_OK(reader_wrapper->PrepareForReading( |
| /*target_row_groups=*/{TargetRowGroup(/*rg_index=*/1, /*is_partially_matched=*/false, |
| /*ranges=*/RowRanges())}, |
| /*column_indices=*/{0})); |
| // seek before actual read range |
| ASSERT_OK(reader_wrapper->SeekToRow(0)); |
| ASSERT_EQ(1000, reader_wrapper->GetNextRowToRead()); |
| ASSERT_EQ(std::numeric_limits<uint64_t>::max(), |
| reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| ASSERT_OK_AND_ASSIGN(auto record_batch, reader_wrapper->Next()); |
| ASSERT_EQ(512, record_batch->num_rows()); |
| ASSERT_EQ(1, record_batch->num_columns()); |
| ASSERT_EQ(1512, reader_wrapper->GetNextRowToRead()); |
| ASSERT_EQ(1000, reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| ASSERT_OK_AND_ASSIGN(record_batch, reader_wrapper->Next()); |
| ASSERT_TRUE(record_batch); |
| ASSERT_EQ(488, record_batch->num_rows()); |
| ASSERT_EQ(5500, reader_wrapper->GetNextRowToRead()); |
| ASSERT_EQ(1512, reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| ASSERT_OK_AND_ASSIGN(record_batch, reader_wrapper->Next()); |
| ASSERT_FALSE(record_batch); |
| |
| // empty column indices |
| ASSERT_OK(reader_wrapper->PrepareForReading( |
| /*target_row_groups=*/{TargetRowGroup(/*rg_index=*/0, /*is_partially_matched=*/false, |
| /*ranges=*/RowRanges()), |
| TargetRowGroup(/*rg_index=*/1, /*is_partially_matched=*/false, |
| /*ranges=*/RowRanges())}, |
| /*column_indices=*/{})); |
| ASSERT_EQ(0, reader_wrapper->GetNextRowToRead()); |
| ASSERT_EQ(std::numeric_limits<uint64_t>::max(), |
| reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| ASSERT_OK_AND_ASSIGN(record_batch, reader_wrapper->Next()); |
| ASSERT_EQ(512, record_batch->num_rows()); |
| ASSERT_EQ(0, record_batch->num_columns()); |
| |
| // empty row group indices |
| ASSERT_OK(reader_wrapper->PrepareForReading( |
| /*target_row_groups=*/{}, |
| /*column_indices=*/{0})); |
| ASSERT_EQ(5500, reader_wrapper->GetNextRowToRead()); |
| ASSERT_EQ(std::numeric_limits<uint64_t>::max(), |
| reader_wrapper->GetPreviousBatchFirstRowNumber().value()); |
| } |
| |
| } // namespace paimon::parquet::test |