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


##########
be/src/format_v2/table_reader.cpp:
##########
@@ -736,35 +741,272 @@ 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));
+    RETURN_IF_ERROR(conjunct->prepare(_runtime_state, RowDescriptor {}));
+    RETURN_IF_ERROR(conjunct->open(_runtime_state));
+    *prepared = std::move(conjunct);
     return Status::OK();
 }
 
+Status TableReader::_replace_conjuncts(const VExprContextSPtrs& conjuncts) {
+    VExprContextSPtrs prepared;
+    prepared.reserve(conjuncts.size());
+    for (size_t conjunct_index = 0; conjunct_index < conjuncts.size(); 
++conjunct_index) {
+        VExprContextSPtr conjunct;
+        if (conjunct_index < _table_reader_owned_conjunct_count) {
+            RETURN_IF_ERROR(_prepare_conjunct(conjuncts[conjunct_index], 
&conjunct));
+        } else {
+            // Scanner-owned suffixes are cloned only for pruning analysis; 
preparing them here
+            // would duplicate expression state that must remain exclusively 
in Scanner.
+            RETURN_IF_ERROR(_clone_conjunct(conjuncts[conjunct_index], 
&conjunct));
+        }
+        prepared.push_back(std::move(conjunct));
+    }
+    _conjuncts = std::move(prepared);
+    return Status::OK();
+}
+
+Status TableReader::append_conjuncts_with_ownership(const VExprContextSPtrs& 
conjuncts,
+                                                    size_t 
table_reader_owned_conjunct_count) {
+    DORIS_CHECK(!_appended_table_reader_owned_conjunct_count.has_value());
+    _appended_table_reader_owned_conjunct_count = 
table_reader_owned_conjunct_count;
+    auto status = append_conjuncts(conjuncts);
+    _appended_table_reader_owned_conjunct_count.reset();
+    return status;
+}
+
+Status TableReader::append_conjuncts(const VExprContextSPtrs& conjuncts) {
+    const size_t owned_count =
+            
_appended_table_reader_owned_conjunct_count.value_or(conjuncts.size());
+    DORIS_CHECK_LE(owned_count, conjuncts.size());
+    // Once Scanner owns a suffix, later predicates cannot be inserted into 
the TableReader-owned
+    // prefix without reordering them ahead of that stateful/error-preserving 
barrier.
+    DORIS_CHECK(owned_count == 0 || _table_reader_owned_conjunct_count == 
_conjuncts.size());
+    for (size_t conjunct_index = 0; conjunct_index < conjuncts.size(); 
++conjunct_index) {
+        const auto& source = conjuncts[conjunct_index];
+        VExprContextSPtr conjunct;
+        if (conjunct_index < owned_count) {
+            RETURN_IF_ERROR(_prepare_conjunct(source, &conjunct));
+        } else {
+            // Preserve Scanner as the sole owner of runtime state for 
appended residuals.
+            RETURN_IF_ERROR(_clone_conjunct(source, &conjunct));
+        }
+        _conjuncts.push_back(conjunct);
+        if (conjunct_index < owned_count) {
+            ++_table_reader_owned_conjunct_count;
+        }
+        if (_current_task != nullptr && conjunct_index < owned_count) {
+            // Keep late predicates residual for this split even after a 
refreshed request is
+            // queued: the physical reader may not activate it until the next 
row-group boundary.
+            _remaining_conjuncts.push_back(std::move(conjunct));

Review Comment:
   [P1] Preserve late-filter values until ownership transfers
   
   This appends the late RF to TableReader residuals immediately, but the 
active Parquet request may already have marked its slot predicate-only and 
replaced its values with defaults. The new residual then filters those 
placeholders instead of the real values; after the next row-group boundary, the 
refreshed request can still be predicate-only while this residual remains, so 
valid rows continue to be dropped. Please keep every possible late-RF slot 
materialized, or defer this residual until the reader acknowledges activation 
and retire it atomically when file-level ownership begins.



##########
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 values needed by later file-local stages
   
   `referenced_only_by_localized_filters` only says each table filter moved 
into the file request; it does not mean this column has no later consumer. With 
dictionary-encoded Parquet, `id > 1` can run as the single-column dictionary 
stage and, because `id` is predicate-only here, replace surviving IDs with 
defaults. A later localized `id + other > 10` stage then evaluates those 
defaults. Iceberg has the same problem because `customize_file_scan_request()` 
appends equality-delete conjuncts only after this flag is computed, allowing 
deleted rows to survive when their key is defaulted. Please derive 
predicate-only status from all request consumers/stages, including delete 
conjuncts, or defer payload destruction until the last such consumer has run.



-- 
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