@@ -343,6 +343,122 @@ TEST_F(ParquetFileBatchReaderTest, TestParquetMetadataCacheBypassesWhenGetUriFai
343343 ASSERT_EQ (0 , cache->Size ());
344344}
345345
346+ TEST_F (ParquetFileBatchReaderTest, TestPageIndexBytesSurviveReaderClose) {
347+ WriteArray (file_path_, struct_array_, schema_, /* write_batch_size=*/ 1 ,
348+ /* enable_dictionary=*/ false , /* max_row_group_length=*/ 3 ,
349+ /* max_page_size=*/ 1 );
350+ auto cache = std::make_shared<paimon::test::CountingRoutingCache>(CacheKind::DATA_FILE_FOOTER ,
351+ 128 * 1024 * 1024 );
352+ ASSERT_OK_AND_ASSIGN (std::shared_ptr<InputStream> raw_input, fs_->Open (file_path_));
353+ ASSERT_OK_AND_ASSIGN (int64_t length, raw_input->Length ());
354+ auto raw = std::make_shared<ArrowInputStreamAdapter>(raw_input, length, pool_);
355+ auto raw_reader = ::parquet::ParquetFileReader::Open (raw);
356+ auto metadata = raw_reader->metadata ();
357+ int64_t expected_index_reads = 0 ;
358+ std::weak_ptr<MemoryPool> cached_pool;
359+ for (int32_t round = 0 ; round < 3 ; ++round) {
360+ if (round == 2 ) {
361+ cache->InvalidateAll ();
362+ ASSERT_TRUE (cached_pool.expired ());
363+ }
364+ ASSERT_OK_AND_ASSIGN (std::shared_ptr<InputStream> input, fs_->Open (file_path_));
365+ std::shared_ptr<MemoryPool> query_pool = GetMemoryPool ();
366+ auto stream = std::make_shared<ParquetInputStream>(input, length, pool_, query_pool, cache,
367+ file_path_);
368+ stream->SetPageIndexRanges (*metadata);
369+ for (int32_t rg = 0 ; rg < metadata->num_row_groups (); ++rg) {
370+ auto ranges = ::parquet::PageIndexReader::DeterminePageIndexRangesInRowGroup (
371+ *metadata->RowGroup (rg), {});
372+ for (const auto & range : {ranges.column_index , ranges.offset_index }) {
373+ ASSERT_TRUE (range.has_value ());
374+ auto expected = raw->ReadAt (range->offset , range->length );
375+ ASSERT_TRUE (expected.ok ()) << expected.status ().ToString ();
376+ auto actual = stream->ReadAt (range->offset , range->length );
377+ ASSERT_TRUE (actual.ok ()) << actual.status ().ToString ();
378+ ASSERT_TRUE (actual.ValueOrDie ()->Equals (*expected.ValueOrDie ()));
379+ if (round != 1 ) {
380+ ++expected_index_reads;
381+ }
382+ }
383+ }
384+ ASSERT_EQ (expected_index_reads, cache->SupplierCallCount ());
385+ // Reader 2 serves all indexes from the shared cache without storage IO.
386+ if (round == 1 ) {
387+ ASSERT_EQ (0 , stream->StorageReadBytes ()->load ());
388+ } else {
389+ ASSERT_GT (stream->StorageReadBytes ()->load (), 0 );
390+ }
391+ // Ordinary file bytes must still use storage, even on a cache hit round.
392+ uint64_t bytes_before = stream->StorageReadBytes ()->load ();
393+ auto magic = stream->ReadAt (0 , 4 );
394+ ASSERT_TRUE (magic.ok ()) << magic.status ().ToString ();
395+ ASSERT_EQ (" PAR1" , magic.ValueOrDie ()->ToString ());
396+ ASSERT_EQ (bytes_before + 4 , stream->StorageReadBytes ()->load ());
397+ ASSERT_EQ (expected_index_reads, cache->SupplierCallCount ());
398+ ASSERT_TRUE (stream->Close ().ok ());
399+ if (round != 1 ) {
400+ cached_pool = query_pool;
401+ }
402+ stream.reset ();
403+ query_pool.reset ();
404+ ASSERT_FALSE (cached_pool.expired ());
405+ }
406+ cache->InvalidateAll ();
407+ ASSERT_TRUE (cached_pool.expired ());
408+ }
409+
410+ TEST_F (ParquetFileBatchReaderTest, TestCachedFooterKeepsAllocatorAliveUntilEviction) {
411+ WriteArray (file_path_, struct_array_, schema_, /* write_batch_size=*/ 1 ,
412+ /* enable_dictionary=*/ false , /* max_row_group_length=*/ 3 );
413+ auto cache = std::make_shared<paimon::test::CountingRoutingCache>(CacheKind::DATA_FILE_FOOTER ,
414+ 128 * 1024 * 1024 );
415+ std::weak_ptr<MemoryPool> cached_pool;
416+ {
417+ std::shared_ptr<MemoryPool> query_pool = GetMemoryPool ();
418+ cached_pool = query_pool;
419+ ParquetReaderBuilder builder ({}, 10 );
420+ builder.WithMemoryPool (query_pool)->WithCache (cache);
421+ ASSERT_OK_AND_ASSIGN (std::shared_ptr<InputStream> input, fs_->Open (file_path_));
422+ ASSERT_OK_AND_ASSIGN (auto reader, builder.Build (input));
423+ }
424+ ASSERT_FALSE (cached_pool.expired ());
425+ cache->InvalidateAll ();
426+ ASSERT_TRUE (cached_pool.expired ());
427+ }
428+
429+ TEST_F (ParquetFileBatchReaderTest, TestPointReadReusesFooterAndPageIndexes) {
430+ WriteArray (file_path_, struct_array_, schema_, /* write_batch_size=*/ 1 ,
431+ /* enable_dictionary=*/ false , /* max_row_group_length=*/ 3 ,
432+ /* max_page_size=*/ 1 );
433+ auto cache = std::make_shared<paimon::test::CountingRoutingCache>(CacheKind::DATA_FILE_FOOTER ,
434+ 128 * 1024 * 1024 );
435+ auto projection = arrow::schema ({schema_->GetFieldByName (" f4" ), schema_->GetFieldByName (" f8" )});
436+ auto predicate = PredicateBuilder::Equal (0 , " f4" , FieldType::INT , Literal (300002 ));
437+ int64_t cold_reads = 0 ;
438+ std::shared_ptr<arrow::ChunkedArray> cold_result;
439+ for (int32_t round = 0 ; round < 2 ; ++round) {
440+ ASSERT_OK_AND_ASSIGN (std::shared_ptr<InputStream> input, fs_->Open (file_path_));
441+ ParquetReaderBuilder builder ({{PARQUET_READ_ENABLE_PAGE_INDEX_FILTER , " true" }}, 10 );
442+ builder.WithCache (cache);
443+ ASSERT_OK_AND_ASSIGN (auto reader, builder.Build (input));
444+ ArrowSchema c_schema;
445+ ASSERT_TRUE (arrow::ExportSchema (*projection, &c_schema).ok ());
446+ ASSERT_OK (reader->SetReadSchema (&c_schema, predicate, std::nullopt ));
447+ ASSERT_OK_AND_ASSIGN (auto result,
448+ paimon::test::ReadResultCollector::CollectResult (reader.get ()));
449+ ASSERT_EQ (1 , result->length ());
450+ if (round == 0 ) {
451+ cold_reads = cache->SupplierCallCount ();
452+ ASSERT_GT (cold_reads, 1 ); // Footer plus actual page-index ranges.
453+ cold_result = result;
454+ } else {
455+ ASSERT_TRUE (result->Equals (cold_result));
456+ ASSERT_EQ (cold_reads, cache->SupplierCallCount ());
457+ ASSERT_GT (cache->GetCount (), cold_reads);
458+ }
459+ }
460+ }
461+
346462TEST_F (ParquetFileBatchReaderTest, TestReadBinaryWrittenFromBinaryAndLargeBinary) {
347463 auto check_binary_read_result = [&](const std::shared_ptr<arrow::DataType>& write_type,
348464 const std::string& file_name) {
0 commit comments