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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/analysis/SubqueryToApply.java:
##########
@@ -118,20 +118,15 @@ public List<Rule> buildRules() {
                                 ctx.statementContext, 
shouldOutputMarkJoinSlot.get(i));
                         SubqueryContext context = new 
SubqueryContext(subqueryExprs);
                         Expression conjunct = 
replaceSubquery.replace(oldConjuncts.get(i), context);
-                        // TODO: The way to optimize null aware mark join is 
not right.
-                        //   remove it temporary until we refactor it.
-                        // ExpressionRewriteContext rewriteContext = new 
ExpressionRewriteContext(ctx.cascadesContext);
-                        // boolean isMarkSlotNotNull = 
conjunct.containsType(MarkJoinSlotReference.class)
-                        //                 ? 
ExpressionUtils.canInferNotNullForMarkSlot(
-                        //                         
TrySimplifyPredicateWithMarkJoinSlot.INSTANCE.rewrite(conjunct,
-                        //                                 rewriteContext), 
rewriteContext)
-                        //                 : false;
-                        boolean isMarkSlotNotNull = false;
+                        Pair<Expression, Map<MarkJoinSlotReference, 
Pair<Boolean, Boolean>>> simplifyResult =
+                                simplifyConjunctWithMarkJoinSlot(conjunct, 
filter, ctx.cascadesContext);

Review Comment:
   [P1] Fence elimination against the complete evaluation domain
   
   Inference receives only the current marker-replaced conjunct. For `WHERE 
ifnull(k IN (SELECT v FROM s), FALSE) AND assert_true(guard,'bad')`, top-level 
conjunction extraction puts the assertion in a sibling expression, so the first 
conjunct alone yields Pair.second=true. The resulting semi join prunes an 
unmatched `guard=FALSE` row before the filter, whereas a mixed match/nonmatch 
block from the retained mark Apply still evaluates the sibling assertion and 
raises. A second blind spot occurs when the sensitive expression is inside a 
later subquery plan: marker replacement erases that plan before this call, so 
an earlier eliminable Apply can prune its input. The existing same-expression 
fence cannot see either case. Please validate Pair.second against the complete 
containing conjunct set and all affected subquery plans, with sibling-conjunct 
and ordered multi-Apply regressions.



##########
fe/fe-core/src/test/java/org/apache/doris/nereids/util/InferMarkSlotNotNullMapTest.java:
##########
@@ -0,0 +1,230 @@
+// 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.util;
+
+import org.apache.doris.common.Pair;
+import org.apache.doris.nereids.rules.expression.ExpressionRewriteTestHelper;
+import org.apache.doris.nereids.trees.expressions.And;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.IsNull;
+import org.apache.doris.nereids.trees.expressions.MarkJoinSlotReference;
+import org.apache.doris.nereids.trees.expressions.Not;
+import org.apache.doris.nereids.trees.expressions.Or;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.AssertTrue;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.Nvl;
+import org.apache.doris.nereids.trees.expressions.literal.BooleanLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.NullLiteral;
+import org.apache.doris.nereids.trees.expressions.literal.VarcharLiteral;
+import org.apache.doris.nereids.types.BooleanType;
+
+import com.google.common.collect.Lists;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * InferMarkSlotNotNullMapTest.
+ */
+public class InferMarkSlotNotNullMapTest extends ExpressionRewriteTestHelper {
+
+    @Test
+    public void testSingleMarkSlotAndOr() {
+        MarkJoinSlotReference markSlot1 = new 
MarkJoinSlotReference("markSlot1");
+
+        // pair.first is based on the simplified predicate (non-mark-slot 
children in And/Or
+        // are replaced by true/false): true when it taking false or null 
always evaluates
+        // to false or null; pair.second is based on the original predicate: 
true when it
+        // taking false or null always evaluates to false or null
+        assertMarkSlotPair(new And(BooleanLiteral.FALSE, markSlot1), 
markSlot1, true, true);
+        assertMarkSlotPair(new And(BooleanLiteral.TRUE, markSlot1), markSlot1, 
true, true);
+        assertMarkSlotPair(new And(NullLiteral.INSTANCE, markSlot1), 
markSlot1, true, true);
+        // or(true, markSlot1): after simplification the child true is 
replaced by false, so
+        // the simplified predicate or(false, markSlot1) taking false or null 
evaluates to
+        // false or null, making pair.first true; the original or(true, 
markSlot1) taking
+        // false evaluates to true, making pair.second false
+        assertMarkSlotPair(new Or(BooleanLiteral.TRUE, markSlot1), markSlot1, 
true, false);
+        assertMarkSlotPair(new Or(BooleanLiteral.FALSE, markSlot1), markSlot1, 
true, true);
+        assertMarkSlotPair(new Or(NullLiteral.INSTANCE, markSlot1), markSlot1, 
true, true);
+    }
+
+    @Test
+    public void testSingleMarkSlotIsNullIsNotNull() {
+        MarkJoinSlotReference markSlot1 = new 
MarkJoinSlotReference("markSlot1");
+
+        // is null: taking false returns false while taking null returns true, 
which is
+        // neither false nor null, and taking true returns false, so both 
fields are false
+        assertMarkSlotPair(new IsNull(markSlot1), markSlot1, false, false);
+        // is not null: taking false returns true, which is neither false nor 
null, so
+        // pair.first is false; the original predicate taking false also 
evaluates to
+        // true, so pair.second is false too
+        assertMarkSlotPair(new Not(new IsNull(markSlot1)), markSlot1, false, 
false);
+        // markSlot1 and is not null(markSlot1): the predicate is equivalent to
+        // markSlot1 being true
+        assertMarkSlotPair(new And(markSlot1, new Not(new IsNull(markSlot1))),
+                markSlot1, true, true);
+    }
+
+    @Test
+    public void testSingleMarkSlotNvl() {
+        MarkJoinSlotReference markSlot1 = new 
MarkJoinSlotReference("markSlot1");
+
+        // nvl(markSlot1, false) is equivalent to markSlot1 being true
+        assertMarkSlotPair(new Nvl(markSlot1, BooleanLiteral.FALSE), 
markSlot1, true, true);
+        // nvl(markSlot1, true): taking null returns true, which is neither 
false nor null,
+        // so both pair.first and pair.second are false
+        assertMarkSlotPair(new Nvl(markSlot1, BooleanLiteral.TRUE), markSlot1, 
false, false);
+        // nvl(markSlot1, null): taking null returns null, which is treated as 
same as false,
+        // and taking true returns true, so both fields are true
+        assertMarkSlotPair(new Nvl(markSlot1, NullLiteral.INSTANCE), 
markSlot1, true, true);
+    }
+
+    @Test
+    public void testMultiMarkSlot() {
+        MarkJoinSlotReference markSlot1 = new 
MarkJoinSlotReference("markSlot1");
+        MarkJoinSlotReference markSlot2 = new 
MarkJoinSlotReference("markSlot2");
+
+        // or(markSlot1, markSlot2): when the other mark slot is true, the 
target slot taking
+        // false or null evaluates to true, which is neither false nor null, 
so both
+        // pair.first and pair.second are false
+        assertMarkSlotPair(new Or(markSlot1, markSlot2), markSlot1, false, 
false);

Review Comment:
   [P2] Exercise a later base-3 assignment
   
   Every current multi-slot oracle is decided by tuple zero, where the other 
mark is TRUE; the four-slot OR also exits on that all-TRUE tuple and asserts 
only map size. Consequently, changing the implementation to `loopCount = 1` 
still passes this suite. A mutation-sensitive case is `P = (M1 AND FALSE) OR 
NOT(M2)` for target `M1`: `M2=TRUE` appears to allow `(true,true)`, but the 
later `M2=FALSE` assignment makes both predicates TRUE and forces the correct 
`(false,false)`. Please add this later-tuple case plus three-/four-slot carry 
cases so the new enumeration logic is actually covered.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/util/ExpressionUtils.java:
##########
@@ -680,72 +685,139 @@ public static boolean hasNullLiteral(List<Expression> 
children) {
     }
 
     /**
-     * canInferNotNullForMarkSlot
+     * infer the null and false behavior of each mark join slot in the 
predicate.
+     * the predicate is first simplified by 
TrySimplifyPredicateWithMarkJoinSlot, which
+     * replaces the conjuncts without any mark slot in And with true and in Or 
with false,
+     * then both the original predicate and the simplified predicate are 
evaluated.
+     * return a map from mark join slot to a pair:
+     * Pair.first: whether the simplified predicate taking false or null always
+     *             evaluates to a value that is either false or null, i.e. the
+     *             target mark slot's null value can be replaced by false
+     * Pair.second: whether the original predicate taking false or null always
+     *              evaluates to a value that is either false or null, i.e. the
+     *              false and null values of the target mark slot are
+     *              indistinguishable in the original predicate
      */
-    public static boolean canInferNotNullForMarkSlot(Expression predicate, 
ExpressionRewriteContext ctx) {
-        /*
-         * assume predicate is from LogicalFilter
-         * the idea is replacing each mark join slot with null and false 
literal then run FoldConstant rule
-         * if the evaluate result are:
-         * 1. all true
-         * 2. all null and false (in logicalFilter, we discard both null and 
false values)
-         * the mark slot can be non-nullable boolean
-         * and in semi join, we can safely change the mark conjunct to hash 
conjunct
-         */
-        ImmutableList<Literal> literals = 
ImmutableList.of(NullLiteral.BOOLEAN_INSTANCE, BooleanLiteral.FALSE);
+    public static Map<MarkJoinSlotReference, Pair<Boolean, Boolean>> 
inferMarkSlotNotNullMap(
+            Expression predicate, ExpressionRewriteContext ctx) {
+        Expression simplifiedPredicate = 
TrySimplifyPredicateWithMarkJoinSlot.INSTANCE.rewrite(predicate, ctx);
+        Map<MarkJoinSlotReference, Pair<Boolean, Boolean>> result = 
Maps.newLinkedHashMap();
         List<MarkJoinSlotReference> markJoinSlotReferenceList = new 
ArrayList<>(
                 (predicate.collect(MarkJoinSlotReference.class::isInstance)));
         int markSlotSize = markJoinSlotReferenceList.size();
-        int maxMarkSlotCount = 4;
         // if the conjunct has mark slot, and maximum 4 mark slots(for 
performance)
-        if (markSlotSize > 0 && markSlotSize <= maxMarkSlotCount) {
-            Map<Expression, Expression> replaceMap = Maps.newHashMap();
-            boolean meetTrue = false;
-            boolean meetNullOrFalse = false;
+        if (markSlotSize > 0 && markSlotSize <= MAX_MARK_SLOT_COUNT) {
+            for (int targetIdx = 0; targetIdx < markSlotSize; ++targetIdx) {
+                result.put(markJoinSlotReferenceList.get(targetIdx),
+                        inferMarkSlotNotNullForTargetMarkSlot(
+                                predicate, simplifiedPredicate, 
markJoinSlotReferenceList, targetIdx, ctx));
+            }
+        }
+        return result;
+    }
+
+    /**
+     * infer the null and false behavior of the target mark slot
+     * replace the target slot with false and null, and replace other mark 
slots with
+     * true, false and null, and evaluate both the original predicate and the 
simplified
+     * predicate for every combination of other mark slots' values
+     * return a pair:
+     * Pair.first: whether the simplified predicate taking false or null 
always evaluates to
+     *             a value that is either false or null
+     * Pair.second: whether the original predicate taking false or null always 
evaluates to
+     *              a value that is either false or null
+     */
+    private static Pair<Boolean, Boolean> 
inferMarkSlotNotNullForTargetMarkSlot(Expression predicate,
+            Expression simplifiedPredicate,
+            List<MarkJoinSlotReference> markJoinSlotReferenceList, int 
targetIdx, ExpressionRewriteContext ctx) {
+        int markSlotSize = markJoinSlotReferenceList.size();
+        /*
+         * target slot enumerates false and null, other mark slots enumerate 
true, false and null
+         * markSlotSize = 1 -> otherMarkSlotCount = 0 -> loopCount = 1
+         * markSlotSize = 2 -> otherMarkSlotCount = 1 -> loopCount = 3
+         * markSlotSize = 3 -> otherMarkSlotCount = 2 -> loopCount = 9
+         * markSlotSize = 4 -> otherMarkSlotCount = 3 -> loopCount = 27
+         */
+        int otherMarkSlotCount = markSlotSize - 1;
+        int loopCount = 1;
+        for (int i = 0; i < otherMarkSlotCount; ++i) {
+            loopCount *= 3;
+        }
+        ImmutableList<Literal> otherLiterals = ImmutableList.of(
+                BooleanLiteral.TRUE, BooleanLiteral.FALSE, 
NullLiteral.BOOLEAN_INSTANCE);
+        Map<Expression, Expression> replaceMap = Maps.newHashMap();
+        boolean sameResultForFalseAndNull = true;
+        boolean simplifiedForFalseAndNull = true;
+        for (int i = 0; i < loopCount; ++i) {
+            replaceMap.clear();
             /*
-             * markSlotSize = 1 -> loopCount = 2 ---- 0, 1
-             * markSlotSize = 2 -> loopCount = 4 ---- 00, 01, 10, 11
-             * markSlotSize = 3 -> loopCount = 8 ---- 000, 001, 010, 011, 100, 
101, 110, 111
-             * markSlotSize = 4 -> loopCount = 16 ---- 0000, 0001, ... 1111
+             * replace other mark slots with true, false or null
+             * otherLiterals.get(0) -> BooleanLiteral.TRUE
+             * otherLiterals.get(1) -> BooleanLiteral.FALSE
+             * otherLiterals.get(2) -> NullLiteral(BooleanType.INSTANCE)
              */
-            int loopCount = 1 << markSlotSize;
-            for (int i = 0; i < loopCount; ++i) {
-                replaceMap.clear();
-                /*
-                 * replace each mark slot with null or false
-                 * literals.get(0) -> NullLiteral(BooleanType.INSTANCE)
-                 * literals.get(1) -> BooleanLiteral.FALSE
-                 */
-                for (int j = 0; j < markSlotSize; ++j) {
-                    replaceMap.put(markJoinSlotReferenceList.get(j), 
literals.get((i >> j) & 1));
+            int code = i;
+            for (int j = 0; j < markSlotSize; ++j) {
+                if (j == targetIdx) {
+                    continue;
                 }
-                Expression evalResult = FoldConstantRule.evaluate(
-                        ExpressionUtils.replace(predicate, replaceMap),
-                        ctx);
+                replaceMap.put(markJoinSlotReferenceList.get(j), 
otherLiterals.get(code % 3));
+                code /= 3;
+            }
+            // evaluate the original predicate with target slot taking false
+            replaceMap.put(markJoinSlotReferenceList.get(targetIdx), 
BooleanLiteral.FALSE);
+            Expression evalResultWithFalse = FoldConstantRule.evaluate(
+                    ExpressionUtils.replace(predicate, replaceMap), ctx);
+            // evaluate the simplified predicate with target slot taking false
+            Expression simplifiedEvalResultWithFalse = 
FoldConstantRule.evaluate(
+                    ExpressionUtils.replace(simplifiedPredicate, replaceMap), 
ctx);
+            // evaluate the original predicate with target slot taking null
+            replaceMap.put(markJoinSlotReferenceList.get(targetIdx), 
NullLiteral.BOOLEAN_INSTANCE);
+            Expression evalResultWithNull = FoldConstantRule.evaluate(
+                    ExpressionUtils.replace(predicate, replaceMap), ctx);
+            // evaluate the simplified predicate with target slot taking null
+            Expression simplifiedEvalResultWithNull = 
FoldConstantRule.evaluate(
+                    ExpressionUtils.replace(simplifiedPredicate, replaceMap), 
ctx);
+            /*
+             * if the original predicate taking false or null evaluates to a 
value other than
+             * false or null, the false and null values of the target mark 
slot are
+             * distinguishable in the original predicate
+             */
+            if (!isFalseOrNull(evalResultWithFalse) || 
!isFalseOrNull(evalResultWithNull)) {
+                sameResultForFalseAndNull = false;
+            }
 
-                if (evalResult.equals(BooleanLiteral.TRUE)) {
-                    if (meetNullOrFalse) {
-                        return false;
-                    } else {
-                        meetTrue = true;
-                    }
-                } else if ((isNullOrFalse(evalResult))) {
-                    if (meetTrue) {
-                        return false;
-                    } else {
-                        meetNullOrFalse = true;
-                    }
-                } else {
-                    return false;
-                }
+            /*
+             * if the simplified predicate taking false or null evaluates to a 
value other than
+             * false or null, the target slot's null value cannot be replaced 
by false
+             */
+            if (!isFalseOrNull(simplifiedEvalResultWithFalse) || 
!isFalseOrNull(simplifiedEvalResultWithNull)) {
+                simplifiedForFalseAndNull = false;
+            }
+
+            if (!sameResultForFalseAndNull && !simplifiedForFalseAndNull) {
+                break;
             }
-            return true;
         }
-        return false;
+        /*
+         * pair.second is a row-truth proof: it only proves that the filter 
treats the target
+         * mark slot taking false or null identically. dropping the mark join 
(turning the
+         * Apply into a plain semi join) also changes which rows reach the 
other expressions
+         * in the predicate. for a NoneMovableFunction (e.g. assert_true) or a 
volatile
+         * expression, the evaluation domain matters: the semi join prunes the 
unmatched rows
+         * before the filter, so these expressions may no longer be evaluated 
on the same
+         * rows, which changes error behavior or results. fence pair.second to 
false in this
+         * case so that the mark join is never eliminated across such 
expressions.
+         */
+        if (predicate.containsType(NoneMovableFunction.class)
+                || predicate.containsVolatileExpression()) {
+            sameResultForFalseAndNull = false;

Review Comment:
   [P1] Fence Pair.first across evaluation-sensitive expressions
   
   This guard clears only `sameResultForFalseAndNull` (Pair.second), leaving 
`simplifiedForFalseAndNull` (Pair.first) true. For `((M AND 
assert_true(guard,'bad')) OR flag)`, the simplifier produces `(M AND TRUE) OR 
FALSE`, so Pair.first marks `M` non-null even though Pair.second correctly 
keeps the mark Apply. With an unmatched positive-IN probe against a 
NULL-containing build side, `InApplyToJoin` then changes `M` from NULL to 
FALSE. Vectorized AND evaluates its RHS for the nullable NULL input but can 
return before the RHS for an all-FALSE/non-null input, suppressing the required 
`assert_true` error. This is distinct from the existing Pair.second issue 
because the Apply and its row domain remain intact. Please fence Pair.first 
(and the volatile analogue) here too, and add a NULL-producing error regression 
for this retained-mark path.



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