This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 126bb220312 [fix](be) Preserve condition cache granule base (#65496)
126bb220312 is described below

commit 126bb220312f6d774b3524df379628a1b1069ba4
Author: Gabriel <[email protected]>
AuthorDate: Mon Jul 13 09:27:42 2026 +0800

    [fix](be) Preserve condition cache granule base (#65496)
    
    ### What problem does this PR solve?
    
    Issue Number: None
    
    Related PR: None
    
    Problem Summary: External file condition-cache entries stored only their
    survivor bitmap. On a
    later cache hit, the physical reader recomputed the bitmap base from the
    current metadata-pruned
    scan plan. If pruning selected a different first row group, bitmap
    indexes shifted and valid rows
    could be silently skipped.
    
    This change stores the bitmap's base granule with the cache value. The
    V2 table reader restores the
    stored base on cache hits, while V2 Parquet and ORC readers derive a
    base from their current plan
    only when producing a new cache entry. V1 reader behavior is unchanged.
    
    ### Release note
    
    Fix incorrect row filtering when a V2 external file condition-cache hit
    uses a different pruned
    scan plan from the cache-producing scan.
    
    ### Check List (For Author)
    
    - Test: Unit Test
    -
    `ParquetScanConditionCacheTest.HitKeepsCachedBaseWhenCurrentPlanStartsLater`
        - `TableReaderTest.ConditionCacheMissPublishesBitmapAfterReaderEof`
        - `NewOrcReaderTest.ConditionCacheHitUsesSplitBaseGranule`
    - Behavior changed: Yes. V2 condition-cache hits use the bitmap
    coordinate base stored with the
      cache entry instead of recomputing it from the current scan plan.
    - Does this need documentation: No
---
 be/src/format_v2/orc/orc_reader.cpp             | 28 ++++++++-
 be/src/format_v2/orc/orc_reader.h               |  1 +
 be/src/format_v2/parquet/parquet_scan.cpp       | 12 +++-
 be/src/format_v2/table_reader.cpp               | 13 +++-
 be/src/format_v2/table_reader.h                 |  5 +-
 be/src/storage/segment/condition_cache.cpp      | 10 ++-
 be/src/storage/segment/condition_cache.h        | 26 ++++++--
 be/test/format_v2/orc/orc_reader_test.cpp       | 69 +++++++++++++++++++--
 be/test/format_v2/parquet/parquet_scan_test.cpp | 22 +++++++
 be/test/format_v2/table_reader_test.cpp         | 82 ++++++++++++++++++++++++-
 10 files changed, 244 insertions(+), 24 deletions(-)

diff --git a/be/src/format_v2/orc/orc_reader.cpp 
b/be/src/format_v2/orc/orc_reader.cpp
index 32b8aed2a88..7c5b49b1681 100644
--- a/be/src/format_v2/orc/orc_reader.cpp
+++ b/be/src/format_v2/orc/orc_reader.cpp
@@ -885,18 +885,43 @@ Status OrcReader::init(RuntimeState* state) {
     return Status::OK();
 }
 
+void OrcReader::_extend_condition_cache_context_for_current_range() {
+    if (_state->condition_cache_ctx == nullptr ||
+        _state->condition_cache_ctx->filter_result == nullptr ||
+        _state->condition_cache_ctx->is_hit) {
+        return;
+    }
+    const auto end_granule = (_state->row_reader_range_first_row + 
_state->row_reader_range_rows +
+                              ConditionCacheContext::GRANULE_SIZE - 1) /
+                             ConditionCacheContext::GRANULE_SIZE;
+    DORIS_CHECK(end_granule > 
static_cast<uint64_t>(_state->condition_cache_ctx->base_granule));
+    const auto required_granules = static_cast<size_t>(
+            end_granule - 
static_cast<uint64_t>(_state->condition_cache_ctx->base_granule));
+    if (_state->condition_cache_ctx->filter_result->size() < 
required_granules) {
+        _state->condition_cache_ctx->filter_result->resize(required_granules, 
false);
+    }
+    _state->condition_cache_ctx->num_granules =
+            std::max(_state->condition_cache_ctx->num_granules, 
required_granules);
+}
+
 void 
OrcReader::set_condition_cache_context(std::shared_ptr<ConditionCacheContext> 
ctx) {
     DORIS_CHECK(_state != nullptr);
     _state->condition_cache_ctx = std::move(ctx);
     if (_state->condition_cache_ctx != nullptr &&
-        _state->condition_cache_ctx->filter_result != nullptr) {
+        _state->condition_cache_ctx->filter_result != nullptr &&
+        !_state->condition_cache_ctx->is_hit) {
         _state->condition_cache_ctx->base_granule = static_cast<int64_t>(
                 _state->row_reader_range_first_row / 
ConditionCacheContext::GRANULE_SIZE);
+        _state->condition_cache_ctx->num_granules = 0;
+        _extend_condition_cache_context_for_current_range();
     }
 }
 
 int64_t OrcReader::get_total_rows() const {
     DORIS_CHECK(_state != nullptr);
+    if (_state->stripe_pruning_applied && 
_state->selected_stripe_ranges.empty()) {
+        return 0;
+    }
     if (_state->row_reader != nullptr) {
         return cast_set<int64_t>(_state->row_reader_range_rows);
     }
@@ -1543,6 +1568,7 @@ Status OrcReader::_create_row_reader() {
         _state->row_reader_range_end_row =
                 _state->row_reader_range_first_row + 
_state->row_reader_range_rows;
         _state->condition_cache_next_row = _state->row_reader_range_first_row;
+        _extend_condition_cache_context_for_current_range();
         _state->column_to_selected_batch_index.clear();
         size_t physical_read_column_count = 0;
         for (const auto file_column_id : _state->read_columns) {
diff --git a/be/src/format_v2/orc/orc_reader.h 
b/be/src/format_v2/orc/orc_reader.h
index e7de8cf5c1a..279ae7373cc 100644
--- a/be/src/format_v2/orc/orc_reader.h
+++ b/be/src/format_v2/orc/orc_reader.h
@@ -154,6 +154,7 @@ private:
     bool _filter_has_row_level_predicates() const;
     Status _build_keep_filter(Block* file_block, size_t rows, IColumn::Filter* 
keep_filter) const;
     Status _filter_block(Block* file_block, size_t* rows) const;
+    void _extend_condition_cache_context_for_current_range();
     void _skip_condition_cache_false_granules(size_t* rows, bool* eof);
     void _mark_condition_cache_surviving_rows(const IColumn::Filter& 
keep_filter,
                                               size_t rows) const;
diff --git a/be/src/format_v2/parquet/parquet_scan.cpp 
b/be/src/format_v2/parquet/parquet_scan.cpp
index 99bfcc8e0ab..a7602cd11b2 100644
--- a/be/src/format_v2/parquet/parquet_scan.cpp
+++ b/be/src/format_v2/parquet/parquet_scan.cpp
@@ -605,9 +605,17 @@ void 
ParquetScanScheduler::set_condition_cache_context(std::shared_ptr<Condition
         return;
     }
 
-    _condition_cache_ctx->base_granule =
-            _row_group_plans.front().first_file_row / 
ConditionCacheContext::GRANULE_SIZE;
     if (!_condition_cache_ctx->is_hit) {
+        _condition_cache_ctx->base_granule =
+                _row_group_plans.front().first_file_row / 
ConditionCacheContext::GRANULE_SIZE;
+        const auto& last_plan = _row_group_plans.back();
+        const int64_t end_granule = (last_plan.first_file_row + 
last_plan.row_group_rows +
+                                     ConditionCacheContext::GRANULE_SIZE - 1) /
+                                    ConditionCacheContext::GRANULE_SIZE;
+        DORIS_CHECK(end_granule > _condition_cache_ctx->base_granule);
+        _condition_cache_ctx->num_granules =
+                std::min(_condition_cache_ctx->filter_result->size(),
+                         static_cast<size_t>(end_granule - 
_condition_cache_ctx->base_granule));
         return;
     }
 
diff --git a/be/src/format_v2/table_reader.cpp 
b/be/src/format_v2/table_reader.cpp
index 11e2b30df6d..69ae3b562cf 100644
--- a/be/src/format_v2/table_reader.cpp
+++ b/be/src/format_v2/table_reader.cpp
@@ -581,7 +581,8 @@ Status TableReader::_init_reader_condition_cache(const 
FileScanRequest& file_req
     const auto& file = *_current_file_description;
     _condition_cache_key = segment_v2::ConditionCache::ExternalCacheKey(
             file.path, file.mtime, file.file_size, _condition_cache_digest, 
file.range_start_offset,
-            file.range_size);
+            file.range_size,
+            
segment_v2::ConditionCache::ExternalCacheKey::BASE_GRANULE_AWARE_VERSION);
 
     segment_v2::ConditionCacheHandle handle;
     const bool condition_cache_hit = cache->lookup(_condition_cache_key, 
&handle);
@@ -605,6 +606,10 @@ Status TableReader::_init_reader_condition_cache(const 
FileScanRequest& file_req
         _condition_cache_ctx = std::make_shared<ConditionCacheContext>();
         _condition_cache_ctx->is_hit = condition_cache_hit;
         _condition_cache_ctx->filter_result = _condition_cache;
+        _condition_cache_ctx->num_granules = _condition_cache->size();
+        if (condition_cache_hit) {
+            _condition_cache_ctx->base_granule = handle.get_base_granule();
+        }
         _data_reader.reader->set_condition_cache_context(_condition_cache_ctx);
     }
     return Status::OK();
@@ -624,8 +629,10 @@ void TableReader::_finalize_reader_condition_cache() {
         _condition_cache_ctx = nullptr;
         return;
     }
-    segment_v2::ConditionCache::instance()->insert(_condition_cache_key,
-                                                   
std::move(_condition_cache));
+    DORIS_CHECK(_condition_cache_ctx->num_granules <= 
_condition_cache->size());
+    _condition_cache->resize(_condition_cache_ctx->num_granules);
+    segment_v2::ConditionCache::instance()->insert(
+            _condition_cache_key, std::move(_condition_cache), 
_condition_cache_ctx->base_granule);
     _condition_cache = nullptr;
     _condition_cache_ctx = nullptr;
 }
diff --git a/be/src/format_v2/table_reader.h b/be/src/format_v2/table_reader.h
index da2cb351ff6..ec179abb92f 100644
--- a/be/src/format_v2/table_reader.h
+++ b/be/src/format_v2/table_reader.h
@@ -252,9 +252,10 @@ public:
             size_t current_rows = 0;
             
RETURN_IF_ERROR(_data_reader.reader->get_block(&_data_reader.block_template,
                                                            &current_rows, 
&current_eof));
+            const bool stopped_during_read = _io_ctx != nullptr && 
_io_ctx->should_stop;
             if (current_rows == 0) {
                 if (current_eof) {
-                    _current_reader_reached_eof = true;
+                    _current_reader_reached_eof = !stopped_during_read;
                     RETURN_IF_ERROR(close_current_reader());
                 }
                 continue;
@@ -271,7 +272,7 @@ public:
                     _check_table_block_columns("after finalize_chunk", block, 
current_rows));
 #endif
             if (current_eof) {
-                _current_reader_reached_eof = true;
+                _current_reader_reached_eof = !stopped_during_read;
                 RETURN_IF_ERROR(close_current_reader());
             }
             return Status::OK();
diff --git a/be/src/storage/segment/condition_cache.cpp 
b/be/src/storage/segment/condition_cache.cpp
index d6499f0eaa8..78ca6a6d053 100644
--- a/be/src/storage/segment/condition_cache.cpp
+++ b/be/src/storage/segment/condition_cache.cpp
@@ -38,7 +38,8 @@ bool ConditionCache::lookup(const KeyType& key, 
ConditionCacheHandle* handle) {
 }
 
 template <typename KeyType>
-void ConditionCache::insert(const KeyType& key, 
std::shared_ptr<std::vector<bool>> result) {
+void ConditionCache::insert(const KeyType& key, 
std::shared_ptr<std::vector<bool>> result,
+                            int64_t base_granule) {
     auto encoded_key = key.encode();
     if (encoded_key.empty()) {
         return;
@@ -46,6 +47,7 @@ void ConditionCache::insert(const KeyType& key, 
std::shared_ptr<std::vector<bool
     std::unique_ptr<ConditionCache::CacheValue> cache_value_ptr =
             std::make_unique<ConditionCache::CacheValue>();
     cache_value_ptr->filter_result = result;
+    cache_value_ptr->base_granule = base_granule;
 
     ConditionCacheHandle(this, LRUCachePolicy::insert(encoded_key, 
(void*)cache_value_ptr.release(),
                                                       result->capacity(), 
result->capacity(),
@@ -58,8 +60,10 @@ template bool 
ConditionCache::lookup<ConditionCache::CacheKey>(const CacheKey& k
 template bool ConditionCache::lookup<ConditionCache::ExternalCacheKey>(
         const ExternalCacheKey& key, ConditionCacheHandle* handle);
 template void ConditionCache::insert<ConditionCache::CacheKey>(
-        const CacheKey& key, std::shared_ptr<std::vector<bool>> filter_result);
+        const CacheKey& key, std::shared_ptr<std::vector<bool>> filter_result,
+        int64_t base_granule);
 template void ConditionCache::insert<ConditionCache::ExternalCacheKey>(
-        const ExternalCacheKey& key, std::shared_ptr<std::vector<bool>> 
filter_result);
+        const ExternalCacheKey& key, std::shared_ptr<std::vector<bool>> 
filter_result,
+        int64_t base_granule);
 
 } // namespace doris::segment_v2
diff --git a/be/src/storage/segment/condition_cache.h 
b/be/src/storage/segment/condition_cache.h
index a189312ee14..0ef5534a622 100644
--- a/be/src/storage/segment/condition_cache.h
+++ b/be/src/storage/segment/condition_cache.h
@@ -48,6 +48,7 @@ struct ConditionCacheContext {
     bool is_hit = false;
     std::shared_ptr<std::vector<bool>> filter_result; // per-granule: true = 
has surviving rows
     int64_t base_granule = 0;                         // global granule index 
of filter_result[0]
+    size_t num_granules = 0; // authoritative bitmap length; excludes 
allocation-only guard bits
     static constexpr int GRANULE_SIZE = 2048;
 };
 
@@ -80,35 +81,44 @@ public:
     class CacheValue : public LRUCacheValueBase {
     public:
         std::shared_ptr<std::vector<bool>> filter_result;
+        // The bitmap coordinate system is part of the cached result. A later 
scan may prune a
+        // different first row group, so it must not derive this origin from 
its current plan.
+        int64_t base_granule = 0;
     };
 
     // Cache key for external tables (Hive ORC/Parquet)
     struct ExternalCacheKey {
+        static constexpr uint8_t BASE_GRANULE_AWARE_VERSION = 1;
+
         ExternalCacheKey() = default;
         ExternalCacheKey(const std::string& path_, int64_t modification_time_, 
int64_t file_size_,
-                         uint64_t digest_, int64_t start_offset_, int64_t 
size_)
+                         uint64_t digest_, int64_t start_offset_, int64_t 
size_,
+                         uint8_t format_version_ = 0)
                 : path(path_),
                   modification_time(modification_time_),
                   file_size(file_size_),
                   digest(digest_),
                   start_offset(start_offset_),
-                  size(size_) {}
+                  size(size_),
+                  format_version(format_version_) {}
         std::string path;
         int64_t modification_time = 0;
         int64_t file_size = 0;
         uint64_t digest = 0;
         int64_t start_offset = 0;
         int64_t size = 0;
+        uint8_t format_version = 0;
 
         [[nodiscard]] std::string encode() const {
             std::string key = path;
-            char buf[40];
+            char buf[41];
             memcpy(buf, &modification_time, 8);
             memcpy(buf + 8, &file_size, 8);
             memcpy(buf + 16, &digest, 8);
             memcpy(buf + 24, &start_offset, 8);
             memcpy(buf + 32, &size, 8);
-            key.append(buf, 40);
+            buf[40] = static_cast<char>(format_version);
+            key.append(buf, 41);
             return key;
         }
     };
@@ -135,7 +145,8 @@ public:
     bool lookup(const KeyType& key, ConditionCacheHandle* handle);
 
     template <typename KeyType>
-    void insert(const KeyType& key, std::shared_ptr<std::vector<bool>> 
filter_result);
+    void insert(const KeyType& key, std::shared_ptr<std::vector<bool>> 
filter_result,
+                int64_t base_granule = 0);
 };
 
 class ConditionCacheHandle {
@@ -172,6 +183,11 @@ public:
         return 
((ConditionCache::CacheValue*)_cache->value(_handle))->filter_result;
     }
 
+    int64_t get_base_granule() const {
+        DORIS_CHECK(_cache != nullptr);
+        return 
((ConditionCache::CacheValue*)_cache->value(_handle))->base_granule;
+    }
+
 private:
     LRUCachePolicy* _cache = nullptr;
     Cache::Handle* _handle = nullptr;
diff --git a/be/test/format_v2/orc/orc_reader_test.cpp 
b/be/test/format_v2/orc/orc_reader_test.cpp
index 0e36498c41d..c2e7bf63236 100644
--- a/be/test/format_v2/orc/orc_reader_test.cpp
+++ b/be/test/format_v2/orc/orc_reader_test.cpp
@@ -3419,7 +3419,8 @@ void write_multi_stripe_orc_int_file(const std::string& 
file_path,
 }
 
 void write_multi_stripe_orc_int_only_file(const std::string& file_path,
-                                          const std::vector<int64_t>& 
first_values) {
+                                          const std::vector<int64_t>& 
first_values,
+                                          int64_t rows_per_stripe = 200) {
     auto type = 
std::unique_ptr<::orc::Type>(::orc::Type::buildTypeFromString("struct<id:int>"));
 
     MemoryOutputStream memory_stream(1024 * 1024);
@@ -3430,15 +3431,14 @@ void write_multi_stripe_orc_int_only_file(const 
std::string& file_path,
     auto writer = ::orc::createWriter(*type, &memory_stream, options);
 
     auto add_batch = [&](int64_t first_value) {
-        constexpr int64_t ROWS_PER_STRIPE = 200;
-        auto batch = writer->createRowBatch(ROWS_PER_STRIPE);
+        auto batch = writer->createRowBatch(rows_per_stripe);
         auto& struct_batch = dynamic_cast<::orc::StructVectorBatch&>(*batch);
         auto& id_batch = 
dynamic_cast<::orc::LongVectorBatch&>(*struct_batch.fields[0]);
-        for (int64_t row = 0; row < ROWS_PER_STRIPE; ++row) {
+        for (int64_t row = 0; row < rows_per_stripe; ++row) {
             id_batch.data[row] = first_value + row;
         }
-        struct_batch.numElements = ROWS_PER_STRIPE;
-        id_batch.numElements = ROWS_PER_STRIPE;
+        struct_batch.numElements = rows_per_stripe;
+        id_batch.numElements = rows_per_stripe;
         writer->add(*batch);
     };
 
@@ -5455,6 +5455,7 @@ TEST_F(NewOrcReaderTest, 
ConditionCacheHitUsesSplitBaseGranule) {
     auto ctx = std::make_shared<ConditionCacheContext>();
     ctx->is_hit = true;
     const auto base_granule = stripe_first_row / 
ConditionCacheContext::GRANULE_SIZE;
+    ctx->base_granule = static_cast<int64_t>(base_granule);
     const auto last_granule = (stripe_first_row + layout[stripe_index].rows - 
1) /
                               ConditionCacheContext::GRANULE_SIZE;
     ctx->filter_result = std::make_shared<std::vector<bool>>(last_granule - 
base_granule + 1, true);
@@ -8724,6 +8725,59 @@ TEST_F(NewOrcReaderTest, 
SargConjunctReadsNonAdjacentStripeRangesAfterPruning) {
     EXPECT_EQ(reader->reader_statistics().filtered_group_rows, 200);
 }
 
+TEST_F(NewOrcReaderTest, 
ConditionCacheMissExtendsAcrossNonAdjacentStripeRanges) {
+    constexpr int64_t ROWS_PER_STRIPE = 2500;
+    const auto multi_stripe_file_path =
+            (_test_dir / "condition_cache_non_adjacent_stripes.orc").string();
+    write_multi_stripe_orc_int_only_file(multi_stripe_file_path, {1, 10000, 
3000}, ROWS_PER_STRIPE);
+    ASSERT_EQ(get_orc_stripe_count(multi_stripe_file_path), 3);
+
+    auto reader = create_reader_for_path(multi_stripe_file_path);
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    ASSERT_TRUE(reader->init(&state).ok());
+
+    std::vector<format::ColumnDefinition> schema;
+    ASSERT_TRUE(reader->get_schema(&schema).ok());
+    ASSERT_EQ(schema.size(), 1);
+
+    auto request = std::make_shared<format::FileScanRequest>();
+    request->predicate_columns = {field_projection(0)};
+    request->conjuncts.push_back(
+            
VExprContext::create_shared(std::make_shared<NullableInt32LessThanExpr>(0, 
5000)));
+    ASSERT_TRUE(reader->open(request).ok());
+    EXPECT_EQ(reader->get_total_rows(), ROWS_PER_STRIPE);
+
+    auto cache_ctx = std::make_shared<ConditionCacheContext>();
+    cache_ctx->filter_result = std::make_shared<std::vector<bool>>(
+            (reader->get_total_rows() + ConditionCacheContext::GRANULE_SIZE - 
1) /
+                            ConditionCacheContext::GRANULE_SIZE +
+                    1,
+            false);
+    reader->set_condition_cache_context(cache_ctx);
+    EXPECT_EQ(cache_ctx->base_granule, 0);
+    EXPECT_EQ(cache_ctx->num_granules, 2);
+
+    bool eof = false;
+    size_t result_rows = 0;
+    while (!eof) {
+        Block block = build_file_block(schema);
+        size_t rows = 0;
+        ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+        result_rows += rows;
+    }
+
+    EXPECT_EQ(result_rows, 4500);
+    ASSERT_NE(cache_ctx->filter_result, nullptr);
+    EXPECT_EQ(cache_ctx->num_granules, 4);
+    ASSERT_EQ(cache_ctx->filter_result->size(), 4);
+    EXPECT_TRUE((*cache_ctx->filter_result)[0]);
+    EXPECT_TRUE((*cache_ctx->filter_result)[1]);
+    EXPECT_TRUE((*cache_ctx->filter_result)[2]);
+    EXPECT_TRUE((*cache_ctx->filter_result)[3]);
+    EXPECT_EQ(reader->reader_statistics().filtered_row_groups, 1);
+    EXPECT_EQ(reader->reader_statistics().filtered_group_rows, 
ROWS_PER_STRIPE);
+}
+
 TEST_F(NewOrcReaderTest, SargConjunctReturnsEofWhenAllStripesArePruned) {
     const auto multi_stripe_file_path = (_test_dir / 
"all_pruned_stripes.orc").string();
     write_multi_stripe_orc_int_file(multi_stripe_file_path);
@@ -8742,6 +8796,9 @@ TEST_F(NewOrcReaderTest, 
SargConjunctReturnsEofWhenAllStripesArePruned) {
     request->conjuncts.push_back(
             
VExprContext::create_shared(std::make_shared<NullableInt32GreaterThanExpr>(0, 
5000)));
     ASSERT_TRUE(reader->open(request).ok());
+    // TableReader uses this value to decide whether a condition-cache MISS 
bitmap should be
+    // created. When SARG pruning removes every stripe, there is no row-reader 
range to cache.
+    EXPECT_EQ(reader->get_total_rows(), 0);
 
     Block block = build_file_block(schema);
     size_t rows = 0;
diff --git a/be/test/format_v2/parquet/parquet_scan_test.cpp 
b/be/test/format_v2/parquet/parquet_scan_test.cpp
index edd51c45203..a101936c391 100644
--- a/be/test/format_v2/parquet/parquet_scan_test.cpp
+++ b/be/test/format_v2/parquet/parquet_scan_test.cpp
@@ -537,6 +537,28 @@ TEST_F(ParquetScanTest, 
PlanRowGroupsSelectsAllRowGroupsWithoutFilters) {
     }
 }
 
+TEST(ParquetScanConditionCacheTest, 
HitKeepsCachedBaseWhenCurrentPlanStartsLater) {
+    format::parquet::RowGroupScanPlan plan;
+    plan.row_groups.push_back(
+            {.row_group_id = 1,
+             .first_file_row = ConditionCacheContext::GRANULE_SIZE,
+             .row_group_rows = ConditionCacheContext::GRANULE_SIZE,
+             .selected_ranges = {{.start = 0, .length = 
ConditionCacheContext::GRANULE_SIZE}},
+             .page_skip_plans = {}});
+
+    format::parquet::ParquetScanScheduler scheduler;
+    scheduler.set_plan(std::move(plan));
+    auto ctx = std::make_shared<ConditionCacheContext>();
+    ctx->is_hit = true;
+    ctx->base_granule = 0;
+    ctx->filter_result = std::make_shared<std::vector<bool>>(std::vector<bool> 
{false});
+    scheduler.set_condition_cache_context(ctx);
+
+    EXPECT_FALSE(scheduler.empty());
+    EXPECT_EQ(scheduler.condition_cache_filtered_rows(), 0);
+    EXPECT_EQ(ctx->base_granule, 0);
+}
+
 TEST_F(ParquetScanTest, PageIndexIntersectsMultipleFiltersAndBuildsSkipPlan) {
     write_page_index_pair_parquet_file(_file_path);
     auto parquet_file_reader = 
::parquet::ParquetFileReader::OpenFile(_file_path, false);
diff --git a/be/test/format_v2/table_reader_test.cpp 
b/be/test/format_v2/table_reader_test.cpp
index 252bfa5c04d..528da7338fd 100644
--- a/be/test/format_v2/table_reader_test.cpp
+++ b/be/test/format_v2/table_reader_test.cpp
@@ -975,9 +975,12 @@ struct FakeFileReaderState {
     int close_count = 0;
     int64_t total_rows = 2;
     int64_t aggregate_count = -1;
+    int64_t condition_cache_base_granule = 0;
+    size_t condition_cache_num_granules = 0;
     bool eof_with_first_batch = true;
     bool inject_delete_conjunct = false;
     bool stop_during_aggregate = false;
+    bool stop_during_read = false;
     bool not_found_during_init = false;
     std::shared_ptr<FileScanRequest> last_request;
     std::shared_ptr<ConditionCacheContext> condition_cache_ctx;
@@ -1068,6 +1071,10 @@ public:
             }
         }
 
+        if (_state->stop_during_read) {
+            DORIS_CHECK(_state->io_ctx != nullptr);
+            _state->io_ctx->should_stop = true;
+        }
         _returned_batch = true;
         *rows = 2;
         *eof = _state->eof_with_first_batch;
@@ -1105,6 +1112,12 @@ public:
 
     void set_condition_cache_context(std::shared_ptr<ConditionCacheContext> 
ctx) override {
         _state->condition_cache_ctx = std::move(ctx);
+        if (_state->condition_cache_ctx != nullptr && 
!_state->condition_cache_ctx->is_hit) {
+            _state->condition_cache_ctx->base_granule = 
_state->condition_cache_base_granule;
+            if (_state->condition_cache_num_granules > 0) {
+                _state->condition_cache_ctx->num_granules = 
_state->condition_cache_num_granules;
+            }
+        }
     }
 
     int64_t get_total_rows() const override { return _state->total_rows; }
@@ -1843,6 +1856,8 @@ TEST(TableReaderTest, 
ConditionCacheMissPublishesBitmapAfterReaderEof) {
     RuntimeState state {TQueryOptions(), TQueryGlobals()};
     auto fake_state = std::make_shared<FakeFileReaderState>();
     fake_state->total_rows = ConditionCacheContext::GRANULE_SIZE;
+    fake_state->condition_cache_base_granule = 7;
+    fake_state->condition_cache_num_granules = 1;
     FakeTableReader reader(file_schema, fake_state);
     ASSERT_TRUE(reader.init({
                                     .projected_columns = projected_columns,
@@ -1867,13 +1882,20 @@ TEST(TableReaderTest, 
ConditionCacheMissPublishesBitmapAfterReaderEof) {
     ASSERT_NE(fake_state->condition_cache_ctx, nullptr);
     EXPECT_FALSE(fake_state->condition_cache_ctx->is_hit);
 
-    segment_v2::ConditionCache::ExternalCacheKey 
key("fake-table-reader-input", 0, -1, 7, 0, -1);
+    segment_v2::ConditionCache::ExternalCacheKey 
legacy_key("fake-table-reader-input", 0, -1, 7, 0,
+                                                            -1);
     segment_v2::ConditionCacheHandle handle;
+    EXPECT_FALSE(cache.get()->lookup(legacy_key, &handle));
+    segment_v2::ConditionCache::ExternalCacheKey key(
+            "fake-table-reader-input", 0, -1, 7, 0, -1,
+            
segment_v2::ConditionCache::ExternalCacheKey::BASE_GRANULE_AWARE_VERSION);
     ASSERT_TRUE(cache.get()->lookup(key, &handle));
     const auto cached_bitmap = handle.get_filter_result();
     ASSERT_NE(cached_bitmap, nullptr);
     ASSERT_FALSE(cached_bitmap->empty());
+    EXPECT_EQ(cached_bitmap->size(), 1);
     EXPECT_TRUE((*cached_bitmap)[0]);
+    EXPECT_EQ(handle.get_base_granule(), 7);
 
     ASSERT_TRUE(reader.close().ok());
 }
@@ -1919,11 +1941,67 @@ TEST(TableReaderTest, 
ConditionCacheMissIsDroppedWhenReaderClosesBeforeEof) {
     EXPECT_FALSE(fake_state->condition_cache_ctx->is_hit);
 
     ASSERT_TRUE(reader.close().ok());
-    segment_v2::ConditionCache::ExternalCacheKey 
key("fake-table-reader-input", 0, -1, 7, 0, -1);
+    segment_v2::ConditionCache::ExternalCacheKey key(
+            "fake-table-reader-input", 0, -1, 7, 0, -1,
+            
segment_v2::ConditionCache::ExternalCacheKey::BASE_GRANULE_AWARE_VERSION);
     segment_v2::ConditionCacheHandle handle;
     EXPECT_FALSE(cache.get()->lookup(key, &handle));
 }
 
+// Scenario: a stop request can arrive while a physical read is in progress. 
Even if the reader
+// converts that stop into eof=true, TableReader must not publish the 
partially visited MISS bitmap.
+TEST(TableReaderTest, ConditionCacheMissIsDroppedWhenStopTurnsReadIntoEof) {
+    ScopedConditionCacheForTest cache;
+
+    std::vector<ColumnDefinition> file_schema;
+    file_schema.push_back(make_file_column(0, "id", 
std::make_shared<DataTypeInt32>()));
+
+    std::vector<ColumnDefinition> projected_columns;
+    projected_columns.push_back(make_table_column(0, "id", 
std::make_shared<DataTypeInt32>()));
+    set_name_identifiers(&projected_columns);
+
+    auto io_ctx = std::make_shared<io::IOContext>();
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    auto fake_state = std::make_shared<FakeFileReaderState>();
+    fake_state->total_rows = ConditionCacheContext::GRANULE_SIZE * 2;
+    fake_state->condition_cache_num_granules = 2;
+    fake_state->stop_during_read = true;
+    fake_state->io_ctx = io_ctx;
+    FakeTableReader reader(file_schema, fake_state);
+    ASSERT_TRUE(reader.init({
+                                    .projected_columns = projected_columns,
+                                    .conjuncts = {prepared_conjunct(
+                                            &state, 
table_int32_greater_than_expr(0, 0, 0))},
+                                    .format = FileFormat::PARQUET,
+                                    .scan_params = nullptr,
+                                    .io_ctx = io_ctx,
+                                    .runtime_state = &state,
+                                    .scanner_profile = nullptr,
+                                    .condition_cache_digest = 7,
+                            })
+                        .ok());
+
+    SplitReadOptions split_options;
+    split_options.current_range.__set_path("fake-table-reader-input");
+    ASSERT_TRUE(reader.prepare_split(split_options).ok());
+
+    Block block = build_table_block(projected_columns);
+    bool eos = false;
+    ASSERT_TRUE(reader.get_block(&block, &eos).ok());
+    EXPECT_TRUE(io_ctx->should_stop);
+    EXPECT_EQ(block.rows(), 2);
+    ASSERT_NE(fake_state->condition_cache_ctx, nullptr);
+    EXPECT_FALSE(fake_state->condition_cache_ctx->is_hit);
+
+    segment_v2::ConditionCache::ExternalCacheKey key(
+            "fake-table-reader-input", 0, -1, 7, 0, -1,
+            
segment_v2::ConditionCache::ExternalCacheKey::BASE_GRANULE_AWARE_VERSION);
+    segment_v2::ConditionCacheHandle handle;
+    EXPECT_FALSE(cache.get()->lookup(key, &handle));
+
+    ASSERT_TRUE(reader.close().ok());
+}
+
 TEST(TableReaderTest, PushDownCountFromNewParquetReader) {
     const auto test_dir = std::filesystem::temp_directory_path() / 
"doris_table_reader_count_test";
     std::filesystem::remove_all(test_dir);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to