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


##########
fe/fe-core/src/test/java/org/apache/doris/nereids/postprocess/PushDownFilterThroughProjectTest.java:
##########
@@ -156,4 +161,98 @@ public void testNotPushFilterWithNonfoldable() {
         Assertions.assertTrue(newPlan.child(0) instanceof PhysicalProject);
         Assertions.assertTrue(newPlan.child(0).child(0) instanceof 
PhysicalProject);
     }
+
+    /**
+     * A conjunct that itself contains a NoneMovableFunction (assert_true) may 
still be pushed
+     * through plain row-preserving projects: it is evaluated on exactly the 
same rows, and the
+     * row-changing operators below already refuse NoneMovableFunction 
conjuncts.
+     */
+    @Test
+    public void testPushFilterWithNoneMovableFunctionConjunct() {
+        LogicalProperties placeHolder = Mockito.mock(LogicalProperties.class);
+        CascadesContext ctx = Mockito.mock(CascadesContext.class);
+        OlapTable t1 = PlanConstructor.newOlapTable(0, "t1", 0, 
KeysType.DUP_KEYS);
+        List<String> qualifier = new ArrayList<>();
+        qualifier.add("test");
+        List<Slot> t1Output = new ArrayList<>();
+        SlotReference a = new SlotReference("a", IntegerType.INSTANCE);
+        SlotReference b = new SlotReference("b", IntegerType.INSTANCE);
+        SlotReference c = new SlotReference("c", IntegerType.INSTANCE);
+        t1Output.add(a);
+        t1Output.add(b);
+        t1Output.add(c);
+        LogicalProperties t1Properties = new LogicalProperties(() -> t1Output, 
() -> DataTrait.EMPTY_TRAIT);
+        PhysicalOlapScan scan = new 
PhysicalOlapScan(RelationId.createGenerator().getNextId(), t1,
+                qualifier, 0L, Collections.emptyList(), 
Collections.emptyList(), null,
+                PreAggStatus.on(), ImmutableList.of(), Optional.empty(), 
t1Properties,
+                Optional.empty(), new ArrayList<>(), ImmutableList.of(), 
ImmutableList.of(), Optional.empty(),
+                Optional.empty(), ImmutableList.of(), Optional.empty());
+        Alias x = new Alias(a, "x");
+        List<NamedExpression> projList3 = Lists.newArrayList(x, b, c);
+        PhysicalProject proj3 = new PhysicalProject(projList3, placeHolder, 
scan);
+        Alias y = new Alias(x.toSlot(), "y");
+        Alias z = new Alias(b, "z");
+        List<NamedExpression> projList2 = Lists.newArrayList(y, z, c);
+        PhysicalProject proj2 = new PhysicalProject(projList2, placeHolder, 
proj3);
+        Set<Expression> conjuncts = Sets.newHashSet();
+        conjuncts.add(new AssertTrue(new EqualTo(y.toSlot(), Literal.of(0)), 
new StringLiteral("msg")));
+        PhysicalFilter filter = new PhysicalFilter(conjuncts, 
proj2.getLogicalProperties(), proj2);
+
+        PushDownFilterThroughProject processor = new 
PushDownFilterThroughProject();

Review Comment:
   **[P2] Test the logical rule changed by this PR**
   
   These additions instantiate 
`org.apache.doris.nereids.processor.post.PushDownFilterThroughProject`, but the 
production hunk is in the different 
`rules.rewrite.PushDownFilterThroughProject`. The physical processor already 
(1) pushes an assertion through plain projects and (2) rejects any project 
containing a NoneMovable expression, so both tests pass without this PR. Please 
add a logical-rule test in `rules/rewrite/PushDowFilterThroughProjectTest` that 
fails when the changed hunk is reverted; if the existing project-wide matcher 
intentionally makes the new alias-local NoneMovable check unreachable, remove 
or refactor that dead admission instead of testing the unrelated layer.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughSetOperation.java:
##########
@@ -87,7 +87,11 @@ public Rule build() {
                     pushableConjuncts = new LinkedHashSet<>();
                     Set<Expression> kept = new LinkedHashSet<>();
                     for (Expression c : origFilter.getConjuncts()) {
-                        if (c.containsVolatileExpression()) {
+                        // a NoneMovableFunction (e.g. assert_true) must stay 
above the set
+                        // operation just like a volatile expression: for 
INTERSECT/EXCEPT/
+                        // UNION DISTINCT the set-op semantics depend on the 
full branch row
+                        // sets, so evaluating assert_true in each branch 
changes its domain.
+                        if (c.containsNoneMovableOrVolatile()) {

Review Comment:
   **[P1] Do not clone volatility across UNION ALL branches**
   
   The `canPushVolatileExpr` fast path bypasses this new fence. For
   
   ```text
   Filter(random(1) > c)
     UnionAll
       ScanA(one row)
       ScanB(one row)
   ```
   
   the original filter has one seeded function context and consumes `r1, r2`; 
`addFiltersToNewChildren` creates separate branch filters, so both contexts 
restart at `r1`. Choosing `c` between the first two values changes the returned 
rows, and `assert_true(random(1) > c, 'bad')` likewise changes error behavior. 
The 1:1 row-count argument does not preserve state across cloned contexts. 
Please keep volatile/NoneMovable conjuncts above UNION ALL too, or materialize 
one shared evaluation, with a two-branch seeded regression.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/PushDownFilterThroughProject.java:
##########
@@ -123,9 +123,16 @@ private static Pair<Set<Expression>, Set<Expression>> 
splitConjunctsByChildOutpu
             // If filter slot is alias and its expression contains 
non-foldable expression, it can't push down, example:
             // `filter(a > 1) -> project(b + random(1, 10) as a)`, if push 
down filter, it got
             // `project(b + random(1, 10) as a) -> filter(b + random(1, 10) > 
1)`, it contains two distinct RANDOM.
+            // The same applies to NoneMovableFunction (e.g. assert_true): if 
the referenced
+            // project alias computes a NoneMovableFunction, pushing the 
filter below would
+            // duplicate its evaluation (once in the project, once in the 
filter). a conjunct
+            // that itself contains a NoneMovableFunction may still be pushed 
through a plain
+            // slot project: the project is row-preserving, so the expression 
is evaluated on
+            // exactly the same rows, and the row-changing operators below 
(joins,
+            // aggregations, ...) already refuse to accept NoneMovableFunction 
conjuncts.
             if (childOutputs.containsAll(conjunctSlots)
                     && 
conjunctSlots.stream().map(childAlias::get).filter(Objects::nonNull)
-                            
.noneMatch(Expression::containsVolatileExpression)) {
+                            
.noneMatch(Expression::containsNoneMovableOrVolatile)) {

Review Comment:
   **[P1] Keep volatile project outputs on their original row domain**
   
   A filter that references only `k` still passes this alias-local check:
   
   ```text
   Filter(k = 1)
     Project(k, random(1) AS r)
       Scan(k = 0, 1)
   ```
   
   The rewrite produces `Project(random(1) AS r) -> Filter(k=1) -> Scan`. BE 
seeds one generator per function context and advances it once per project input 
row, so the original surviving row carries the second seeded value while the 
rewritten plan returns the first. This is distinct from the existing Limit 
thread: it is the active filter/project rule, plus its physical postprocessor 
parallel, changing a retained volatile output. Please make any project 
containing `containsNoneMovableOrVolatile()` a boundary in both paths and add a 
seeded-random regression.



##########
fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/ReorderJoinTest.java:
##########
@@ -350,4 +356,63 @@ private void testAsofJoinHelper(JoinType joinType) {
                                 .whenNot(join -> 
join.getJoinType().isCrossJoin()))
                 .printlnTree();
     }
+
+    /**
+     * A filter containing a NoneMovableFunction (assert_true) must prevent 
ReorderJoin from
+     * collecting the filter conjuncts into the join and redistributing them 
below the join:
+     * the child would evaluate assert_true on a superset of rows.
+     */
+    @Test
+    public void testNotReorderJoinWithNoneMovableFunction() {
+        Expression assertTrueExpr = new AssertTrue(

Review Comment:
   **[P2] Use a reorderable cluster to test the new filter guard**
   
   With only A and B, the old `findInnerJoin` cannot attach `assert_true(A.x > 
0)` to the join because its slots are wholly covered by A. It remains in 
`joinFilter`, and `PlanUtils.filterOrSelf` recreates the same outer filter, so 
this generic two-scan matcher passes without the new early return. Use a 
three-relation cluster where the old reorder moves a cross-side assertion onto 
an earlier edge, then assert scan identities/ExprIds and the exact assertion 
edge so the test fails on the base implementation.



##########
fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/InferPredicatesTest.java:
##########
@@ -907,4 +907,29 @@ void inferPredicatesLeftAsofInner() {
                 );
     }
 
+    @Test
+    public void testDoNotInferNoneMovableFunction() {

Review Comment:
   **[P2] Exercise the changed inference helpers**
   
   This assertion is a WHERE filter above the join. 
`InferPredicates.visitLogicalFilter` never calls either changed 
`inferNewPredicate*` helper, while the separately changed 
`PushDownFilterThroughJoin` rule is sufficient to leave it in exactly the 
matched shape. Reverting the `InferPredicates` hunk therefore still passes this 
test. Please apply `InferPredicates` directly to a join input/ON or, more 
directly, an EXCEPT/INTERSECT branch-substitution case and assert that no 
sibling filter is synthesized; cover both helper sites with a base-failing test.



##########
fe/fe-core/src/test/java/org/apache/doris/nereids/rules/rewrite/JoinExtractOrFromCaseWhenTest.java:
##########
@@ -0,0 +1,93 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.rules.rewrite;
+
+import org.apache.doris.nereids.rules.RuleFactory;
+import org.apache.doris.nereids.trees.expressions.Add;
+import org.apache.doris.nereids.trees.expressions.And;
+import org.apache.doris.nereids.trees.expressions.EqualTo;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.GreaterThan;
+import org.apache.doris.nereids.trees.expressions.Slot;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.AssertTrue;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.If;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.StringLiteral;
+import org.apache.doris.nereids.trees.plans.JoinType;
+import org.apache.doris.nereids.trees.plans.logical.LogicalOlapScan;
+import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
+import org.apache.doris.nereids.util.LogicalPlanBuilder;
+import org.apache.doris.nereids.util.MemoPatternMatchSupported;
+import org.apache.doris.nereids.util.MemoTestUtils;
+import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.nereids.util.PlanConstructor;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link JoinExtractOrFromCaseWhen}.
+ */
+class JoinExtractOrFromCaseWhenTest implements MemoPatternMatchSupported {
+
+    private final LogicalOlapScan scan1 = 
PlanConstructor.newLogicalOlapScan(0, "t1", 0);
+    private final LogicalOlapScan scan2 = 
PlanConstructor.newLogicalOlapScan(1, "t2", 0);
+
+    private RuleFactory joinExtractOrFromCaseWhenRule() {
+        return new JoinExtractOrFromCaseWhen();
+    }
+
+    /**
+     * A join condition mixing both sides with a case-when-like expression is 
normally rewritten
+     * into an OR-expansion condition; but when the condition also contains a 
NoneMovableFunction
+     * (assert_true), the rewrite must be skipped so the join is left 
untouched.
+     */
+    @Test
+    void testNoneMovableFunctionSkipsRewrite() {
+        Slot leftA = scan1.getOutput().get(0);
+        Slot leftB = scan1.getOutput().get(1);
+        Slot rightA = scan2.getOutput().get(0);
+        Slot rightB = scan2.getOutput().get(1);
+        // (case when leftA > 0 then rightA else rightB end) = leftA + leftB
+        Expression caseWhen = new If(new GreaterThan(leftA, new 
IntegerLiteral(0)), rightA, rightB);
+        Expression extractable = new EqualTo(caseWhen, new Add(leftA, leftB));
+
+        // control: without assert_true the rewrite fires and adds an 
OR-expansion condition.
+        // (the condition must be an other join conjunct: 
OrExpansion.needRewriteJoin only
+        // accepts a join with empty hash conjuncts, i.e. a nested loop join)
+        LogicalPlan control = new LogicalPlanBuilder(scan1).join(scan2, 
JoinType.INNER_JOIN,
+                ImmutableList.of(), ImmutableList.of(extractable)).build();
+        PlanChecker.from(MemoTestUtils.createConnectContext(), control)
+                .applyTopDown(joinExtractOrFromCaseWhenRule())
+                .matches(
+                        logicalJoin().when(join -> 
!join.getOtherJoinConjuncts().isEmpty())
+                );
+
+        // guard: with assert_true the join condition must be left untouched.
+        Expression guarded = new And(extractable,

Review Comment:
   **[P2] Make the guard case reach CASE/OR extraction**
   
   Wrapping the extractable equality and `assert_true` in a top-level `And` 
makes both direct children reference both join sides. The base 
`extractExpression` therefore satisfies neither one-side branch, and `And` is 
not an `EqualPredicate`, so OR expansion also does nothing; the guarded matcher 
passes before this change. The control is non-discriminating too because it 
only checks that the already non-empty other-conjunct list stays non-empty. Put 
the assertion inside a CASE/IF equality that the base rule demonstrably 
expands, and assert the exact derived OR conjunct for the control versus 
exactly one unchanged conjunct for the guarded 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