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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/processor/post/TopnFilterPushDownVisitor.java:
##########
@@ -108,9 +136,46 @@ public Boolean visitPhysicalProject(
         return project.child().accept(this, ctxProjectProbeExpr);
     }
 
+    private static boolean hasEvaluationSensitiveExpressions(Plan plan) {
+        return plan.getExpressions().stream().anyMatch(expression -> 
!safeOnReducedInput(expression));
+    }
+
+    private static boolean safeOnReducedInput(Expression expression) {
+        // Determinism and monotonicity do not imply total evaluation: 
from_second(-1), for example,
+        // throws. Keep a closed set of proven-total expressions instead of 
enumerating throwing
+        // functions. Unknown functions, casts and UDFs must preserve their 
original evaluation domain.
+        if (expression instanceof Slot || expression instanceof Literal) {
+            return true;
+        }
+        boolean safeNode = expression instanceof Alias || expression 
instanceof Nullable
+                || expression instanceof And || expression instanceof Or || 
expression instanceof Not
+                || expression instanceof IsNull;
+        if (expression instanceof EqualTo || expression instanceof 
NullSafeEqual
+                || expression instanceof LessThan || expression instanceof 
LessThanEqual
+                || expression instanceof GreaterThan || expression instanceof 
GreaterThanEqual
+                || expression instanceof InPredicate) {
+            safeNode = expression.children().stream().allMatch(child -> {
+                DataType type = child.getDataType();
+                return type.isNumericType() || type.isBooleanType() || 
type.isStringLikeType()
+                        || type.isDateLikeType() || type.isTimeType() || 
type.isIPType()
+                        || type.isUuidType() || type.isNullType();
+            });
+        } else if (expression instanceof Substring) {
+            safeNode = expression.children().subList(1, 
expression.arity()).stream()
+                    .allMatch(Literal.class::isInstance);
+        } else if (expression instanceof DateAddSubMonotonic) {
+            // Zero intervals are identities on already-typed dates; nonzero 
arithmetic can overflow.
+            safeNode = 
!ExpressionUtils.containsUnsafeDateArithmetic(expression);
+        }
+        return safeNode && 
expression.children().stream().allMatch(TopnFilterPushDownVisitor::safeOnReducedInput);
+    }
+
     @Override
     public Boolean visitPhysicalSetOperation(
             PhysicalSetOperation setOperation, PushDownContext ctx) {
+        if (hasEvaluationSensitiveExpressions(setOperation)) {

Review Comment:
   [P2] Check sensitive siblings before reducing a set build input
   
   The set operator's `getExpressions()` contains output slots, so this check 
does not cover expressions in its later inputs. Those inputs can still be 
reduced indirectly: 
`RuntimeFilterGenerator.generateRuntimeFilterForSetOperation` builds a filter 
from child 0 into later children, and runtime-filter pushdown crosses a project 
without checking its unrelated outputs. A reduced triggering tree is:
   
   ```text
   TopN(k ASC, v ASC LIMIT 1)
     UnionAll
       Safe(k, v)
       Intersect
         ParquetScan A(k INT, v DATETIMEV2(6))
         Project(k, from_microsecond(ts) AS v)
           Scan B(k INT, ts BIGINT)
   ```
   
   On one BE, preserve this tree by disabling `PUSH_DOWN_TOP_N_THROUGH_UNION`, 
`REORDER_INTERSECT`, and `INFER_SET_OPERATOR_DISTINCT`. Use a high 
`topn_filter_ratio`, `enable_runtime_filter_prune=false`, 
`runtime_filter_type=1`, and `runtime_filter_wait_infinitely=true`; keep A 
below the set-filter build-row threshold. The set filter targets the first 
nonuniform output `k`. A's `v` is a matching nullable Parquet 
TIMESTAMP(MICROS), so its mappings are trivial. Put `k=100` in physical RG0 and 
full `k=0` groups later; B includes `(100,-1)`. Choose the reachable 
interleaving where A returns its first block before Safe supplies the outer 
bound `k=0`, then A finishes before B opens.
   
   Physical-first A already retained 100, so the published set filter includes 
it and B evaluates `from_microsecond(-1)`, raising an error. Best-first A 
initially returns 0; after Safe tightens the bound, RG0 is skipped, the set 
filter excludes 100, and B's error disappears. UNION streams its independent 
branch while the set build is unfinished; query-owned TopN predicates are 
shared across fragments on the same BE.
   
   The existing epoch-project fix checks only the traversed path; this is a 
separate sibling consumer through a generated filter. Fence sensitive later 
inputs before permitting this reduction, and add the set-RF/sibling-error 
regression.



##########
be/src/format_v2/parquet/parquet_scan.cpp:
##########
@@ -1409,21 +1414,147 @@ std::vector<format::LocalColumnIndex> 
ParquetScanScheduler::adaptive_predicate_p
     return result;
 }
 
+namespace {
+
+struct TopNOrderingColumn {
+    const VTopNPred* predicate;
+    const ParquetColumnSchema* schema;
+};
+
+std::optional<TopNOrderingColumn> find_topn_ordering_column(
+        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
+        const format::FileScanRequest& request) {
+    const size_t safe_count =
+            std::min(request.metadata_pruning_safe_conjunct_count, 
request.conjuncts.size());
+    const VTopNPred* topn = nullptr;
+    for (size_t idx = 0; idx < safe_count; ++idx) {
+        const auto& root = request.conjuncts[idx]->root();
+        if (root->is_topn_filter() && root->can_evaluate_zonemap_filter()) {
+            topn = assert_cast<const VTopNPred*>(root.get());
+            break;
+        }
+    }
+    if (topn == nullptr) {
+        return std::nullopt;
+    }
+    const auto* slot = assert_cast<const VSlotRef*>(topn->get_child(0).get());
+    const auto position = std::ranges::find_if(request.local_positions, 
[&](const auto& entry) {
+        return entry.second.value() == slot->column_id();
+    });
+    if (position == request.local_positions.end()) {
+        return std::nullopt;
+    }
+    const auto column = std::ranges::find_if(file_schema, [&](const auto& 
schema) {
+        return schema->local_id == position->first.value();
+    });
+    if (column == file_schema.end() || (*column)->kind != 
ParquetColumnSchemaKind::PRIMITIVE ||
+        (*column)->max_repetition_level != 0 ||
+        !expr_zonemap::data_types_compatible((*column)->type, 
slot->data_type())) {
+        return std::nullopt;
+    }
+    return TopNOrderingColumn {.predicate = topn, .schema = column->get()};
+}
+
+} // namespace
+
+void ParquetScanScheduler::prepare_topn_row_group_order(
+        const ParquetFileContext& file_context,
+        const std::vector<std::unique_ptr<ParquetColumnSchema>>& file_schema,
+        const format::FileScanRequest& request) {
+    _row_group_order_prepared = true;
+    const auto binding = find_topn_ordering_column(file_schema, request);
+    if (!binding.has_value()) {
+        return;
+    }
+    const auto* topn = binding->predicate;
+    const auto& schema = *binding->schema;
+    const size_t safe_count =
+            std::min(request.metadata_pruning_safe_conjunct_count, 
request.conjuncts.size());
+    const auto& metadata = file_context.native_metadata->to_thrift();
+    _topn_footer_recheck = std::ranges::any_of(_scan_plan->row_groups, 
[&](const auto& plan) {
+        const auto& group = metadata.row_groups[plan.row_group_id];
+        DORIS_CHECK(schema.leaf_column_id >= 0 && schema.leaf_column_id < 
group.columns.size());
+        const auto& chunk = group.columns[schema.leaf_column_id];
+        return chunk.__isset.meta_data && chunk.meta_data.__isset.statistics;
+    });
+    // Files without statistics cannot benefit from repeated footer checks. 
Row-ID fetches promise
+    // increasing positions, and an unsafe table expression must continue 
observing file order.
+    if (!_topn_footer_recheck || request.row_ids.has_value() || 
!request.allow_row_reordering ||

Review Comment:
   [P2] Fence throwing native materialization for identity mappings
   
   A trivial table mapping does not establish that physical Parquet 
materialization is total. This reduced plan remains eligible with 
`enable_strict_cast=true` and matching nullable file/table types:
   
   ```text
   TopN(k ASC, d ASC LIMIT 1)
     FileScan(k INT, d DATE)
   ```
   
   In one split/scanner using PLAIN pages, let physical RG0 contain `k=100` and 
DATE day `2932897` (`10000-01-01`), then place several full batches of `k=0` 
with ordinary dates in later groups. Parquet DATE uses a signed INT32 epoch-day 
domain ([format 
specification](https://github.com/apache/parquet-format/blob/master/LogicalTypes.md#date));
 this value exceeds Doris's date range without requiring a malformed file or 
schema evolution.
   
   Logical DATE infers `TYPE_DATEV2`, so identical file/table types keep 
`mapping.is_trivial=true`. Nevertheless, `DateV2ParquetConsumer` calls 
`decode_parquet_date`, which returns `DataQualityError` for this day number; 
the strict mode passed by `NativeColumnReader` prevents NULL substitution. 
Physical order therefore fails on RG0 before a bound exists. The new ordering 
reads valid low-key groups first, publishes `k=0`, and prunes RG0 before native 
conversion, suppressing the error.
   
   This is distinct from the existing table `EvolutionCast` report: no table 
cast or nontrivial mapping exists here. Require a file-reader proof that native 
logical materialization is safe to reorder, or conservatively fence the 
affected conversion paths, and add a strict identity-DATE late-bound regression.



##########
be/src/format_v2/table_reader.cpp:
##########
@@ -1614,6 +1615,15 @@ bool TableReader::_is_safe_to_pre_execute(const 
VExprContextSPtr& conjunct) {
     return predicate->is_safe_to_execute_on_selected_rows();
 }
 
+bool TableReader::_is_safe_to_reorder_rows(const TableColumnMapper& mapper) 
const {
+    // FE/scan conjuncts cannot see implicit materialization: an unrelated 
DATE -> TIMESTAMP_NS
+    // output can overflow. Only direct mappings are proven safe, including 
their nested children;
+    // a complex remap can run child casts even when its outer projection is a 
SlotRef.
+    return std::ranges::all_of(_conjuncts, _is_safe_to_pre_execute) &&
+           std::ranges::all_of(mapper.mappings(),

Review Comment:
   [P2] Fence hidden equality-delete key casts before reordering
   
   `mapper.mappings()` covers projected output mappings, while Iceberg 
customization can append a throwing cast to `FileScanRequest::delete_conjuncts` 
for an unprojected equality-delete key. A reduced triggering plan is:
   
   ```text
   TopN(k ASC LIMIT 1)
     Iceberg FileScan(output: k INT; hidden delete key: ts TIMESTAMP_NS)
       retained Parquet: k INT, ts DATE
   ```
   
   Use an Iceberg v3 DATE-to-TIMESTAMP_NS promotion with stable field IDs and 
an applicable newer equality-delete file containing a representable, 
nonmatching `ts`. `_append_equality_delete_predicates` inserts `EvolutionCast` 
for the hidden data key; `EvolutionCast::prepare` forces strict conversion for 
this promotion. Neither that expression nor its hidden mapping is in the two 
collections checked here. Reconciliation sees the unchanged projected `k`, so 
it retains the TopN conjunct and permits reordering.
   
   In one split/scanner, put `(k=100, ts=1000-01-01)` in physical RG0, followed 
by several full low-key groups `(k=0, ts=2024-01-02)`. Physical order evaluates 
the hidden delete-key cast and raises overflow before any bound. Best-first 
reads the valid groups, publishes `k=0`, and rejects RG0 at the new footer 
check before delete evaluation, allowing the query to succeed.
   
   This is separate from the existing projected-output conversion thread: its 
repaired visible-mapping proof cannot inspect this customization-created delete 
expression. Include finalized delete-key evaluation in the initial and refresh 
safety proof and cover the output-only-`k` case.



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