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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ReorderJoin.java:
##########
@@ -153,7 +175,10 @@ public Plan joinToMultiJoin(Plan plan, Map<Plan, 
DistributeHint> planToHintType)
                 // (t1 join t2) join t3 where t1.a = t3.x + random()
                 // if reorder, then may have ((t1 join t3) on t1.a = t3.x + 
random()) join t2,
                 // then the reorder result will less rows than origin.
-                if (conjunct.containsVolatileExpression()) {
+                // a NoneMovableFunction (e.g. assert_true) has the same 
problem: reordering
+                // turns the filter into a join condition that is evaluated on 
a different
+                // (superset) row set, which changes its error behavior or 
results.
+                if (conjunct.containsNoneMovableOrVolatile()) {
                     return plan;

Review Comment:
   **[P1] Preserve NoneMovable predicates already stored on join edges**
   
   This check covers only conjuncts on the surrounding `LogicalFilter`. 
Ordinary inner-join ON predicates still enter both reorder implementations. In 
hypergraph-v2, `HyperGraph.addJoin` splits every conjunct onto the minimal 
referenced node pair; for example:
   
   ```text
   Join(other: assert_true(A.x = C.x))
     Join(hash: A.k = B.k)
       A
       B
     C
   ```
   
   can be enumerated as `Join(hash A-B, Join(other assert A-C, A, C), B)`, 
evaluating the assertion before a nonmatching B join that originally eliminated 
all rows. The classical MultiJoin path has the same issue in a three-way 
cluster because it can prefer a later hash-connected child over an 
assertion-only edge. Please make a join containing a volatile/NoneMovable hash 
or other conjunct a boundary (or keep that conjunct on its original full edge) 
in both reorder paths.



##########
fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoinTest.java:
##########
@@ -79,4 +82,27 @@ public void testHashJoin() {
                         )
                 ).printlnTree();
     }
+
+    /**
+     * A join condition containing a NoneMovableFunction (assert_true) must 
stay inline in the
+     * other join conjuncts: aliasing it into a child project would change its 
evaluation
+     * granularity from per join pair to per row of that child.
+     */
+    @Test
+    public void testNoneMovableConditionStaysInline() {
+        Slot a = scan1.getOutput().get(1);
+        Slot b = scan2.getOutput().get(0);
+        Expression otherCondition = new AssertTrue(
+                new LessThan(a, new Add(b, b)), new StringLiteral("msg"));

Review Comment:
   **[P2] Exercise the side-local branch changed by this patch**
   
   This `AssertTrue(a < b + b)` references both join children. Before the new 
guard, neither child's slot set covered the outer expression, so the rewriter 
already recursed and left the outer `AssertTrue` inline; only the deterministic 
`b + b` subexpression could be projected. Therefore the current matcher passes 
against the pre-change implementation.
   
   Please use a wholly side-local condition such as `assert_true(a > 0, 
'msg')`, assert that neither child project contains the assertion, and include 
a deterministic side-local control that is still projected.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/ExpressionTrait.java:
##########
@@ -139,4 +139,16 @@ default boolean isVolatile() {
     default boolean containsVolatileExpression() {
         return containsType(VolatileExpression.class) && anyMatch(expr -> 
((ExpressionTrait) expr).isVolatile());
     }
+
+    /**
+     * Identify whether the expression contains a volatile expression or a 
NoneMovableFunction.
+     * Both kinds must not be moved, duplicated or pruned by rewrite rules: a 
volatile expression
+     * (e.g. rand(), uuid()) is non-deterministic, and a NoneMovableFunction 
(e.g. assert_true)
+     * has side effects (throws errors). Relocating either to a different row 
domain, or
+     * duplicating its evaluation, changes query semantics or error behavior, 
so rules should
+     * treat both identically (keep them above row-changing operators, never 
clone them).
+     */

Review Comment:
   **[P1] Apply this contract to active Sort and constant-UNION rewrites**
   
   Two registered paths still violate the contract documented here:
   
   - `PushDownFilterThroughSort` unconditionally changes `Limit -> 
Filter(assert_true) -> Sort -> Scan` to `Limit -> Sort -> Filter -> Scan`. The 
blocking Sort now evaluates a bad input row before producing the safe prefix 
that the original Limit could consume.
   - After `MergeOneRowRelationIntoUnion`, `PushProjectIntoUnion` uses 
volatile-only admission. `Project(1 AS y) -> UnionAll(const assert_true(false) 
AS x, const true)` can therefore drop the unreferenced assertion and turn a 
required error into returned rows.
   
   Please complete the registration-wide fence using 
`containsNoneMovableOrVolatile()` and add Sort-plus-Limit and constant-UNION 
pruning regressions.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughAggregation.java:
##########
@@ -69,7 +69,11 @@ public Rule build() {
                 // 2. if the conjunct contains unique function, it should not 
be pushed down;
                 //    e.g. 'select a, sum(a) from t group by a having a + 
random() >  10'
                 //    not equals 'select a, sum(a) from t where a + random() > 
10 group by a'
-                if (!conjunct.containsVolatileExpression()
+                // 3. a NoneMovableFunction (e.g. assert_true) must not be 
pushed below the
+                //    aggregation either: the aggregation changes which rows 
are evaluated
+                //    (grouped output vs input rows), so assert_true would run 
on a different
+                //    domain and its error behavior would change.
+                if (!conjunct.containsNoneMovableOrVolatile()
                         && !conjunctSlots.isEmpty() && 
canPushDownSlots.containsAll(conjunctSlots)) {

Review Comment:
   **[P1] Preserve the entire filter boundary around NoneMovable conjuncts**
   
   Blocking only the assertion conjunct is insufficient when a sibling is 
moved. For example:
   
   ```text
   Filter(assert_true(k > 0, 'bad'), k = 1)
     Aggregate(group by k)
       Scan(k = -1, 1)
   ```
   
   becomes `Filter(assert_true) -> Aggregate -> Filter(k=1) -> Scan`, so the 
failing `k=-1` group disappears before the assertion. The same per-conjunct 
partitioning exists in several changed join/CTE/set-op/window/generate paths, 
and some new mixed tests codify that partial movement.
   
   The inverse is also unsafe: registered `MergeFilters` collapses 
`Filter(assert_true) -> Filter(k=1)`; BE evaluates each conjunct expression on 
the same full block before combining masks, newly exposing the assertion to 
`k=-1`. Please treat any filter containing a NoneMovable conjunct as a whole 
boundary across row-domain-changing rewrites and filter merging, with 
mixed-conjunct runtime regressions.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/eageraggregation/EagerAggRewriter.java:
##########
@@ -784,7 +784,7 @@ public Plan visitLogicalFilter(LogicalFilter<? extends 
Plan> filter, PushDownAgg
         if (context.aggFuncAndGroupKeyAllEmpty() || 
context.hasVolatileFunctions()) {
             return filter;
         }
-        if 
(filter.getConjuncts().stream().anyMatch(Expression::containsVolatileExpression))
 {
+        if 
(filter.getConjuncts().stream().anyMatch(Expression::containsNoneMovableOrVolatile))
 {
             return genAggregate(filter, context);

Review Comment:
   **[P1] Fence NoneMovable aggregate arguments, not only filters**
   
   The new check protects a `LogicalFilter`, but eager-aggregation admission 
and context checks still reject only `containsVolatileExpression()`. A 
reachable tree is:
   
   ```text
   Aggregate(group=t2.id, count(assert_true(t1.id > 0, 'bad')))
     InnerJoin(t1.id = t2.id)
       Scan t1
       Scan t2
   ```
   
   With eager aggregation enabled, `count(assert_true(...))` can be generated 
below the join on the `t1` child. A failing `t1` row with no matching `t2` row 
is never evaluated in the original tree, but it throws in the pushed child 
aggregate. Please apply the combined fence at aggregate-function admission and 
every context/project recheck, and add an unmatched-row regression; the new 
filter-only test cannot cover this path.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/ProjectOtherJoinConditionForNestedLoopJoin.java:
##########
@@ -114,7 +114,9 @@ public Expression visit(Expression expression, 
ReplacerContext ctx) {
             // pair" to "per row of that child", which silently changes 
results. Keep such
             // expressions inline in otherJoinConjuncts, but still recurse to 
extract deterministic
             // child expressions.
-            if (expression.containsVolatileExpression()) {
+            // A NoneMovableFunction (e.g. assert_true) has the same 
granularity problem, so it
+            // must stay inline as well.
+            if (expression.containsNoneMovableOrVolatile()) {
                 return super.visit(expression, ctx);

Review Comment:
   **[P1] Guard equal ON predicates before hash classification**
   
   This guard runs after `FindHashConditionForJoin`. The shared 
`JoinUtils.isHashJoinCondition` still rejects only volatile expressions, so an 
ON predicate such as
   
   ```text
   NestedLoopJoin(other: t1.flag = assert_true(t2.v > 0, 'bad'))
     Empty t1
     Scan t2
   ```
   
   is first promoted to a hash conjunct; `PushDownExpressionsInHashCondition` 
then materializes `assert_true` in the right-child project. The original nested 
loop has zero candidate pairs and does not evaluate the assertion, while the 
hash build evaluates the failing `t2` row and throws. Please use 
`containsNoneMovableOrVolatile()` in the shared hash classifier (and 
defensively in hash-operand materialization), with an empty-probe regression.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownProjectThroughLimit.java:
##########
@@ -49,10 +49,15 @@ public class PushDownProjectThroughLimit extends 
OneRewriteRuleFactory {
 
     @Override
     public Rule build() {
-        return logicalProject(logicalLimit()).thenApply(ctx -> {
-            LogicalProject<LogicalLimit<Plan>> logicalProject = ctx.root;
-            LogicalLimit<Plan> logicalLimit = logicalProject.child();
-            return 
logicalLimit.withChildren(logicalProject.withChildren(logicalLimit.child()));
-        }).toRule(RuleType.PUSH_DOWN_PROJECT_THROUGH_LIMIT);
+        // a project that computes a NoneMovableFunction (e.g. assert_true) 
must not be
+        // pushed below the limit: the limit prunes rows, so assert_true would 
be evaluated
+        // on a different (larger) domain and its error behavior would change.
+        return logicalProject(logicalLimit())
+                .whenNot(LogicalProject::containsNoneMovableFunction)
+                .thenApply(ctx -> {

Review Comment:
   **[P1] Fence both project-relocation directions and both expression kinds**
   
   The new predicate covers only `NoneMovableFunction` in this direction. 
`Project(random(1) AS r) -> Limit(1 OFFSET 10) -> Scan` is still pushed below 
Limit, advancing the seeded sequence on discarded rows and changing the 
retained value.
   
   Conversely, registered `PullUpProjectUnderLimit`, `PullUpProjectUnderTopN`, 
and `PullUpProjectBetweenTopNAndAgg` do not check NoneMovable expressions. A 
matching `Limit -> Project(assert_true(...)) -> CrossJoin` becomes 
`Project(assert_true(...)) -> Limit -> CrossJoin`, so discarded join rows stop 
evaluating the assertion. Please use the combined volatile/NoneMovable fence 
here and on all inverse whole-project pull-ups; the existing inverse Limit/TopN 
tests are disabled.



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