github-actions[bot] commented on code in PR #66356:
URL: https://github.com/apache/doris/pull/66356#discussion_r3698349055


##########
be/src/exec/scan/file_scanner_v2.cpp:
##########
@@ -483,18 +485,22 @@ Status FileScannerV2::_get_block_impl(RuntimeState* 
state, Block* block, bool* e
 }
 
 Status FileScannerV2::_filter_output_block(Block* block) {
-    return 
_contextualize_output_filter_status(Scanner::_filter_output_block(block),
-                                               _get_current_format_type());
+    // TableReader is the single owner of predicates that cannot be localized, 
while FileReader is
+    // the exact owner of localized predicates. Re-evaluating either set here 
would duplicate
+    // mutable expression state and force predicate-only columns to be 
materialized for Scanner.
+    DORIS_CHECK(_conjuncts.empty());
+    (void)block;
+    return Status::OK();
 }
 
-Status FileScannerV2::_contextualize_output_filter_status(Status status,
-                                                          
TFileFormatType::type format_type) {
-    if (!status.ok() && format_type == TFileFormatType::FORMAT_ORC) {
-        // Error-preserving expressions cannot be reordered into the ORC 
reader and therefore run
-        // at the scanner boundary; keep their error context identical to ORC 
callback failures.
-        status.prepend("Orc row reader nextBatch failed. reason = ");
-    }
-    return status;
+size_t FileScannerV2::_last_block_rows_read(const Block& block) const {
+    const auto& stats = _table_reader->last_materialized_block_stats();

Review Comment:
   [P2] Account for JNI batches before falling back to survivor rows. 
`JniTableReader::get_block()` applies the now-owned (including late) conjuncts 
and loops again whenever a Java batch is fully rejected, but it never records 
`MaterializedBlockStats`. Since Scanner filtering is now a no-op, a late 
selective RF can drain the rest of a large JNI split in one scheduler turn, 
while these hooks see only zero/survivor rows and cannot enforce the row/byte 
budget or learn the pre-filter width. Please record the pre-filter JNI 
rows/bytes and yield after one rejected materialized batch, as the base 
TableReader path now does.



##########
be/src/format_v2/table_reader.cpp:
##########
@@ -1085,11 +1324,10 @@ Status 
TableReader::_evaluate_partition_prune_conjuncts(const VExprContextSPtrs&
     for (const auto& conjunct : conjuncts) {
         DORIS_CHECK(conjunct != nullptr);
         DORIS_CHECK(conjunct->root() != nullptr);
-        // Keep only the safe prefix of the original conjunct order. If an 
unsafe conjunct is
-        // skipped, a later predicate could prune the split before the unsafe 
one reaches its
-        // normal row-level evaluation point.
-        if (!_is_safe_to_pre_execute(conjunct)) {
-            break;
+        // Unsafe or non-deterministic predicates remain at TableReader. Safe 
predicates are
+        // independent pruning candidates even when an earlier predicate 
cannot be pre-executed.
+        if (!is_safe_to_pre_execute(conjunct)) {
+            continue;

Review Comment:
   [P1] Preserve the unsafe conjunct as an ordering barrier. With a partition 
value `p = BIGINT_MIN` and ordered predicates `mod(p, -1) > 0 AND p > 0`, the 
first predicate is unsafe because it must raise `INVALID_ARGUMENT`, while the 
second is safely false. Continuing here lets the second predicate prune the 
split before the modulo executes, suppressing that required error; V1 and the 
pre-change V2 path stop at the first unsafe conjunct for exactly this reason. 
Please restore the safe-prefix barrier for partition/constant pruning and later 
localization, and cover this error-preserving order.



##########
be/src/format_v2/table_reader.cpp:
##########
@@ -736,32 +741,263 @@ Status TableReader::init(TableReadOptions&& options) {
     }
     _system_properties = create_system_properties(_scan_params);
     _mapper_options.mode = TableColumnMappingMode::BY_NAME;
-    _conjuncts = std::move(options.conjuncts);
+    return _replace_conjuncts(options.conjuncts);
+}
+
+Status TableReader::_clone_conjunct(const VExprContextSPtr& source, 
VExprContextSPtr* cloned) {
+    DORIS_CHECK(source != nullptr);
+    DORIS_CHECK(source->root() != nullptr);
+    DORIS_CHECK(cloned != nullptr);
+    VExprSPtr root;
+    RETURN_IF_ERROR(clone_table_expr_tree(source->root(), &root));
+    *cloned = VExprContext::create_shared(std::move(root));
+    return Status::OK();
+}
+
+Status TableReader::_prepare_conjunct(const VExprContextSPtr& source, 
VExprContextSPtr* prepared) {
+    DORIS_CHECK(prepared != nullptr);
+    VExprContextSPtr conjunct;
+    RETURN_IF_ERROR(_clone_conjunct(source, &conjunct));

Review Comment:
   [P1] Open rebuilt predicate contexts with per-scanner function state. 
`_clone_conjunct()` creates a fresh `VExprContext`, so this `open()` uses 
`FRAGMENT_LOCAL`; however residual functions such as LIKE/REGEXP initialize 
their `LikeState` only for `THREAD_LOCAL` and then dereference it during 
execution (Java/Python UDFs have the same contract). Because LIKE is unsafe to 
localize, `WHERE s LIKE 'x%'` now reaches TableReader's residual filter after 
Scanner relinquishes the predicate and fails with missing/null function state. 
Please preserve the original fragment state and clone/open the rewritten 
context with thread-local state, and add a residual LIKE regression.



##########
be/src/format_v2/parquet/parquet_scan.cpp:
##########
@@ -2731,9 +2743,10 @@ Status ParquetScanScheduler::read_next_batch(
     };
     while (true) {
         if (!_has_current_row_group) {
+            activate_pending_scan_request_at_row_group_boundary();

Review Comment:
   [P2] Bound all-reject work after activating the refreshed request. If a late 
RF rejects the remaining row groups, `read_next_batch()` keeps growing and 
retrying zero-survivor probes until a survivor or EOF; Parquet publishes its 
raw-row delta only after that call returns, so Scanner cannot charge its row 
budget or reach cancellation checkpoints during the drain. This PR newly moves 
active-split late RFs into that loop. Please return a bounded empty progress 
unit (with consumed raw rows/bytes) and add an all-reject late-RF regression.



##########
be/src/format_v2/column_mapper.cpp:
##########
@@ -2197,27 +2235,44 @@ Status TableColumnMapper::create_scan_request(
     // 2. Build referenced predicate columns
     // Hidden filter mappings must be built before localizing filters, so that 
they can be localized together with visible mappings and referenced by 
localized filter expressions.
     RETURN_IF_ERROR(_build_hidden_filter_mappings(table_filters));
-    RETURN_IF_ERROR(localize_filters(table_filters, file_request, 
runtime_state));
-    for (const auto& mapping : _hidden_mappings) {
-        if (!mapping.file_local_id.has_value()) {
-            continue;
-        }
-        const auto local_id = LocalColumnId(*mapping.file_local_id);
-        const bool is_visible_output =
+    FilterLocalizationResult local_localization_result;
+    auto* exact_localization_result =
+            localization_result == nullptr ? &local_localization_result : 
localization_result;
+    RETURN_IF_ERROR(localize_filters(table_filters, file_request, 
runtime_state,
+                                     exact_localization_result));
+    for (const auto& predicate_column : file_request->predicate_columns) {
+        const auto local_id = predicate_column.column_id();
+        const bool value_required_after_filter =
                 std::ranges::any_of(_mappings, [local_id](const ColumnMapping& 
visible_mapping) {
                     return visible_mapping.file_local_id.has_value() &&
-                           LocalColumnId(*visible_mapping.file_local_id) == 
local_id;
+                           LocalColumnId(*visible_mapping.file_local_id) == 
local_id &&
+                           visible_mapping.value_required_after_filter;
                 });
-        if (is_visible_output) {
+        if (value_required_after_filter) {
             continue;
         }
-        // File-local filtering is an optimization; Scanner still evaluates 
the original
-        // table-level conjunct after TableReader returns. Only truly hidden 
mappings are absent
-        // from that scanner-visible block and may safely discard their 
payload here.
-        if (std::ranges::any_of(file_request->predicate_columns,
-                                [local_id](const LocalColumnIndex& projection) 
{
-                                    return projection.column_id() == local_id;
-                                }) &&
+        bool referenced_by_filter = false;
+        bool referenced_only_by_localized_filters = true;
+        for (size_t filter_index = 0; filter_index < table_filters.size(); 
++filter_index) {
+            const bool references_local_column = std::ranges::any_of(
+                    table_filters[filter_index].global_indices, 
[&](GlobalIndex global_index) {
+                        const auto* mapping = 
_find_filter_mapping(global_index);
+                        return mapping != nullptr && 
mapping->file_local_id.has_value() &&
+                               LocalColumnId(*mapping->file_local_id) == 
local_id;
+                    });
+            if (!references_local_column) {
+                continue;
+            }
+            referenced_by_filter = true;
+            referenced_only_by_localized_filters &=
+                    exact_localization_result->localized_filters[filter_index];
+        }
+        // A scan tuple can contain a filter slot that its upstream projection 
does not consume.
+        // For example, `SELECT SUM(measure) FROM t WHERE filter_key BETWEEN 1 
AND 20` needs
+        // `filter_key` while evaluating the predicate, but not afterwards. 
Discard its payload
+        // only when every predicate referencing the physical column was 
localized exactly for this
+        // split; schema evolution or a TableReader residual must retain the 
value.
+        if (referenced_by_filter && referenced_only_by_localized_filters &&

Review Comment:
   [P1] Keep predicate-only values needed by later consumers. This mapper 
decision only proves that query filters localized into FileReader; it does not 
prove Parquet can discard the value while staging them. On dictionary encoding, 
the direct path checks only same-column dictionary residuals, emits a default 
placeholder, and marks the position materialized even when 
`residual_predicate_positions` says a multi-column residual or Iceberg 
equality-delete conjunct still needs the real value. Those later predicates 
then evaluate defaults and can return wrong rows. Please gate dictionary 
predicate-only mode on the residual/delete position set (as the fixed/runtime 
direct path does) and cover both overlaps.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to