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

Gabriel39 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 2eaf314fc0d [fix](be) Rework FileScannerV2 scan accounting and stop 
handling (#65218)
2eaf314fc0d is described below

commit 2eaf314fc0d2ad81995271da806222f6ae4ee35c
Author: Gabriel <[email protected]>
AuthorDate: Tue Jul 7 14:22:50 2026 +0800

    [fix](be) Rework FileScannerV2 scan accounting and stop handling (#65218)
    
    FileScannerV2 only refreshed profile file-read counters
    in `update_realtime_counters()`, so the query IOContext counters stayed
    at zero for external file scans. Workload policies using
    `be_scan_bytes_from_remote_storage` could therefore miss remote
    Hive/Parquet reads and fail to stop queries. This PR publishes
    FileScannerV2 scan rows, scan bytes, local bytes, and remote bytes to
    the query IOContext and Doris metrics using deltas from cumulative
    reader/cache stats.
    
    The row-accounting implementation was reworked after review. The old
    approach tried to synchronize concrete `FileReader` private row
    statistics from `TableReader` after `get_block()` or `close()`. That
    abstraction was wrong because `FORMAT_JNI` overrides
    `TableReader::get_block()`, materialized readers could expose
    post-filter rows, and Parquet `COUNT(complex_col)` aggregate reads
    nested levels without going through the normal block row path.
    
    The current implementation records scan rows at the point each reader
    actually reads or materializes source rows, before file-local filters:
    
    - Parquet normal scan records the scheduler raw-row delta.
    - Parquet `COUNT(complex_col)` records the selected batch rows loaded
    through nested levels.
    - CSV/TEXT records `rows_before_filter`.
    - JSON, Native, and RemoteDoris record rows after materializing the file
    block and before applying filters.
    - JNI table readers record the `current_rows` returned by the Java
    scanner before `finalize_jni_block()`.
    
    This PR also handles `io_ctx->should_stop` in the FileScannerV2 reader
    path. `TableReader` short-circuits when the scanner stop flag is set,
    stop-time reader init/open EOF is converted to normal EOS, the Parquet
    v2 reader / Arrow `RandomAccessFile` path checks the stop flag before
    file size/read operations, and Parquet aggregate pushdown converts
    stop-time nested-level read EOF into normal scanner EOS instead of a
    scan failure.
---
 be/src/exec/scan/file_scanner_v2.cpp               | 115 +++++++++++++++++++-
 be/src/exec/scan/file_scanner_v2.h                 |  26 +++++
 .../delimited_text/delimited_text_reader.cpp       |   4 +-
 be/src/format_v2/file_reader.h                     |   8 ++
 be/src/format_v2/jni/jni_table_reader.cpp          |   1 +
 be/src/format_v2/json/json_reader.cpp              |   2 +-
 be/src/format_v2/native/native_reader.cpp          |   2 +-
 be/src/format_v2/parquet/parquet_file_context.cpp  |  16 +++
 be/src/format_v2/parquet/parquet_reader.cpp        |  52 ++++++++-
 be/src/format_v2/parquet/parquet_reader.h          |   2 +
 be/src/format_v2/parquet/parquet_scan.cpp          |   2 +
 be/src/format_v2/parquet/parquet_scan.h            |   2 +
 be/src/format_v2/table/remote_doris_reader.cpp     |   2 +-
 be/src/format_v2/table_reader.cpp                  |  20 +++-
 be/src/format_v2/table_reader.h                    |  21 +++-
 be/test/exec/scan/file_scanner_v2_test.cpp         | 117 +++++++++++++++++++++
 .../format_v2/delimited_text/csv_reader_test.cpp   |   3 +
 be/test/format_v2/parquet/parquet_scan_test.cpp    |  47 ++++++++-
 be/test/format_v2/table_reader_test.cpp            | 108 ++++++++++++++++++-
 19 files changed, 533 insertions(+), 17 deletions(-)

diff --git a/be/src/exec/scan/file_scanner_v2.cpp 
b/be/src/exec/scan/file_scanner_v2.cpp
index e3c42679a29..a65f9a74d6a 100644
--- a/be/src/exec/scan/file_scanner_v2.cpp
+++ b/be/src/exec/scan/file_scanner_v2.cpp
@@ -30,6 +30,7 @@
 #include "common/cast_set.h"
 #include "common/config.h"
 #include "common/consts.h"
+#include "common/metrics/doris_metrics.h"
 #include "common/status.h"
 #include "core/assert_cast.h"
 #include "core/block/column_with_type_and_name.h"
@@ -230,6 +231,18 @@ Status 
FileScannerV2::TEST_rewrite_slot_refs_to_global_index(
         const std::unordered_map<int32_t, format::GlobalIndex>& 
slot_id_to_global_index) {
     return rewrite_slot_refs_to_global_index(expr, slot_id_to_global_index);
 }
+
+FileScannerV2::RealtimeCounterDeltas 
FileScannerV2::TEST_collect_realtime_counter_deltas(
+        const io::FileReaderStats& file_reader_stats,
+        const io::FileCacheStatistics& file_cache_statistics,
+        UncachedReaderBytesStorage uncached_reader_bytes_storage, int64_t* 
last_read_bytes,
+        int64_t* last_read_rows, int64_t* last_bytes_read_from_local,
+        int64_t* last_bytes_read_from_remote) {
+    return _collect_realtime_counter_deltas(file_reader_stats, 
file_cache_statistics,
+                                            uncached_reader_bytes_storage, 
last_read_bytes,
+                                            last_read_rows, 
last_bytes_read_from_local,
+                                            last_bytes_read_from_remote);
+}
 #endif
 
 bool FileScannerV2::is_supported(const TFileScanRangeParams& params, const 
TFileRangeDesc& range) {
@@ -748,10 +761,110 @@ void FileScannerV2::update_realtime_counters() {
     if (_file_reader_stats == nullptr) {
         return;
     }
-    const int64_t bytes_read = _file_reader_stats->read_bytes;
+    DORIS_CHECK(_file_cache_statistics != nullptr);
+    const int64_t bytes_read = 
cast_set<int64_t>(_file_reader_stats->read_bytes);
+    auto* local_state = static_cast<FileScanLocalState*>(_local_state);
+    const auto file_type =
+            _current_range.__isset.file_type
+                    ? _current_range.file_type
+                    : (_params != nullptr && _params->__isset.file_type ? 
_params->file_type
+                                                                        : 
TFileType::FILE_LOCAL);
+    const auto deltas = _collect_realtime_counter_deltas(
+            *_file_reader_stats, *_file_cache_statistics, 
_uncached_reader_bytes_storage(file_type),
+            &_last_read_bytes, &_last_read_rows, &_last_bytes_read_from_local,
+            &_last_bytes_read_from_remote);
+
+    COUNTER_UPDATE(local_state->_scan_bytes, deltas.scan_bytes);
+    COUNTER_UPDATE(local_state->_scan_rows, deltas.scan_rows);
+
+    
_state->get_query_ctx()->resource_ctx()->io_context()->update_scan_rows(deltas.scan_rows);
+    
_state->get_query_ctx()->resource_ctx()->io_context()->update_scan_bytes(deltas.scan_bytes);
+    
_state->get_query_ctx()->resource_ctx()->io_context()->update_scan_bytes_from_local_storage(
+            deltas.scan_bytes_from_local_storage);
+    
_state->get_query_ctx()->resource_ctx()->io_context()->update_scan_bytes_from_remote_storage(
+            deltas.scan_bytes_from_remote_storage);
+
     COUNTER_SET(_file_read_bytes_counter, bytes_read);
     COUNTER_SET(_file_read_calls_counter, 
cast_set<int64_t>(_file_reader_stats->read_calls));
     COUNTER_SET(_file_read_time_counter, 
cast_set<int64_t>(_file_reader_stats->read_time_ns));
+
+    DorisMetrics::instance()->query_scan_bytes->increment(deltas.scan_bytes);
+    DorisMetrics::instance()->query_scan_rows->increment(deltas.scan_rows);
+    DorisMetrics::instance()->query_scan_bytes_from_local->increment(
+            deltas.scan_bytes_from_local_storage);
+    DorisMetrics::instance()->query_scan_bytes_from_remote->increment(
+            deltas.scan_bytes_from_remote_storage);
+}
+
+FileScannerV2::RealtimeCounterDeltas 
FileScannerV2::_collect_realtime_counter_deltas(
+        const io::FileReaderStats& file_reader_stats,
+        const io::FileCacheStatistics& file_cache_statistics,
+        UncachedReaderBytesStorage uncached_reader_bytes_storage, int64_t* 
last_read_bytes,
+        int64_t* last_read_rows, int64_t* last_bytes_read_from_local,
+        int64_t* last_bytes_read_from_remote) {
+    DORIS_CHECK(last_read_bytes != nullptr);
+    DORIS_CHECK(last_read_rows != nullptr);
+    DORIS_CHECK(last_bytes_read_from_local != nullptr);
+    DORIS_CHECK(last_bytes_read_from_remote != nullptr);
+
+    const int64_t read_bytes = cast_set<int64_t>(file_reader_stats.read_bytes);
+    const int64_t read_rows = cast_set<int64_t>(file_reader_stats.read_rows);
+    const int64_t bytes_read_from_local = 
file_cache_statistics.bytes_read_from_local;
+    const int64_t bytes_read_from_remote = 
file_cache_statistics.bytes_read_from_remote;
+    DORIS_CHECK(read_bytes >= *last_read_bytes);
+    DORIS_CHECK(read_rows >= *last_read_rows);
+    DORIS_CHECK(bytes_read_from_local >= *last_bytes_read_from_local);
+    DORIS_CHECK(bytes_read_from_remote >= *last_bytes_read_from_remote);
+
+    RealtimeCounterDeltas deltas;
+    deltas.scan_rows = read_rows - *last_read_rows;
+    deltas.scan_bytes = read_bytes - *last_read_bytes;
+    // Peer cache is a known cache source, but it is not remote object storage.
+    const bool has_cache_source_stats = 
file_cache_statistics.num_local_io_total != 0 ||
+                                        
file_cache_statistics.num_remote_io_total != 0 ||
+                                        
file_cache_statistics.num_peer_io_total != 0 ||
+                                        bytes_read_from_local != 0 || 
bytes_read_from_remote != 0 ||
+                                        
file_cache_statistics.bytes_read_from_peer != 0;
+    if (!has_cache_source_stats) {
+        switch (uncached_reader_bytes_storage) {
+        case UncachedReaderBytesStorage::LOCAL:
+            deltas.scan_bytes_from_local_storage = deltas.scan_bytes;
+            break;
+        case UncachedReaderBytesStorage::REMOTE:
+            deltas.scan_bytes_from_remote_storage = deltas.scan_bytes;
+            break;
+        case UncachedReaderBytesStorage::NONE:
+            break;
+        }
+    } else {
+        deltas.scan_bytes_from_local_storage = bytes_read_from_local - 
*last_bytes_read_from_local;
+        deltas.scan_bytes_from_remote_storage =
+                bytes_read_from_remote - *last_bytes_read_from_remote;
+    }
+
+    *last_read_bytes = read_bytes;
+    *last_read_rows = read_rows;
+    *last_bytes_read_from_local = bytes_read_from_local;
+    *last_bytes_read_from_remote = bytes_read_from_remote;
+    return deltas;
+}
+
+FileScannerV2::UncachedReaderBytesStorage 
FileScannerV2::_uncached_reader_bytes_storage(
+        TFileType::type file_type) {
+    switch (file_type) {
+    case TFileType::FILE_LOCAL:
+        return UncachedReaderBytesStorage::LOCAL;
+    case TFileType::FILE_STREAM:
+        return UncachedReaderBytesStorage::NONE;
+    case TFileType::FILE_BROKER:
+    case TFileType::FILE_S3:
+    case TFileType::FILE_HDFS:
+    case TFileType::FILE_NET:
+    case TFileType::FILE_HTTP:
+        return UncachedReaderBytesStorage::REMOTE;
+    }
+    DORIS_CHECK(false) << "unknown file type: " << file_type;
+    return UncachedReaderBytesStorage::NONE;
 }
 
 void FileScannerV2::_collect_profile_before_close() {
diff --git a/be/src/exec/scan/file_scanner_v2.h 
b/be/src/exec/scan/file_scanner_v2.h
index 7764d095885..a911d42c3ad 100644
--- a/be/src/exec/scan/file_scanner_v2.h
+++ b/be/src/exec/scan/file_scanner_v2.h
@@ -54,6 +54,15 @@ public:
     static constexpr const char* NAME = "FileScannerV2";
     static constexpr size_t ADAPTIVE_BATCH_INITIAL_PROBE_ROWS = 32;
 
+    struct RealtimeCounterDeltas {
+        int64_t scan_rows = 0;
+        int64_t scan_bytes = 0;
+        int64_t scan_bytes_from_local_storage = 0;
+        int64_t scan_bytes_from_remote_storage = 0;
+    };
+
+    enum class UncachedReaderBytesStorage { LOCAL, REMOTE, NONE };
+
     static bool is_supported(const TFileScanRangeParams& params, const 
TFileRangeDesc& range);
 #ifdef BE_TEST
     static Status TEST_to_file_format(TFileFormatType::type format_type,
@@ -65,6 +74,12 @@ public:
     static Status TEST_rewrite_slot_refs_to_global_index(
             VExprSPtr* expr,
             const std::unordered_map<int32_t, format::GlobalIndex>& 
slot_id_to_global_index);
+    static RealtimeCounterDeltas TEST_collect_realtime_counter_deltas(
+            const io::FileReaderStats& file_reader_stats,
+            const io::FileCacheStatistics& file_cache_statistics,
+            UncachedReaderBytesStorage uncached_reader_bytes_storage, int64_t* 
last_read_bytes,
+            int64_t* last_read_rows, int64_t* last_bytes_read_from_local,
+            int64_t* last_bytes_read_from_remote);
 #endif
 
     FileScannerV2(RuntimeState* state, FileScanLocalState* parent, int64_t 
limit,
@@ -113,6 +128,13 @@ private:
     bool _should_run_adaptive_batch_size() const;
     size_t _predict_reader_batch_rows();
     void _update_adaptive_batch_size(const Block& block);
+    static RealtimeCounterDeltas _collect_realtime_counter_deltas(
+            const io::FileReaderStats& file_reader_stats,
+            const io::FileCacheStatistics& file_cache_statistics,
+            UncachedReaderBytesStorage uncached_reader_bytes_storage, int64_t* 
last_read_bytes,
+            int64_t* last_read_rows, int64_t* last_bytes_read_from_local,
+            int64_t* last_bytes_read_from_remote);
+    static UncachedReaderBytesStorage 
_uncached_reader_bytes_storage(TFileType::type file_type);
     void _report_file_reader_predicate_filtered_rows();
     void _report_condition_cache_profile();
 
@@ -156,6 +178,10 @@ private:
     int64_t _reported_predicate_filtered_rows = 0;
     int64_t _reported_condition_cache_hit_count = 0;
     int64_t _reported_condition_cache_filtered_rows = 0;
+    int64_t _last_read_bytes = 0;
+    int64_t _last_read_rows = 0;
+    int64_t _last_bytes_read_from_local = 0;
+    int64_t _last_bytes_read_from_remote = 0;
 };
 
 } // namespace doris
diff --git a/be/src/format_v2/delimited_text/delimited_text_reader.cpp 
b/be/src/format_v2/delimited_text/delimited_text_reader.cpp
index f6e84b4aa77..b26e0d44456 100644
--- a/be/src/format_v2/delimited_text/delimited_text_reader.cpp
+++ b/be/src/format_v2/delimited_text/delimited_text_reader.cpp
@@ -340,6 +340,7 @@ Status DelimitedTextReader::get_block(Block* file_block, 
size_t* rows, bool* eof
 
     const size_t rows_before_filter = *rows;
     update_counter(_text_profile.rows_read_before_filter, rows_before_filter);
+    _record_scan_rows(cast_set<int64_t>(rows_before_filter));
 
     MaterializedReaderFilterProfile filter_profile;
     filter_profile.delete_conjunct_filter_time = 
_text_profile.delete_conjunct_filter_time;
@@ -350,7 +351,6 @@ Status DelimitedTextReader::get_block(Block* file_block, 
size_t* rows, bool* eof
     RETURN_IF_ERROR(apply_materialized_reader_filters(_request.get(), 
_io_ctx.get(), file_block,
                                                       rows, &filter_profile));
     update_counter(_text_profile.rows_returned, *rows);
-    _reader_statistics.read_rows += *rows;
     *eof = _line_reader_eof && *rows == 0;
     _eof = *eof;
     return Status::OK();
@@ -390,7 +390,7 @@ Status DelimitedTextReader::get_aggregate_result(const 
FileAggregateRequest& req
     result->columns.clear();
     update_counter(_text_profile.rows_read_before_filter, count);
     update_counter(_text_profile.rows_returned, count);
-    _reader_statistics.read_rows += count;
+    _record_scan_rows(count);
     _eof = true;
     return Status::OK();
 }
diff --git a/be/src/format_v2/file_reader.h b/be/src/format_v2/file_reader.h
index 57cfe3aa5ea..135df282014 100644
--- a/be/src/format_v2/file_reader.h
+++ b/be/src/format_v2/file_reader.h
@@ -24,6 +24,7 @@
 #include <utility>
 #include <vector>
 
+#include "common/cast_set.h"
 #include "common/status.h"
 #include "core/data_type/data_type.h"
 #include "core/field.h"
@@ -319,6 +320,13 @@ public:
 
 protected:
     virtual void _init_profile() {}
+    void _record_scan_rows(int64_t rows) {
+        DORIS_CHECK(rows >= 0);
+        _reader_statistics.read_rows += rows;
+        if (_io_ctx != nullptr && _io_ctx->file_reader_stats != nullptr) {
+            _io_ctx->file_reader_stats->read_rows += cast_set<size_t>(rows);
+        }
+    }
 
     io::FileReaderSPtr _file_reader;
     // _tracing_file_reader wraps _file_reader.
diff --git a/be/src/format_v2/jni/jni_table_reader.cpp 
b/be/src/format_v2/jni/jni_table_reader.cpp
index d43a22e632b..01c2dd6ecd0 100644
--- a/be/src/format_v2/jni/jni_table_reader.cpp
+++ b/be/src/format_v2/jni/jni_table_reader.cpp
@@ -83,6 +83,7 @@ Status JniTableReader::get_block(Block* output_block, bool* 
eos) {
             return Status::OK();
         }
 
+        _record_scan_rows(current_rows);
         RETURN_IF_ERROR(finalize_jni_block(&_jni_block_template, output_block, 
&current_rows));
         if (current_rows == 0) {
             output_block->clear_column_data(_projected_columns.size());
diff --git a/be/src/format_v2/json/json_reader.cpp 
b/be/src/format_v2/json/json_reader.cpp
index f0219bb7d85..313115e82ce 100644
--- a/be/src/format_v2/json/json_reader.cpp
+++ b/be/src/format_v2/json/json_reader.cpp
@@ -323,8 +323,8 @@ Status JsonReader::get_block(Block* file_block, size_t* 
rows, bool* eof) {
     }
 
     *rows = file_block->rows();
+    _record_scan_rows(cast_set<int64_t>(*rows));
     RETURN_IF_ERROR(_apply_filters(file_block, rows));
-    _reader_statistics.read_rows += *rows;
     *eof = _reader_eof && *rows == 0;
     _eof = *eof;
     return Status::OK();
diff --git a/be/src/format_v2/native/native_reader.cpp 
b/be/src/format_v2/native/native_reader.cpp
index 2a0a89f80ad..0e348f3de2c 100644
--- a/be/src/format_v2/native/native_reader.cpp
+++ b/be/src/format_v2/native/native_reader.cpp
@@ -134,8 +134,8 @@ Status NativeReader::get_block(Block* file_block, size_t* 
rows, bool* eof) {
     RETURN_IF_ERROR(source_block.deserialize(pblock, &uncompressed_bytes, 
&decompress_time));
     RETURN_IF_ERROR(_materialize_requested_columns(source_block, file_block));
     *rows = file_block->rows();
+    _record_scan_rows(cast_set<int64_t>(*rows));
     RETURN_IF_ERROR(_apply_filters(file_block, rows));
-    _reader_statistics.read_rows += *rows;
 
     if (_first_block_loaded && !_first_block_consumed) {
         _first_block_consumed = true;
diff --git a/be/src/format_v2/parquet/parquet_file_context.cpp 
b/be/src/format_v2/parquet/parquet_file_context.cpp
index dac09747408..1df3d7b353d 100644
--- a/be/src/format_v2/parquet/parquet_file_context.cpp
+++ b/be/src/format_v2/parquet/parquet_file_context.cpp
@@ -26,6 +26,7 @@
 #include <exception>
 #include <limits>
 #include <mutex>
+#include <string_view>
 #include <unordered_map>
 #include <utility>
 
@@ -36,6 +37,7 @@
 #include "io/fs/buffered_reader.h"
 #include "io/fs/file_reader.h"
 #include "io/fs/tracing_file_reader.h"
+#include "io/io_common.h"
 #include "storage/cache/page_cache.h"
 #include "util/slice.h"
 
@@ -245,6 +247,9 @@ public:
         if (!_file_reader) {
             return arrow::Status::IOError("Doris file reader is not open");
         }
+        if (_io_ctx != nullptr && _io_ctx->should_stop) {
+            return arrow::Status::IOError("stop");
+        }
         return static_cast<int64_t>(_file_reader->size());
     }
 
@@ -266,6 +271,9 @@ public:
         if (!_file_reader) {
             return arrow::Status::IOError("Doris file reader is not open");
         }
+        if (_io_ctx != nullptr && _io_ctx->should_stop) {
+            return arrow::Status::IOError("stop");
+        }
         if (position < 0 || nbytes < 0) {
             return arrow::Status::Invalid("negative read position or length");
         }
@@ -537,8 +545,16 @@ Status ParquetFileContext::open(io::FileReaderSPtr 
input_file_reader, io::IOCont
         metadata = this->file_reader->metadata();
         schema = metadata != nullptr ? metadata->schema() : nullptr;
     } catch (const ::parquet::ParquetException& e) {
+        if (io_ctx != nullptr && io_ctx->should_stop &&
+            std::string_view(e.what()).find("stop") != std::string_view::npos) 
{
+            return Status::EndOfFile("stop");
+        }
         return Status::Corruption("Failed to open parquet file: {}", e.what());
     } catch (const std::exception& e) {
+        if (io_ctx != nullptr && io_ctx->should_stop &&
+            std::string_view(e.what()).find("stop") != std::string_view::npos) 
{
+            return Status::EndOfFile("stop");
+        }
         return Status::InternalError("Failed to open parquet file: {}", 
e.what());
     }
 
diff --git a/be/src/format_v2/parquet/parquet_reader.cpp 
b/be/src/format_v2/parquet/parquet_reader.cpp
index 9c3363ae41a..82ad3f78891 100644
--- a/be/src/format_v2/parquet/parquet_reader.cpp
+++ b/be/src/format_v2/parquet/parquet_reader.cpp
@@ -26,6 +26,7 @@
 #include <utility>
 #include <vector>
 
+#include "common/cast_set.h"
 #include "core/assert_cast.h"
 #include "core/block/block.h"
 #include "core/data_type/data_type_array.h"
@@ -39,6 +40,7 @@
 #include "format_v2/parquet/parquet_scan.h"
 #include "format_v2/parquet/parquet_statistics.h"
 #include "format_v2/parquet/reader/column_reader.h"
+#include "io/io_common.h"
 #include "runtime/runtime_state.h"
 
 namespace doris::format::parquet {
@@ -318,6 +320,9 @@ 
ParquetReader::ParquetReader(std::shared_ptr<io::FileSystemProperties>& system_p
 ParquetReader::~ParquetReader() = default;
 
 Status ParquetReader::init(RuntimeState* state) {
+    if (_io_ctx != nullptr && _io_ctx->should_stop) {
+        return Status::EndOfFile("stop");
+    }
     RETURN_IF_ERROR(format::FileReader::init(state));
     if (_profile != nullptr) {
         COUNTER_UPDATE(_parquet_profile.file_reader_create_time,
@@ -466,6 +471,10 @@ Status ParquetReader::get_block(Block* file_block, size_t* 
rows, bool* eof) {
         return Status::Uninitialized("ParquetReader is not open");
     }
     *rows = 0;
+    if (_io_ctx != nullptr && _io_ctx->should_stop) {
+        *eof = true;
+        return Status::OK();
+    }
     if (_eof) {
         *eof = true;
         return Status::OK();
@@ -476,17 +485,40 @@ Status ParquetReader::get_block(Block* file_block, 
size_t* rows, bool* eof) {
     }
 
     const auto predicate_filtered_rows_before = 
_state->scheduler.predicate_filtered_rows();
-    RETURN_IF_ERROR(_state->scheduler.read_next_batch(_state->file_context, 
_state->file_schema,
-                                                      *request_snapshot, 
file_block, rows, eof));
+    const auto raw_rows_read_before = _state->scheduler.raw_rows_read();
+    Status st = _state->scheduler.read_next_batch(_state->file_context, 
_state->file_schema,
+                                                  *request_snapshot, 
file_block, rows, eof);
+    if (!st.ok()) {
+        if (_io_ctx != nullptr && _io_ctx->should_stop) {
+            *rows = 0;
+            *eof = true;
+            return Status::OK();
+        }
+        return st;
+    }
     _sync_page_cache_profile();
     if (_io_ctx != nullptr) {
         _io_ctx->predicate_filtered_rows +=
                 _state->scheduler.predicate_filtered_rows() - 
predicate_filtered_rows_before;
     }
+    const auto raw_rows_read = _state->scheduler.raw_rows_read();
+    DORIS_CHECK(raw_rows_read >= raw_rows_read_before);
+    _record_scan_rows(raw_rows_read - raw_rows_read_before);
     _eof = *eof;
     return Status::OK();
 }
 
+bool ParquetReader::_should_stop() const {
+    return _io_ctx != nullptr && _io_ctx->should_stop;
+}
+
+Status ParquetReader::_stop_status_if_requested(const Status& status) const {
+    if (!status.ok() && _should_stop()) {
+        return Status::EndOfFile("stop");
+    }
+    return status;
+}
+
 void ParquetReader::_sync_page_cache_profile() {
     if (_profile == nullptr || _state == nullptr) {
         return;
@@ -539,6 +571,9 @@ Status ParquetReader::get_aggregate_result(const 
format::FileAggregateRequest& r
         _state->file_context.schema == nullptr) {
         return Status::Uninitialized("ParquetReader is not open");
     }
+    if (_should_stop()) {
+        return Status::EndOfFile("stop");
+    }
     result->count = 0;
     result->columns.clear();
     if (request.agg_type != TPushAggOp::type::COUNT &&
@@ -569,9 +604,15 @@ Status ParquetReader::get_aggregate_result(const 
format::FileAggregateRequest& r
             try {
                 row_group = 
_state->file_context.file_reader->RowGroup(row_group_plan.row_group_id);
             } catch (const ::parquet::ParquetException& e) {
+                if (_should_stop()) {
+                    return Status::EndOfFile("stop");
+                }
                 return Status::Corruption("Failed to open parquet row group 
{}: {}",
                                           row_group_plan.row_group_id, 
e.what());
             } catch (const std::exception& e) {
+                if (_should_stop()) {
+                    return Status::EndOfFile("stop");
+                }
                 return Status::InternalError("Failed to open parquet row group 
{}: {}",
                                              row_group_plan.row_group_id, 
e.what());
             }
@@ -589,7 +630,8 @@ Status ParquetReader::get_aggregate_result(const 
format::FileAggregateRequest& r
             int64_t row_group_cursor = 0;
             for (const auto& selected_range : row_group_plan.selected_ranges) {
                 DORIS_CHECK(selected_range.start >= row_group_cursor);
-                RETURN_IF_ERROR(shape_reader->skip(selected_range.start - 
row_group_cursor));
+                RETURN_IF_ERROR(_stop_status_if_requested(
+                        shape_reader->skip(selected_range.start - 
row_group_cursor)));
                 row_group_cursor = selected_range.start;
 
                 int64_t range_rows_read = 0;
@@ -601,7 +643,9 @@ Status ParquetReader::get_aggregate_result(const 
format::FileAggregateRequest& r
                     // or values_column. MAP chooses the key leaf; 
ARRAY/STRUCT may choose a string
                     // leaf, but the levels-only protocol still avoids 
Doris-side string
                     // materialization for that leaf.
-                    
RETURN_IF_ERROR(shape_reader->load_nested_levels_batch(batch_rows));
+                    RETURN_IF_ERROR(_stop_status_if_requested(
+                            
shape_reader->load_nested_levels_batch(batch_rows)));
+                    _record_scan_rows(batch_rows);
                     result->count +=
                             count_loaded_non_null_values(root_schema, 
*shape_reader, batch_rows);
                     range_rows_read += batch_rows;
diff --git a/be/src/format_v2/parquet/parquet_reader.h 
b/be/src/format_v2/parquet/parquet_reader.h
index ff74b97a26e..6c8e88cc27a 100644
--- a/be/src/format_v2/parquet/parquet_reader.h
+++ b/be/src/format_v2/parquet/parquet_reader.h
@@ -76,6 +76,8 @@ protected:
 
 private:
     void _sync_page_cache_profile();
+    bool _should_stop() const;
+    Status _stop_status_if_requested(const Status& status) const;
 
     void _fill_column_definition(const ParquetColumnSchema& column_schema,
                                  format::ColumnDefinition* field) const;
diff --git a/be/src/format_v2/parquet/parquet_scan.cpp 
b/be/src/format_v2/parquet/parquet_scan.cpp
index 0e1b546e5c6..86de8be9f92 100644
--- a/be/src/format_v2/parquet/parquet_scan.cpp
+++ b/be/src/format_v2/parquet/parquet_scan.cpp
@@ -454,6 +454,7 @@ void 
ParquetScanScheduler::set_condition_cache_context(std::shared_ptr<Condition
 
 void ParquetScanScheduler::reset() {
     _next_row_group_plan_idx = 0;
+    _raw_rows_read = 0;
     reset_current_row_group();
 }
 
@@ -679,6 +680,7 @@ Status ParquetScanScheduler::read_current_row_group_batch(
     if (_scan_profile.raw_rows_read != nullptr) {
         COUNTER_UPDATE(_scan_profile.raw_rows_read, batch_rows);
     }
+    _raw_rows_read += batch_rows;
     if (_current_predicate_columns.empty() && 
_current_non_predicate_columns.empty()) {
         *rows = static_cast<size_t>(batch_rows);
         if (_scan_profile.selected_rows != nullptr) {
diff --git a/be/src/format_v2/parquet/parquet_scan.h 
b/be/src/format_v2/parquet/parquet_scan.h
index e8e12ea3f66..1fb313d4f75 100644
--- a/be/src/format_v2/parquet/parquet_scan.h
+++ b/be/src/format_v2/parquet/parquet_scan.h
@@ -132,6 +132,7 @@ public:
     bool empty() const { return _row_group_plans.empty(); }
     int64_t condition_cache_filtered_rows() const { return 
_condition_cache_filtered_rows; }
     int64_t predicate_filtered_rows() const { return _predicate_filtered_rows; 
}
+    int64_t raw_rows_read() const { return _raw_rows_read; }
 
     Status read_next_batch(ParquetFileContext& file_context,
                            const 
std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
@@ -201,6 +202,7 @@ private:
     std::shared_ptr<ConditionCacheContext> _condition_cache_ctx;
     int64_t _condition_cache_filtered_rows = 0;
     int64_t _predicate_filtered_rows = 0;
+    int64_t _raw_rows_read = 0;
 };
 
 } // namespace doris::format::parquet
diff --git a/be/src/format_v2/table/remote_doris_reader.cpp 
b/be/src/format_v2/table/remote_doris_reader.cpp
index 39580fd2561..c67cece6e05 100644
--- a/be/src/format_v2/table/remote_doris_reader.cpp
+++ b/be/src/format_v2/table/remote_doris_reader.cpp
@@ -231,9 +231,9 @@ Status RemoteDorisFileReader::get_block(Block* file_block, 
size_t* rows, bool* e
     }
 
     RETURN_IF_ERROR(_materialize_record_batch(*batch, file_block, rows));
+    _record_scan_rows(cast_set<int64_t>(*rows));
     RETURN_IF_ERROR(
             apply_materialized_reader_filters(_request.get(), _io_ctx.get(), 
file_block, rows));
-    _reader_statistics.read_rows += *rows;
     return Status::OK();
 }
 
diff --git a/be/src/format_v2/table_reader.cpp 
b/be/src/format_v2/table_reader.cpp
index cee93f5eb30..11317c5fd7b 100644
--- a/be/src/format_v2/table_reader.cpp
+++ b/be/src/format_v2/table_reader.cpp
@@ -672,8 +672,24 @@ Status TableReader::create_next_reader(bool* eos) {
     if (_batch_size > 0) {
         _data_reader.reader->set_batch_size(_batch_size);
     }
-    RETURN_IF_ERROR(_data_reader.reader->init(_runtime_state));
-    RETURN_IF_ERROR(open_reader());
+    Status st = _data_reader.reader->init(_runtime_state);
+    if (!st.ok()) {
+        if (_io_ctx != nullptr && _io_ctx->should_stop && 
st.is<ErrorCode::END_OF_FILE>()) {
+            *eos = true;
+            _data_reader.reader.reset();
+            return Status::OK();
+        }
+        return st;
+    }
+    st = open_reader();
+    if (!st.ok()) {
+        if (_io_ctx != nullptr && _io_ctx->should_stop && 
st.is<ErrorCode::END_OF_FILE>()) {
+            *eos = true;
+            _data_reader.reader.reset();
+            return Status::OK();
+        }
+        return st;
+    }
     if (_data_reader.reader == nullptr) {
         *eos = _current_task == nullptr;
         return Status::OK();
diff --git a/be/src/format_v2/table_reader.h b/be/src/format_v2/table_reader.h
index 4f117632277..66b7eddf720 100644
--- a/be/src/format_v2/table_reader.h
+++ b/be/src/format_v2/table_reader.h
@@ -62,6 +62,7 @@
 #include "format_v2/parquet/reader/column_reader.h"
 #include "format_v2/schema_projection.h"
 #include "gen_cpp/PlanNodes_types.h"
+#include "io/io_common.h"
 #include "runtime/descriptors.h"
 #include "storage/segment/condition_cache.h"
 
@@ -179,6 +180,10 @@ public:
             if (*eos) {
                 return Status::OK();
             }
+            if (_io_ctx != nullptr && _io_ctx->should_stop) {
+                *eos = true;
+                return Status::OK();
+            }
             if (!_data_reader.reader) {
                 if (_is_table_level_count_active()) {
                     RETURN_IF_ERROR(_read_table_level_count(block, eos));
@@ -198,7 +203,15 @@ public:
             if (!_aggregate_pushdown_tried) {
                 SCOPED_TIMER(_profile.pushdown_agg_timer);
                 bool pushed_down = false;
-                
RETURN_IF_ERROR(_try_materialize_aggregate_pushdown_rows(block, &pushed_down));
+                const auto status = 
_try_materialize_aggregate_pushdown_rows(block, &pushed_down);
+                if (!status.ok()) {
+                    if (_io_ctx != nullptr && _io_ctx->should_stop &&
+                        status.is<ErrorCode::END_OF_FILE>()) {
+                        *eos = true;
+                        return Status::OK();
+                    }
+                    return status;
+                }
                 if (pushed_down) {
                     return Status::OK();
                 }
@@ -566,6 +579,12 @@ protected:
         return Status::OK();
     }
 
+    void _record_scan_rows(size_t rows) {
+        if (_io_ctx != nullptr && _io_ctx->file_reader_stats != nullptr) {
+            _io_ctx->file_reader_stats->read_rows += rows;
+        }
+    }
+
     // Finalize file-local block to table/global schema block.
     Status finalize_chunk(Block* block, const size_t rows) {
         SCOPED_TIMER(_profile.finalize_timer);
diff --git a/be/test/exec/scan/file_scanner_v2_test.cpp 
b/be/test/exec/scan/file_scanner_v2_test.cpp
index 436a18c66de..9eb71301801 100644
--- a/be/test/exec/scan/file_scanner_v2_test.cpp
+++ b/be/test/exec/scan/file_scanner_v2_test.cpp
@@ -222,6 +222,123 @@ TEST(FileScannerV2Test, FileFormatConversionMatrix) {
     }
 }
 
+TEST(FileScannerV2Test, 
RealtimeCounterDeltasUseReaderBytesAsRemoteWithoutCacheStats) {
+    io::FileReaderStats file_reader_stats;
+    io::FileCacheStatistics file_cache_statistics;
+    int64_t last_read_bytes = 0;
+    int64_t last_read_rows = 0;
+    int64_t last_bytes_read_from_local = 0;
+    int64_t last_bytes_read_from_remote = 0;
+
+    file_reader_stats.read_bytes = 100;
+    file_reader_stats.read_rows = 7;
+    auto deltas = FileScannerV2::TEST_collect_realtime_counter_deltas(
+            file_reader_stats, file_cache_statistics,
+            FileScannerV2::UncachedReaderBytesStorage::REMOTE, 
&last_read_bytes, &last_read_rows,
+            &last_bytes_read_from_local, &last_bytes_read_from_remote);
+    EXPECT_EQ(7, deltas.scan_rows);
+    EXPECT_EQ(100, deltas.scan_bytes);
+    EXPECT_EQ(0, deltas.scan_bytes_from_local_storage);
+    EXPECT_EQ(100, deltas.scan_bytes_from_remote_storage);
+
+    deltas = FileScannerV2::TEST_collect_realtime_counter_deltas(
+            file_reader_stats, file_cache_statistics,
+            FileScannerV2::UncachedReaderBytesStorage::REMOTE, 
&last_read_bytes, &last_read_rows,
+            &last_bytes_read_from_local, &last_bytes_read_from_remote);
+    EXPECT_EQ(0, deltas.scan_rows);
+    EXPECT_EQ(0, deltas.scan_bytes);
+    EXPECT_EQ(0, deltas.scan_bytes_from_local_storage);
+    EXPECT_EQ(0, deltas.scan_bytes_from_remote_storage);
+
+    file_reader_stats.read_bytes = 160;
+    file_reader_stats.read_rows = 9;
+    deltas = FileScannerV2::TEST_collect_realtime_counter_deltas(
+            file_reader_stats, file_cache_statistics,
+            FileScannerV2::UncachedReaderBytesStorage::REMOTE, 
&last_read_bytes, &last_read_rows,
+            &last_bytes_read_from_local, &last_bytes_read_from_remote);
+    EXPECT_EQ(2, deltas.scan_rows);
+    EXPECT_EQ(60, deltas.scan_bytes);
+    EXPECT_EQ(0, deltas.scan_bytes_from_local_storage);
+    EXPECT_EQ(60, deltas.scan_bytes_from_remote_storage);
+}
+
+TEST(FileScannerV2Test, RealtimeCounterDeltasUseFileCacheDeltasWhenAvailable) {
+    io::FileReaderStats file_reader_stats;
+    io::FileCacheStatistics file_cache_statistics;
+    int64_t last_read_bytes = 0;
+    int64_t last_read_rows = 0;
+    int64_t last_bytes_read_from_local = 0;
+    int64_t last_bytes_read_from_remote = 0;
+
+    file_reader_stats.read_bytes = 100;
+    file_reader_stats.read_rows = 7;
+    file_cache_statistics.bytes_read_from_local = 30;
+    file_cache_statistics.bytes_read_from_remote = 70;
+    auto deltas = FileScannerV2::TEST_collect_realtime_counter_deltas(
+            file_reader_stats, file_cache_statistics,
+            FileScannerV2::UncachedReaderBytesStorage::REMOTE, 
&last_read_bytes, &last_read_rows,
+            &last_bytes_read_from_local, &last_bytes_read_from_remote);
+    EXPECT_EQ(7, deltas.scan_rows);
+    EXPECT_EQ(100, deltas.scan_bytes);
+    EXPECT_EQ(30, deltas.scan_bytes_from_local_storage);
+    EXPECT_EQ(70, deltas.scan_bytes_from_remote_storage);
+
+    file_reader_stats.read_bytes = 125;
+    file_reader_stats.read_rows = 10;
+    file_cache_statistics.bytes_read_from_local = 35;
+    file_cache_statistics.bytes_read_from_remote = 90;
+    deltas = FileScannerV2::TEST_collect_realtime_counter_deltas(
+            file_reader_stats, file_cache_statistics,
+            FileScannerV2::UncachedReaderBytesStorage::REMOTE, 
&last_read_bytes, &last_read_rows,
+            &last_bytes_read_from_local, &last_bytes_read_from_remote);
+    EXPECT_EQ(3, deltas.scan_rows);
+    EXPECT_EQ(25, deltas.scan_bytes);
+    EXPECT_EQ(5, deltas.scan_bytes_from_local_storage);
+    EXPECT_EQ(20, deltas.scan_bytes_from_remote_storage);
+}
+
+TEST(FileScannerV2Test, 
RealtimeCounterDeltasDoNotChargePeerCacheAsRemoteStorage) {
+    io::FileReaderStats file_reader_stats;
+    io::FileCacheStatistics file_cache_statistics;
+    int64_t last_read_bytes = 0;
+    int64_t last_read_rows = 0;
+    int64_t last_bytes_read_from_local = 0;
+    int64_t last_bytes_read_from_remote = 0;
+
+    file_reader_stats.read_bytes = 100;
+    file_reader_stats.read_rows = 7;
+    file_cache_statistics.num_peer_io_total = 1;
+    file_cache_statistics.bytes_read_from_peer = 100;
+    auto deltas = FileScannerV2::TEST_collect_realtime_counter_deltas(
+            file_reader_stats, file_cache_statistics,
+            FileScannerV2::UncachedReaderBytesStorage::REMOTE, 
&last_read_bytes, &last_read_rows,
+            &last_bytes_read_from_local, &last_bytes_read_from_remote);
+    EXPECT_EQ(7, deltas.scan_rows);
+    EXPECT_EQ(100, deltas.scan_bytes);
+    EXPECT_EQ(0, deltas.scan_bytes_from_local_storage);
+    EXPECT_EQ(0, deltas.scan_bytes_from_remote_storage);
+}
+
+TEST(FileScannerV2Test, 
RealtimeCounterDeltasDoNotChargeLocalFileFallbackAsRemoteStorage) {
+    io::FileReaderStats file_reader_stats;
+    io::FileCacheStatistics file_cache_statistics;
+    int64_t last_read_bytes = 0;
+    int64_t last_read_rows = 0;
+    int64_t last_bytes_read_from_local = 0;
+    int64_t last_bytes_read_from_remote = 0;
+
+    file_reader_stats.read_bytes = 100;
+    file_reader_stats.read_rows = 7;
+    auto deltas = FileScannerV2::TEST_collect_realtime_counter_deltas(
+            file_reader_stats, file_cache_statistics,
+            FileScannerV2::UncachedReaderBytesStorage::LOCAL, 
&last_read_bytes, &last_read_rows,
+            &last_bytes_read_from_local, &last_bytes_read_from_remote);
+    EXPECT_EQ(7, deltas.scan_rows);
+    EXPECT_EQ(100, deltas.scan_bytes);
+    EXPECT_EQ(100, deltas.scan_bytes_from_local_storage);
+    EXPECT_EQ(0, deltas.scan_bytes_from_remote_storage);
+}
+
 // Scenario: partition slots are identified from the explicit FE category when 
present, otherwise
 // from the legacy is_file_slot flag. Scanner-generated rowid columns must 
never be treated as
 // partition columns even if FE marks them as non-file slots.
diff --git a/be/test/format_v2/delimited_text/csv_reader_test.cpp 
b/be/test/format_v2/delimited_text/csv_reader_test.cpp
index 7c787de7f8c..d5eafec7142 100644
--- a/be/test/format_v2/delimited_text/csv_reader_test.cpp
+++ b/be/test/format_v2/delimited_text/csv_reader_test.cpp
@@ -410,7 +410,9 @@ TEST_F(CsvV2ReaderTest, 
ProfileCountersTrackReadParseDeserializeAndFilter) {
     output.close();
 
     _state._query_options.__set_read_csv_empty_line_as_null(true);
+    io::FileReaderStats file_reader_stats;
     auto io_ctx = std::make_shared<io::IOContext>();
+    io_ctx->file_reader_stats = &file_reader_stats;
     auto reader = create_reader(profile_path, &_params, _slots, &_state, 
&_profile, 0, -1,
                                 TFileCompressType::UNKNOWN, io_ctx);
     std::vector<ColumnDefinition> schema;
@@ -443,6 +445,7 @@ TEST_F(CsvV2ReaderTest, 
ProfileCountersTrackReadParseDeserializeAndFilter) {
     EXPECT_EQ(counter_value(&_profile, "RowsReadBeforeFilter"), 3);
     EXPECT_EQ(counter_value(&_profile, "RowsFilteredByConjunct"), 2);
     EXPECT_EQ(io_ctx->predicate_filtered_rows, 2);
+    EXPECT_EQ(file_reader_stats.read_rows, 3);
     EXPECT_EQ(counter_value(&_profile, "RowsFilteredByDeleteConjunct"), 0);
     EXPECT_EQ(counter_value(&_profile, "RowsReturned"), 1);
     EXPECT_EQ(counter_value(&_profile, "EmptyLinesRead"), 1);
diff --git a/be/test/format_v2/parquet/parquet_scan_test.cpp 
b/be/test/format_v2/parquet/parquet_scan_test.cpp
index 339ccfb27fc..8c1ac915755 100644
--- a/be/test/format_v2/parquet/parquet_scan_test.cpp
+++ b/be/test/format_v2/parquet/parquet_scan_test.cpp
@@ -355,7 +355,8 @@ protected:
     std::unique_ptr<format::parquet::ParquetReader> create_reader(
             int64_t range_start_offset = 0, int64_t range_size = -1,
             RuntimeProfile* profile = nullptr,
-            std::optional<format::GlobalRowIdContext> global_rowid_context = 
std::nullopt) const {
+            std::optional<format::GlobalRowIdContext> global_rowid_context = 
std::nullopt,
+            std::shared_ptr<io::IOContext> io_ctx = nullptr) const {
         auto system_properties = std::make_shared<io::FileSystemProperties>();
         system_properties->system_type = TFileType::FILE_LOCAL;
         auto file_description = std::make_unique<io::FileDescription>();
@@ -363,8 +364,9 @@ protected:
         file_description->file_size = 
static_cast<int64_t>(std::filesystem::file_size(_file_path));
         file_description->range_start_offset = range_start_offset;
         file_description->range_size = range_size;
-        return std::make_unique<format::parquet::ParquetReader>(
-                system_properties, file_description, nullptr, profile, 
global_rowid_context);
+        return 
std::make_unique<format::parquet::ParquetReader>(system_properties, 
file_description,
+                                                                
std::move(io_ctx), profile,
+                                                                
global_rowid_context);
     }
 
     std::shared_ptr<format::FileScanRequest> open_all_row_groups(
@@ -676,6 +678,45 @@ TEST_F(ParquetScanTest, 
AggregateMinMaxSupportsNestedSingleLeafProjection) {
     EXPECT_EQ(result.columns[0].max_value.get<TYPE_INT>(), 11);
 }
 
+TEST_F(ParquetScanTest, AggregateCountOnStructRecordsSelectedRowsRead) {
+    write_struct_parquet_file(_file_path);
+    io::FileReaderStats file_reader_stats;
+    auto io_ctx = std::make_shared<io::IOContext>();
+    io_ctx->file_reader_stats = &file_reader_stats;
+    auto reader = create_reader(0, -1, nullptr, std::nullopt, io_ctx);
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    ASSERT_TRUE(reader->init(&state).ok());
+    open_all_row_groups(reader.get());
+
+    format::FileAggregateRequest aggregate_request;
+    aggregate_request.agg_type = TPushAggOp::COUNT;
+    aggregate_request.columns.push_back({.projection = field_projection(0)});
+    format::FileAggregateResult result;
+    ASSERT_TRUE(reader->get_aggregate_result(aggregate_request, &result).ok());
+    EXPECT_EQ(result.count, 4);
+    EXPECT_EQ(file_reader_stats.read_rows, 4);
+}
+
+TEST_F(ParquetScanTest, AggregateCountOnStructReturnsEndOfFileWhenStopped) {
+    write_struct_parquet_file(_file_path);
+    io::FileReaderStats file_reader_stats;
+    auto io_ctx = std::make_shared<io::IOContext>();
+    io_ctx->file_reader_stats = &file_reader_stats;
+    auto reader = create_reader(0, -1, nullptr, std::nullopt, io_ctx);
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    ASSERT_TRUE(reader->init(&state).ok());
+    open_all_row_groups(reader.get());
+    io_ctx->should_stop = true;
+
+    format::FileAggregateRequest aggregate_request;
+    aggregate_request.agg_type = TPushAggOp::COUNT;
+    aggregate_request.columns.push_back({.projection = field_projection(0)});
+    format::FileAggregateResult result;
+    const auto status = reader->get_aggregate_result(aggregate_request, 
&result);
+    EXPECT_TRUE(status.is<ErrorCode::END_OF_FILE>()) << status;
+    EXPECT_EQ(file_reader_stats.read_rows, 0);
+}
+
 TEST_F(ParquetScanTest, 
AggregateRejectsRepeatedMissingStatisticsAndInvalidRequests) {
     write_list_parquet_file(_file_path);
     auto repeated_reader = create_reader();
diff --git a/be/test/format_v2/table_reader_test.cpp 
b/be/test/format_v2/table_reader_test.cpp
index e2015768e41..b1db95b135e 100644
--- a/be/test/format_v2/table_reader_test.cpp
+++ b/be/test/format_v2/table_reader_test.cpp
@@ -969,10 +969,13 @@ struct FakeFileReaderState {
     int open_count = 0;
     int close_count = 0;
     int64_t total_rows = 2;
+    int64_t aggregate_count = -1;
     bool eof_with_first_batch = true;
     bool inject_delete_conjunct = false;
+    bool stop_during_aggregate = false;
     std::shared_ptr<FileScanRequest> last_request;
     std::shared_ptr<ConditionCacheContext> condition_cache_ctx;
+    std::shared_ptr<io::IOContext> io_ctx;
 };
 
 class FakeFileReader final : public FileReader {
@@ -980,7 +983,7 @@ public:
     FakeFileReader(std::shared_ptr<io::FileSystemProperties>& 
system_properties,
                    std::unique_ptr<io::FileDescription>& file_description,
                    std::vector<ColumnDefinition> schema, 
std::shared_ptr<FakeFileReaderState> state)
-            : FileReader(system_properties, file_description, nullptr, 
nullptr),
+            : FileReader(system_properties, file_description, state->io_ctx, 
nullptr),
               _schema(std::move(schema)),
               _state(std::move(state)) {}
 
@@ -1070,6 +1073,27 @@ public:
         return Status::OK();
     }
 
+    Status get_aggregate_result(const FileAggregateRequest& request,
+                                FileAggregateResult* result) override {
+        DORIS_CHECK(result != nullptr);
+        if (_state->aggregate_count < 0) {
+            return FileReader::get_aggregate_result(request, result);
+        }
+        if (request.agg_type != TPushAggOp::type::COUNT) {
+            return Status::NotSupported("fake reader only supports COUNT 
aggregate pushdown");
+        }
+        if (_state->stop_during_aggregate) {
+            DORIS_CHECK(_state->io_ctx != nullptr);
+            _state->io_ctx->should_stop = true;
+            return Status::EndOfFile("stop");
+        }
+        result->count = _state->aggregate_count;
+        result->columns.clear();
+        _record_scan_rows(_state->aggregate_count);
+        _eof = true;
+        return Status::OK();
+    }
+
     void set_condition_cache_context(std::shared_ptr<ConditionCacheContext> 
ctx) override {
         _state->condition_cache_ctx = std::move(ctx);
     }
@@ -1200,6 +1224,88 @@ TEST(TableReaderTest, 
CanUseInjectedFileReaderForStandaloneUnitTest) {
     EXPECT_TRUE(eos);
 }
 
+TEST(TableReaderTest, PushDownCountRecordsReaderRowsBeforeClosingReader) {
+    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);
+
+    io::FileReaderStats file_reader_stats;
+    auto io_ctx = std::make_shared<io::IOContext>();
+    io_ctx->file_reader_stats = &file_reader_stats;
+
+    RuntimeState state {TQueryOptions(), TQueryGlobals()};
+    auto fake_state = std::make_shared<FakeFileReaderState>();
+    fake_state->aggregate_count = 3;
+    fake_state->io_ctx = io_ctx;
+    FakeTableReader reader(file_schema, fake_state);
+    ASSERT_TRUE(reader.init({
+                                    .projected_columns = projected_columns,
+                                    .conjuncts = {},
+                                    .format = FileFormat::PARQUET,
+                                    .scan_params = nullptr,
+                                    .io_ctx = io_ctx,
+                                    .runtime_state = &state,
+                                    .scanner_profile = nullptr,
+                                    .push_down_agg_type = 
TPushAggOp::type::COUNT,
+                            })
+                        .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_FALSE(eos);
+    EXPECT_EQ(block.rows(), 3);
+    EXPECT_EQ(file_reader_stats.read_rows, 3);
+    EXPECT_EQ(fake_state->close_count, 1);
+}
+
+TEST(TableReaderTest, PushDownCountStopConvertsAggregateEndOfFileToEos) {
+    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->aggregate_count = 3;
+    fake_state->io_ctx = io_ctx;
+    fake_state->stop_during_aggregate = true;
+    FakeTableReader reader(file_schema, fake_state);
+    ASSERT_TRUE(reader.init({
+                                    .projected_columns = projected_columns,
+                                    .conjuncts = {},
+                                    .format = FileFormat::PARQUET,
+                                    .scan_params = nullptr,
+                                    .io_ctx = io_ctx,
+                                    .runtime_state = &state,
+                                    .scanner_profile = nullptr,
+                                    .push_down_agg_type = 
TPushAggOp::type::COUNT,
+                            })
+                        .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(eos);
+    EXPECT_EQ(block.rows(), 0);
+    EXPECT_EQ(fake_state->close_count, 0);
+}
+
 TEST(TableReaderTest, DebugStringCoversReaderStateAndEnumNames) {
     std::vector<ColumnDefinition> file_schema;
     file_schema.push_back(make_file_column(0, "id", 
std::make_shared<DataTypeInt32>()));


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

Reply via email to