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


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/UnCorrelatedApplyAggregateFilter.java:
##########
@@ -64,28 +100,95 @@
  *                              |
  *                    Filter(Uncorrelated predicate)
  * </pre>
+ * <p>
+ * The rewrite above keeps the aggregation of the subquery on the inner side, 
which is only
+ * equivalent to the original subquery when the correlated predicate is an 
equality between the
+ * outer side and the inner side: in that case the inner rows of one outer row 
are exactly the
+ * groups of the aggregate whose key is the value of that inner side, so the 
HAVING clause of every
+ * group is the HAVING clause of the outer row.
+ * The aggregation of an EXISTS/NOT EXISTS subquery is built on the outer side 
instead when that
+ * equivalence does not hold, see
+ * {@link #pullUpCorrelatedPredicateByAggregatingOuter}.
  */
 public class UnCorrelatedApplyAggregateFilter implements RewriteRuleFactory {
+
+    /** name of the projected column which tells whether an inner row matched 
the correlated predicate */
+    private static final String CORRELATION_MATCH_MARKER = 
"$correlation_match_marker";
+
     @Override
     public List<Rule> buildRules() {
+        // The nodes between the apply and the aggregate of the subquery are 
projections and filters
+        // in any order, so the rules cannot enumerate their shapes: match 
every correlated apply
+        // whose right side starts with one of them and locate the aggregate 
in the rule.
         return ImmutableList.of(
-                logicalApply(any(), logicalAggregate(logicalFilter()))
-                        .when(LogicalApply::isCorrelated)
-                        
.then(UnCorrelatedApplyAggregateFilter::pullUpCorrelatedFilter)
-                        .toRule(RuleType.UN_CORRELATED_APPLY_AGGREGATE_FILTER),
-                logicalApply(any(), 
logicalFilter(logicalAggregate(logicalFilter())))
+                logicalApply(any(), subTree(LogicalAggregate.class, 
LogicalProject.class, LogicalFilter.class))
                         .when(LogicalApply::isCorrelated)
+                        .when(apply -> locateAggregate(apply).isPresent())
                         
.then(UnCorrelatedApplyAggregateFilter::pullUpCorrelatedFilter)
-                        
.toRule(RuleType.UN_CORRELATED_APPLY_FILTER_AGGREGATE_FILTER));
+                        
.toRule(RuleType.UN_CORRELATED_APPLY_AGGREGATE_FILTER));
+    }
+
+    /**
+     * The aggregation of the subquery and the filter which holds the 
predicates of its HAVING clause
+     * which were not pulled into the apply: the nodes between the apply and 
the aggregate are the
+     * projections and the filters of the subquery, and the projections 
between the aggregate and its
+     * filter only carry the columns which the aggregation needs.
+     */
+    private static Optional<Pair<LogicalAggregate<?>, 
Optional<LogicalFilter<Plan>>>> locateAggregate(
+            LogicalApply<?, ?> apply) {
+        Plan below = apply.right();
+        Optional<LogicalFilter<Plan>> havingFilter = Optional.empty();
+        while (!(below instanceof LogicalAggregate)) {
+            if (below instanceof LogicalFilter) {
+                havingFilter = Optional.of((LogicalFilter<Plan>) below);

Review Comment:
   [P1] Preserve every wrapper above the aggregate. A valid right side can 
reach this loop as `Filter(r < .5) -> Project(c, random() AS r) -> Filter(c = 
1) -> GlobalAggregate(count)`: filter pushdown intentionally leaves the 
predicate over the volatile alias above its project. This assignment first 
remembers the upper filter and then overwrites it with the deeper HAVING 
filter; the outer-aggregation path later rebuilds only `Filter(c = 1) -> 
Aggregate`, so `r < .5` is deleted and a probabilistic EXISTS becomes always 
true. This is distinct from the existing volatility thread, where the 
expression remains but is shared per key. Track the complete ordered wrapper 
chain and include it in movement-safety/reconstruction, or reject shapes that 
cannot be preserved.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/UnCorrelatedApplyAggregateFilter.java:
##########
@@ -115,10 +241,683 @@ public List<Rule> buildRules() {
         correlatedPredicate = ExpressionUtils.replace(correlatedPredicate, 
unCorrelatedExprToSlot);
         LogicalAggregate newAgg = new LogicalAggregate<>(newGroupby, 
newAggOutput,
                 
PlanUtils.filterOrSelf(ImmutableSet.copyOf(unCorrelatedPredicate), 
filter.child()));
+        // the predicates which were already pulled into the apply are the 
predicates of the HAVING
+        // clause of the subquery: they were evaluated on the rows of the old 
aggregate and have to
+        // stay in the filter of the new apply, otherwise the subquery loses 
them
+        List<Expression> newCorrelationFilter = Lists.newArrayList();
+        apply.getCorrelationFilter().map(ExpressionUtils::extractConjunction)
+                .ifPresent(newCorrelationFilter::addAll);
+        newCorrelationFilter.addAll(correlatedPredicate);
         return new LogicalApply<>(apply.getCorrelationSlot(), 
apply.getSubqueryType(), apply.isNot(),
                 apply.getCompareExpr(), apply.getTypeCoercionExpr(),
-                ExpressionUtils.optionalAnd(correlatedPredicate), 
apply.getMarkJoinSlotReference(),
+                ExpressionUtils.optionalAnd(newCorrelationFilter), 
apply.getMarkJoinSlotReference(),
                 apply.isNeedAddSubOutputToProjects(), 
apply.isMarkJoinSlotNotNull(), apply.left(),
-                isRightChildAgg ? newAgg : apply.right().withChildren(newAgg));
+                replaceAggregate(apply.right(), newAgg));
+    }
+
+    /**
+     * Whether a projection sits between the apply and the aggregation of the 
subquery: it only
+     * exposes a part of the output of the aggregation (the select list of the 
subquery), so the
+     * correlation keys the original rewrite adds to the group by are not 
visible above it.
+     */
+    private static boolean hasProjectionAboveAggregate(LogicalApply<?, ?> 
apply, LogicalAggregate<?> agg) {
+        Plan below = apply.right();
+        while (below != agg) {
+            if (below instanceof LogicalProject) {
+                return true;
+            }
+            below = below.child(0);
+        }
+        return false;
+    }
+
+    /**
+     * Keep the nodes which wrap the aggregate (the HAVING clause, the 
projection of the subquery)
+     * when the aggregate is replaced.
+     */
+    private static Plan replaceAggregate(Plan plan, Plan newAggregate) {
+        if (plan instanceof LogicalAggregate) {
+            return newAggregate;
+        }
+        return plan.withChildren(replaceAggregate(plan.child(0), 
newAggregate));
+    }
+
+    /**
+     * The predicates which relate the correlated subquery to the outer query, 
classified by the node
+     * on which they have to be evaluated:
+     *
+     * <ul>
+     *   <li>domain predicates select the inner rows which belong to the 
correlated domain of one outer
+     *       row. They are the predicates of the WHERE clause of the subquery, 
plus the predicates
+     *       which were already pulled into the apply and do not reference the 
aggregation;</li>
+     *   <li>aggregate predicates reference the output of the aggregate, they 
are the predicates of the
+     *       HAVING clause which were pulled into the apply by {@link 
UnCorrelatedApplyFilter} (that
+     *       rule runs before this one). They decide which rows of the 
aggregation the subquery
+     *       returns for one outer row.</li>
+     * </ul>
+     */
+    private static final class CorrelatedAggregatePredicates {
+        private final List<Expression> whereConjuncts = Lists.newArrayList();
+        private final List<Expression> aggregatePredicates = 
Lists.newArrayList();
+        private final Set<Expression> havingConjuncts = 
Sets.newLinkedHashSet();
+
+        private static CorrelatedAggregatePredicates of(LogicalApply<?, ?> 
apply,
+                LogicalAggregate<?> agg, LogicalFilter<Plan> filter,
+                Optional<LogicalFilter<Plan>> havingFilter, List<Expression> 
whereConjuncts) {
+            CorrelatedAggregatePredicates predicates = new 
CorrelatedAggregatePredicates();
+            predicates.whereConjuncts.addAll(whereConjuncts);
+            // Every predicate which was pulled into the apply was pulled from 
the filter which sits
+            // above the aggregate (the HAVING clause of the subquery), so it 
decides which rows of
+            // the aggregation the subquery returns and has to be evaluated 
above the aggregation of
+            // the rewrite. Its provenance cannot be recovered from the slots 
it uses: a predicate
+            // such as `outer.flag = 1` references no aggregation output and 
no inner column but it
+            // still rejects the row of the aggregation.
+            apply.getCorrelationFilter()
+                    .map(ExpressionUtils::extractConjunction)
+                    .orElse(ImmutableList.of())
+                    .forEach(predicates.aggregatePredicates::add);
+            havingFilter.ifPresent(remaining -> 
predicates.havingConjuncts.addAll(remaining.getConjuncts()));
+            return predicates;
+        }
+
+        private List<Expression> domainPredicates() {
+            return ImmutableList.copyOf(whereConjuncts);
+        }
+
+        private List<Expression> havingPredicates() {
+            return ImmutableList.<Expression>builder()
+                    .addAll(havingConjuncts)
+                    .addAll(aggregatePredicates)
+                    .build();
+        }
+
+        private Set<Expression> havingConjuncts() {
+            return havingConjuncts;
+        }
+
+        private boolean hasHaving() {
+            return !havingConjuncts.isEmpty() || 
!aggregatePredicates.isEmpty();
+        }
+
+        private boolean hasAggregatePredicates() {
+            return !aggregatePredicates.isEmpty();
+        }
+
+        /**
+         * The correlation slots which the keys of the aggregation have to 
contain: the domains of
+         * two outer rows are the same as soon as the slots their predicates 
use are equal.
+         */
+        private Set<Slot> keySlots(List<Slot> correlationSlots) {
+            Set<Slot> keys = new LinkedHashSet<>();
+            for (Expression conjunct : domainPredicates()) {
+                addCorrelationSlots(conjunct, correlationSlots, keys);
+            }
+            for (Expression conjunct : aggregatePredicates) {
+                addCorrelationSlots(conjunct, correlationSlots, keys);
+            }
+            return keys;
+        }
+
+        private static void addCorrelationSlots(Expression conjunct, 
List<Slot> correlationSlots, Set<Slot> keys) {
+            for (Slot slot : conjunct.getInputSlots()) {
+                if (correlationSlots.contains(slot)) {
+                    keys.add(slot);
+                }
+            }
+        }
+
+        /**
+         * Whether every predicate can be evaluated by the plan of the 
aggregation, that is if it only
+         * uses the correlation keys, the inner rows and the output of the 
aggregate.
+         */
+        private boolean isResolvable(LogicalApply<?, ?> apply, 
LogicalAggregate<?> agg,
+                LogicalFilter<Plan> filter) {
+            Set<ExprId> allowed = Sets.newHashSet();
+            apply.getCorrelationSlot().forEach(slot -> 
allowed.add(slot.getExprId()));
+            agg.getOutput().forEach(slot -> allowed.add(slot.getExprId()));
+            filter.child().getOutput().forEach(slot -> 
allowed.add(slot.getExprId()));
+            return domainPredicates().stream().allMatch(conjunct -> 
allowed.containsAll(conjunct.getInputSlotExprIds()))
+                    && havingPredicates().stream()
+                            .allMatch(conjunct -> 
allowed.containsAll(conjunct.getInputSlotExprIds()));
+        }
+    }
+
+    /**
+     * Whether the aggregation of the correlated subquery has to be built on 
the outer side.
+     * <p>
+     * The original rewrite puts the inner side of the correlated predicate 
into the group by of the
+     * aggregate, so the HAVING clause of one group is treated as the HAVING 
clause of one outer row.
+     * That is wrong for:
+     * <ul>
+     *   <li>a correlated predicate which is not an equality (eg. `inner.k 
&lt; outer.k`): the inner
+     *       rows of one outer row are the union of several groups, so group 
wide aggregates such as
+     *       count(*) are computed for a part of the domain of the outer row 
only;</li>
+     *   <li>a global aggregate (no group by) whose HAVING clause holds for an 
empty input
+     *       (eg. `having count(*) = 0`): a global aggregate returns one row 
for every outer row,
+     *       including the outer rows without any matching inner row, and that 
row disappears when
+     *       the inner side of the correlated predicate becomes the group by 
key;</li>
+     *   <li>a HAVING clause which references the outer query: the row kept by 
that HAVING clause is
+     *       the one of the domain of the outer row, so it cannot be evaluated 
on a group of the
+     *       inner side when the domain is empty.</li>
+     * </ul>
+     */
+    private static boolean needCorrelatedAggregationOnOuter(LogicalApply<?, ?> 
apply, LogicalAggregate<?> agg,
+            boolean havingFilterPulled, List<Expression> correlatedPredicate,
+            CorrelatedAggregatePredicates predicates) {
+        if (!apply.isExist()) {
+            // scalar and IN subqueries need the aggregate output to be 
exposed by a join
+            return false;
+        }
+        if (havingFilterPulled && !predicates.hasHaving()) {
+            // EXISTS/NOT EXISTS without a HAVING clause only depends on the 
existence of the
+            // aggregation result, which is kept by grouping the inner side
+            return false;
+        }
+        if (predicates.hasAggregatePredicates() && 
agg.getGroupByExpressions().isEmpty()) {
+            // a predicate of the HAVING clause which references the outer 
query decides whether the
+            // row of a global aggregate is kept, and that row exists for 
every outer row including
+            // the rows of an empty correlated domain, so the predicate cannot 
be evaluated on a
+            // group of the inner side
+            return true;
+        }
+        for (Expression conjunct : correlatedPredicate) {
+            if (!isSupportedCorrelatedComparison(conjunct, 
apply.getCorrelationSlot())) {
+                // keep the behavior of the original rewrite, which reports 
these predicates
+                return false;
+            }
+        }
+        if (predicates.domainPredicates().stream()
+                .anyMatch(conjunct -> breaksDomainPartition(conjunct, 
apply.getCorrelationSlot()))) {
+            // the domain of one outer row is the union of several groups of 
the aggregate
+            return true;
+        }
+        if (predicates.hasAggregatePredicates()) {
+            // the domain of one outer row is exactly one group and the rows 
of the subquery are the
+            // rows of that group, so the original rewrite is still equivalent
+            return false;
+        }
+        if (!agg.getGroupByExpressions().isEmpty()) {
+            // an equality correlated predicate maps the domain of every outer 
row onto exactly one group
+            return false;
+        }
+        return havingMayHoldWithEmptyInput(agg, predicates.havingConjuncts());
+    }
+
+    /**
+     * Whether this predicate changes the domain of an outer row in a way 
which is not a group of the
+     * aggregate. Only an equality between the outer side and the inner side 
partitions the inner rows
+     * of one outer row into exactly the groups of the aggregate, while a 
predicate which does not
+     * reference the outer query at all just filters the inner rows.
+     */
+    private static boolean breaksDomainPartition(Expression conjunct, 
List<Slot> correlationSlots) {
+        if (conjunct instanceof EqualPredicate) {
+            return false;
+        }
+        return 
conjunct.getInputSlots().stream().anyMatch(correlationSlots::contains);
+    }
+
+    /**
+     * Whether the correlated predicate is a comparison whose sides do not mix 
the outer query and the
+     * subquery, eg. `inner.k &lt; outer.k` or `outer.k = inner.abs(k)`. Those 
are the predicates which
+     * can be evaluated by joining the two sides, and the ones supported by 
the original rewrite.
+     */
+    private static boolean isSupportedCorrelatedComparison(Expression 
conjunct, List<Slot> correlationSlots) {
+        Expression predicate = conjunct;
+        if (predicate instanceof Not && predicate.child(0) instanceof 
BinaryExpression) {
+            predicate = predicate.child(0);
+        }
+        if (!(predicate instanceof BinaryExpression)) {
+            return false;
+        }
+        Expression left = ((BinaryExpression) predicate).left();
+        Expression right = ((BinaryExpression) predicate).right();
+        Set<Slot> leftSlots = left.getInputSlots();
+        Set<Slot> rightSlots = right.getInputSlots();
+        boolean correlatedToLeft = !leftSlots.isEmpty() && 
leftSlots.stream().allMatch(correlationSlots::contains)
+                && rightSlots.stream().noneMatch(correlationSlots::contains);
+        boolean correlatedToRight = !rightSlots.isEmpty() && 
rightSlots.stream().allMatch(correlationSlots::contains)
+                && leftSlots.stream().noneMatch(correlationSlots::contains);
+        return correlatedToLeft || correlatedToRight;
+    }
+
+    /**
+     * Rewrite `outer [not] exists (select agg from inner where &lt;correlated 
predicate&gt;
+     * [group by ...] having ...)` into a semi/anti join whose right side 
aggregates the outer rows
+     * together with their correlated inner rows, so that the aggregation of 
one outer row is the
+     * aggregation of exactly the inner rows satisfying the correlated 
predicate:
+     *
+     * <pre>
+     * before:
+     *              Apply(EXISTS, correlationSlot=[outer.k])
+     *             /                \
+     *        outer             Filter(having)
+     *                              +-- Aggregate(group by [inner.g], count(*))
+     *                                    +-- Filter(correlated 
predicate(inner.k &lt; outer.k))
+     *                                          +-- inner
+     *
+     * after:
+     *          LEFT SEMI JOIN(otherJoinConjuncts=[outer.k &lt;=&gt; key.k])
+     *         /                 \
+     *     outer              Filter(having: count(*) =&gt; count(marker))
+     *                           +-- Aggregate(group by [key.k, inner.g], 
count(marker))
+     *                                 +-- LEFT OUTER JOIN(inner.k &lt; key.k) 
    // keeps the empty domain
+     *                                       |-- Aggregate(group by [k], 
output=[k])   // distinct correlated keys
+     *                                       |     +-- outer'
+     *                                       +-- Project(marker, ...)
+     *                                             +-- Filter(uncorrelated 
predicates)
+     *                                                   +-- inner
+     * </pre>
+     * outer' is a deep copy of outer, so that the two references of the outer 
plan have their own
+     * slots and relation ids. The LEFT OUTER JOIN and the marker are not 
needed (a plain inner join
+     * is used) when the aggregate has a group by, because such an aggregate 
returns no row at all
+     * for an empty input. The predicates of the HAVING clause which reference 
the outer query (they
+     * were pulled into the apply by {@link UnCorrelatedApplyFilter}) are 
evaluated above the
+     * aggregate of the outer rows, so that they decide on the aggregation of 
the whole domain of an
+     * outer row, the empty domain included.
+     *
+     * @return null if this rewrite cannot be applied safely, the caller then 
reports the subquery as
+     *         unsupported
+     */
+    private static Plan 
pullUpCorrelatedPredicateByAggregatingOuter(LogicalApply<?, ?> apply,
+            LogicalAggregate<?> agg, LogicalFilter<Plan> filter,
+            List<Expression> unCorrelatedPredicate, 
CorrelatedAggregatePredicates predicates) {
+        Set<Slot> correlationSlots = 
predicates.keySlots(apply.getCorrelationSlot());
+        if (containsSensitiveExpression(apply.left(), correlationSlots)
+                || hasNonDeterministicRows(apply.left())
+                || containsNoneMovableFunction(apply.right())
+                || containsSensitiveSubqueryExpression(agg, filter, predicates)
+                || referencesOuterSlot(apply.right(), 
ImmutableSet.copyOf(predicates.whereConjuncts),
+                        apply.getCorrelationSlot())
+                || !predicates.isResolvable(apply, agg, filter)) {
+            return null;
+        }
+
+        // the domains of two outer rows are the same as soon as their 
correlation slots are equal,
+        // so the correlation slots are the only outer information the 
aggregation needs
+        LogicalPlan outer = (LogicalPlan) apply.left();
+        LogicalPlan outerCopy = LogicalPlanDeepCopier.INSTANCE.deepCopy(outer, 
new DeepCopierContext());
+        List<Slot> outerOutput = outer.getOutput();
+        List<Slot> outerCopyOutput = outerCopy.getOutput();

Review Comment:
   [P1] Require copied outer outputs to have disjoint ExprIds before using them 
as keys. In `Apply(EXISTS, number#7) -> [numbers(3), HAVING count(*)=0 -> 
Aggregate -> Filter(i.k < number#7)]` with inner `k={0}`, only outer number 0 
should survive. The TVF copier dispatches through `visitLogicalRelation`, but 
`LogicalTVFRelation.withRelationId` reuses the original logical properties, so 
`outerCopy.getOutput()` still contains `number#7`. The back condition is 
therefore `number#7 <=> number#7`, which `SimplifySelfComparison` folds to 
TRUE; because copied key 0 exists, the semi join returns 0, 1, and 2. This is 
distinct from the existing nondeterministic-copy thread: both TVF evaluations 
return identical rows, but their slots were never separated. Fix the relation 
copier or reject overlapping outputs.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/rewrite/UnCorrelatedApplyAggregateFilter.java:
##########
@@ -115,10 +241,683 @@ public List<Rule> buildRules() {
         correlatedPredicate = ExpressionUtils.replace(correlatedPredicate, 
unCorrelatedExprToSlot);
         LogicalAggregate newAgg = new LogicalAggregate<>(newGroupby, 
newAggOutput,
                 
PlanUtils.filterOrSelf(ImmutableSet.copyOf(unCorrelatedPredicate), 
filter.child()));
+        // the predicates which were already pulled into the apply are the 
predicates of the HAVING
+        // clause of the subquery: they were evaluated on the rows of the old 
aggregate and have to
+        // stay in the filter of the new apply, otherwise the subquery loses 
them
+        List<Expression> newCorrelationFilter = Lists.newArrayList();
+        apply.getCorrelationFilter().map(ExpressionUtils::extractConjunction)
+                .ifPresent(newCorrelationFilter::addAll);
+        newCorrelationFilter.addAll(correlatedPredicate);
         return new LogicalApply<>(apply.getCorrelationSlot(), 
apply.getSubqueryType(), apply.isNot(),
                 apply.getCompareExpr(), apply.getTypeCoercionExpr(),
-                ExpressionUtils.optionalAnd(correlatedPredicate), 
apply.getMarkJoinSlotReference(),
+                ExpressionUtils.optionalAnd(newCorrelationFilter), 
apply.getMarkJoinSlotReference(),
                 apply.isNeedAddSubOutputToProjects(), 
apply.isMarkJoinSlotNotNull(), apply.left(),
-                isRightChildAgg ? newAgg : apply.right().withChildren(newAgg));
+                replaceAggregate(apply.right(), newAgg));
+    }
+
+    /**
+     * Whether a projection sits between the apply and the aggregation of the 
subquery: it only
+     * exposes a part of the output of the aggregation (the select list of the 
subquery), so the
+     * correlation keys the original rewrite adds to the group by are not 
visible above it.
+     */
+    private static boolean hasProjectionAboveAggregate(LogicalApply<?, ?> 
apply, LogicalAggregate<?> agg) {
+        Plan below = apply.right();
+        while (below != agg) {
+            if (below instanceof LogicalProject) {
+                return true;
+            }
+            below = below.child(0);
+        }
+        return false;
+    }
+
+    /**
+     * Keep the nodes which wrap the aggregate (the HAVING clause, the 
projection of the subquery)
+     * when the aggregate is replaced.
+     */
+    private static Plan replaceAggregate(Plan plan, Plan newAggregate) {
+        if (plan instanceof LogicalAggregate) {
+            return newAggregate;
+        }
+        return plan.withChildren(replaceAggregate(plan.child(0), 
newAggregate));
+    }
+
+    /**
+     * The predicates which relate the correlated subquery to the outer query, 
classified by the node
+     * on which they have to be evaluated:
+     *
+     * <ul>
+     *   <li>domain predicates select the inner rows which belong to the 
correlated domain of one outer
+     *       row. They are the predicates of the WHERE clause of the subquery, 
plus the predicates
+     *       which were already pulled into the apply and do not reference the 
aggregation;</li>
+     *   <li>aggregate predicates reference the output of the aggregate, they 
are the predicates of the
+     *       HAVING clause which were pulled into the apply by {@link 
UnCorrelatedApplyFilter} (that
+     *       rule runs before this one). They decide which rows of the 
aggregation the subquery
+     *       returns for one outer row.</li>
+     * </ul>
+     */
+    private static final class CorrelatedAggregatePredicates {
+        private final List<Expression> whereConjuncts = Lists.newArrayList();
+        private final List<Expression> aggregatePredicates = 
Lists.newArrayList();
+        private final Set<Expression> havingConjuncts = 
Sets.newLinkedHashSet();
+
+        private static CorrelatedAggregatePredicates of(LogicalApply<?, ?> 
apply,
+                LogicalAggregate<?> agg, LogicalFilter<Plan> filter,
+                Optional<LogicalFilter<Plan>> havingFilter, List<Expression> 
whereConjuncts) {
+            CorrelatedAggregatePredicates predicates = new 
CorrelatedAggregatePredicates();
+            predicates.whereConjuncts.addAll(whereConjuncts);
+            // Every predicate which was pulled into the apply was pulled from 
the filter which sits
+            // above the aggregate (the HAVING clause of the subquery), so it 
decides which rows of
+            // the aggregation the subquery returns and has to be evaluated 
above the aggregation of
+            // the rewrite. Its provenance cannot be recovered from the slots 
it uses: a predicate
+            // such as `outer.flag = 1` references no aggregation output and 
no inner column but it
+            // still rejects the row of the aggregation.
+            apply.getCorrelationFilter()
+                    .map(ExpressionUtils::extractConjunction)
+                    .orElse(ImmutableList.of())
+                    .forEach(predicates.aggregatePredicates::add);
+            havingFilter.ifPresent(remaining -> 
predicates.havingConjuncts.addAll(remaining.getConjuncts()));
+            return predicates;
+        }
+
+        private List<Expression> domainPredicates() {
+            return ImmutableList.copyOf(whereConjuncts);
+        }
+
+        private List<Expression> havingPredicates() {
+            return ImmutableList.<Expression>builder()
+                    .addAll(havingConjuncts)
+                    .addAll(aggregatePredicates)
+                    .build();
+        }
+
+        private Set<Expression> havingConjuncts() {
+            return havingConjuncts;
+        }
+
+        private boolean hasHaving() {
+            return !havingConjuncts.isEmpty() || 
!aggregatePredicates.isEmpty();
+        }
+
+        private boolean hasAggregatePredicates() {
+            return !aggregatePredicates.isEmpty();
+        }
+
+        /**
+         * The correlation slots which the keys of the aggregation have to 
contain: the domains of
+         * two outer rows are the same as soon as the slots their predicates 
use are equal.
+         */
+        private Set<Slot> keySlots(List<Slot> correlationSlots) {
+            Set<Slot> keys = new LinkedHashSet<>();
+            for (Expression conjunct : domainPredicates()) {
+                addCorrelationSlots(conjunct, correlationSlots, keys);
+            }
+            for (Expression conjunct : aggregatePredicates) {
+                addCorrelationSlots(conjunct, correlationSlots, keys);
+            }
+            return keys;
+        }
+
+        private static void addCorrelationSlots(Expression conjunct, 
List<Slot> correlationSlots, Set<Slot> keys) {
+            for (Slot slot : conjunct.getInputSlots()) {
+                if (correlationSlots.contains(slot)) {
+                    keys.add(slot);
+                }
+            }
+        }
+
+        /**
+         * Whether every predicate can be evaluated by the plan of the 
aggregation, that is if it only
+         * uses the correlation keys, the inner rows and the output of the 
aggregate.
+         */
+        private boolean isResolvable(LogicalApply<?, ?> apply, 
LogicalAggregate<?> agg,
+                LogicalFilter<Plan> filter) {
+            Set<ExprId> allowed = Sets.newHashSet();
+            apply.getCorrelationSlot().forEach(slot -> 
allowed.add(slot.getExprId()));
+            agg.getOutput().forEach(slot -> allowed.add(slot.getExprId()));
+            filter.child().getOutput().forEach(slot -> 
allowed.add(slot.getExprId()));
+            return domainPredicates().stream().allMatch(conjunct -> 
allowed.containsAll(conjunct.getInputSlotExprIds()))
+                    && havingPredicates().stream()
+                            .allMatch(conjunct -> 
allowed.containsAll(conjunct.getInputSlotExprIds()));
+        }
+    }
+
+    /**
+     * Whether the aggregation of the correlated subquery has to be built on 
the outer side.
+     * <p>
+     * The original rewrite puts the inner side of the correlated predicate 
into the group by of the
+     * aggregate, so the HAVING clause of one group is treated as the HAVING 
clause of one outer row.
+     * That is wrong for:
+     * <ul>
+     *   <li>a correlated predicate which is not an equality (eg. `inner.k 
&lt; outer.k`): the inner
+     *       rows of one outer row are the union of several groups, so group 
wide aggregates such as
+     *       count(*) are computed for a part of the domain of the outer row 
only;</li>
+     *   <li>a global aggregate (no group by) whose HAVING clause holds for an 
empty input
+     *       (eg. `having count(*) = 0`): a global aggregate returns one row 
for every outer row,
+     *       including the outer rows without any matching inner row, and that 
row disappears when
+     *       the inner side of the correlated predicate becomes the group by 
key;</li>
+     *   <li>a HAVING clause which references the outer query: the row kept by 
that HAVING clause is
+     *       the one of the domain of the outer row, so it cannot be evaluated 
on a group of the
+     *       inner side when the domain is empty.</li>
+     * </ul>
+     */
+    private static boolean needCorrelatedAggregationOnOuter(LogicalApply<?, ?> 
apply, LogicalAggregate<?> agg,
+            boolean havingFilterPulled, List<Expression> correlatedPredicate,
+            CorrelatedAggregatePredicates predicates) {
+        if (!apply.isExist()) {
+            // scalar and IN subqueries need the aggregate output to be 
exposed by a join
+            return false;
+        }
+        if (havingFilterPulled && !predicates.hasHaving()) {
+            // EXISTS/NOT EXISTS without a HAVING clause only depends on the 
existence of the
+            // aggregation result, which is kept by grouping the inner side
+            return false;
+        }
+        if (predicates.hasAggregatePredicates() && 
agg.getGroupByExpressions().isEmpty()) {
+            // a predicate of the HAVING clause which references the outer 
query decides whether the
+            // row of a global aggregate is kept, and that row exists for 
every outer row including
+            // the rows of an empty correlated domain, so the predicate cannot 
be evaluated on a
+            // group of the inner side
+            return true;
+        }
+        for (Expression conjunct : correlatedPredicate) {
+            if (!isSupportedCorrelatedComparison(conjunct, 
apply.getCorrelationSlot())) {
+                // keep the behavior of the original rewrite, which reports 
these predicates
+                return false;
+            }
+        }
+        if (predicates.domainPredicates().stream()
+                .anyMatch(conjunct -> breaksDomainPartition(conjunct, 
apply.getCorrelationSlot()))) {
+            // the domain of one outer row is the union of several groups of 
the aggregate
+            return true;
+        }
+        if (predicates.hasAggregatePredicates()) {
+            // the domain of one outer row is exactly one group and the rows 
of the subquery are the
+            // rows of that group, so the original rewrite is still equivalent
+            return false;
+        }
+        if (!agg.getGroupByExpressions().isEmpty()) {
+            // an equality correlated predicate maps the domain of every outer 
row onto exactly one group
+            return false;
+        }
+        return havingMayHoldWithEmptyInput(agg, predicates.havingConjuncts());
+    }
+
+    /**
+     * Whether this predicate changes the domain of an outer row in a way 
which is not a group of the
+     * aggregate. Only an equality between the outer side and the inner side 
partitions the inner rows
+     * of one outer row into exactly the groups of the aggregate, while a 
predicate which does not
+     * reference the outer query at all just filters the inner rows.
+     */
+    private static boolean breaksDomainPartition(Expression conjunct, 
List<Slot> correlationSlots) {
+        if (conjunct instanceof EqualPredicate) {
+            return false;
+        }
+        return 
conjunct.getInputSlots().stream().anyMatch(correlationSlots::contains);
+    }
+
+    /**
+     * Whether the correlated predicate is a comparison whose sides do not mix 
the outer query and the
+     * subquery, eg. `inner.k &lt; outer.k` or `outer.k = inner.abs(k)`. Those 
are the predicates which
+     * can be evaluated by joining the two sides, and the ones supported by 
the original rewrite.
+     */
+    private static boolean isSupportedCorrelatedComparison(Expression 
conjunct, List<Slot> correlationSlots) {
+        Expression predicate = conjunct;
+        if (predicate instanceof Not && predicate.child(0) instanceof 
BinaryExpression) {
+            predicate = predicate.child(0);
+        }
+        if (!(predicate instanceof BinaryExpression)) {
+            return false;
+        }
+        Expression left = ((BinaryExpression) predicate).left();
+        Expression right = ((BinaryExpression) predicate).right();
+        Set<Slot> leftSlots = left.getInputSlots();
+        Set<Slot> rightSlots = right.getInputSlots();
+        boolean correlatedToLeft = !leftSlots.isEmpty() && 
leftSlots.stream().allMatch(correlationSlots::contains)
+                && rightSlots.stream().noneMatch(correlationSlots::contains);
+        boolean correlatedToRight = !rightSlots.isEmpty() && 
rightSlots.stream().allMatch(correlationSlots::contains)
+                && leftSlots.stream().noneMatch(correlationSlots::contains);
+        return correlatedToLeft || correlatedToRight;
+    }
+
+    /**
+     * Rewrite `outer [not] exists (select agg from inner where &lt;correlated 
predicate&gt;
+     * [group by ...] having ...)` into a semi/anti join whose right side 
aggregates the outer rows
+     * together with their correlated inner rows, so that the aggregation of 
one outer row is the
+     * aggregation of exactly the inner rows satisfying the correlated 
predicate:
+     *
+     * <pre>
+     * before:
+     *              Apply(EXISTS, correlationSlot=[outer.k])
+     *             /                \
+     *        outer             Filter(having)
+     *                              +-- Aggregate(group by [inner.g], count(*))
+     *                                    +-- Filter(correlated 
predicate(inner.k &lt; outer.k))
+     *                                          +-- inner
+     *
+     * after:
+     *          LEFT SEMI JOIN(otherJoinConjuncts=[outer.k &lt;=&gt; key.k])
+     *         /                 \
+     *     outer              Filter(having: count(*) =&gt; count(marker))
+     *                           +-- Aggregate(group by [key.k, inner.g], 
count(marker))
+     *                                 +-- LEFT OUTER JOIN(inner.k &lt; key.k) 
    // keeps the empty domain
+     *                                       |-- Aggregate(group by [k], 
output=[k])   // distinct correlated keys
+     *                                       |     +-- outer'
+     *                                       +-- Project(marker, ...)
+     *                                             +-- Filter(uncorrelated 
predicates)
+     *                                                   +-- inner
+     * </pre>
+     * outer' is a deep copy of outer, so that the two references of the outer 
plan have their own
+     * slots and relation ids. The LEFT OUTER JOIN and the marker are not 
needed (a plain inner join
+     * is used) when the aggregate has a group by, because such an aggregate 
returns no row at all
+     * for an empty input. The predicates of the HAVING clause which reference 
the outer query (they
+     * were pulled into the apply by {@link UnCorrelatedApplyFilter}) are 
evaluated above the
+     * aggregate of the outer rows, so that they decide on the aggregation of 
the whole domain of an
+     * outer row, the empty domain included.
+     *
+     * @return null if this rewrite cannot be applied safely, the caller then 
reports the subquery as
+     *         unsupported
+     */
+    private static Plan 
pullUpCorrelatedPredicateByAggregatingOuter(LogicalApply<?, ?> apply,
+            LogicalAggregate<?> agg, LogicalFilter<Plan> filter,
+            List<Expression> unCorrelatedPredicate, 
CorrelatedAggregatePredicates predicates) {
+        Set<Slot> correlationSlots = 
predicates.keySlots(apply.getCorrelationSlot());
+        if (containsSensitiveExpression(apply.left(), correlationSlots)
+                || hasNonDeterministicRows(apply.left())
+                || containsNoneMovableFunction(apply.right())
+                || containsSensitiveSubqueryExpression(agg, filter, predicates)
+                || referencesOuterSlot(apply.right(), 
ImmutableSet.copyOf(predicates.whereConjuncts),
+                        apply.getCorrelationSlot())
+                || !predicates.isResolvable(apply, agg, filter)) {
+            return null;
+        }
+
+        // the domains of two outer rows are the same as soon as their 
correlation slots are equal,
+        // so the correlation slots are the only outer information the 
aggregation needs
+        LogicalPlan outer = (LogicalPlan) apply.left();
+        LogicalPlan outerCopy = LogicalPlanDeepCopier.INSTANCE.deepCopy(outer, 
new DeepCopierContext());
+        List<Slot> outerOutput = outer.getOutput();
+        List<Slot> outerCopyOutput = outerCopy.getOutput();
+        Preconditions.checkState(outerOutput.size() == outerCopyOutput.size(),
+                "the deep copy of the outer plan changed its output size");
+        Map<Expression, Expression> slotToKey = Maps.newLinkedHashMap();
+        for (Slot slot : correlationSlots) {
+            int index = outerOutput.indexOf(slot);
+            if (index < 0) {
+                return null;
+            }
+            slotToKey.put(slot, outerCopyOutput.get(index));
+        }
+        List<NamedExpression> keyExpressions = new 
ArrayList<>(slotToKey.size());
+        for (Expression key : slotToKey.values()) {
+            keyExpressions.add((NamedExpression) key);
+        }
+        LogicalAggregate<Plan> keyAggregate = new LogicalAggregate<>(
+                ImmutableList.copyOf(slotToKey.values()), keyExpressions, 
outerCopy);
+
+        Plan inner = 
PlanUtils.filterOrSelf(ImmutableSet.copyOf(unCorrelatedPredicate), 
filter.child());
+        // An aggregate with a group by returns no row at all for an empty 
correlated domain, so the
+        // inner join below reproduces the behaviour of the subquery, which 
has no group to report.
+        // A global aggregate returns exactly one row for every outer row 
instead, the empty domain
+        // included, so the rows of an empty correlated domain have to be 
kept: the left outer join
+        // gives the aggregate one row whose inner columns are null, which is 
the same input the
+        // subquery aggregates for an empty input. Only the count aggregations 
tell "no row" and "one
+        // row of nulls" apart, and count(*) also counts the kept row itself, 
so the counts are
+        // replaced by counts of the projected marker, which is null for the 
rows which were kept for
+        // an empty correlated domain.
+        boolean keepEmptyDomain = agg.getGroupByExpressions().isEmpty();
+        Slot matchMarker = null;
+        if (keepEmptyDomain) {
+            Alias marker = new Alias(BooleanLiteral.TRUE, 
CORRELATION_MATCH_MARKER);
+            matchMarker = marker.toSlot();
+            List<NamedExpression> projects = Lists.newArrayList(marker);
+            projects.addAll(inner.getOutput());
+            inner = new LogicalProject<>(projects, inner);
+        }
+
+        List<Expression> domainConjuncts = 
predicates.domainPredicates().stream()
+                .map(conjunct -> ExpressionUtils.replace(conjunct, slotToKey))
+                .collect(ImmutableList.toImmutableList());
+        // a grouped aggregate reports an empty domain as "no row", a global 
aggregate as "one row"
+        Plan domainJoin = new LogicalJoin<>(keepEmptyDomain ? 
JoinType.LEFT_OUTER_JOIN : JoinType.INNER_JOIN,
+                ExpressionUtils.EMPTY_CONDITION, domainConjuncts, new 
DistributeHint(DistributeType.NONE),
+                Optional.empty(), keyAggregate, inner, null);
+
+        List<Expression> havingPredicates = predicates.havingPredicates();
+        Set<AggregateFunction> aggregates = Sets.newLinkedHashSet();
+        if (keepEmptyDomain) {
+            // the aggregation of the rewrite is computed over the rows of the 
inner side plus the
+            // row which the left outer join keeps for an empty correlated 
domain, and only the
+            // aggregates which ignore null arguments see that row as an empty 
input
+            for (Expression expression : agg.getOutputExpressions()) {
+                
aggregates.addAll(expression.collect(AggregateFunction.class::isInstance));
+            }
+            for (Expression conjunct : havingPredicates) {
+                
aggregates.addAll(conjunct.collect(AggregateFunction.class::isInstance));
+            }
+            if (aggregates.stream().anyMatch(function -> !(function instanceof 
NullIgnoringAggregateFunction))) {
+                // an aggregate which keeps null arguments cannot tell the row 
kept for an empty
+                // correlated domain from a row of the inner side
+                return null;
+            }
+        }
+        Map<Expression, Expression> compensated = 
guardAggregateArguments(aggregates, matchMarker);
+
+        List<Expression> newGroupBy = Lists.newArrayList(slotToKey.values());
+        newGroupBy.addAll(agg.getGroupByExpressions());
+        List<NamedExpression> newOutputs = Lists.newArrayList(keyExpressions);
+        for (NamedExpression output : agg.getOutputExpressions()) {
+            newOutputs.add((NamedExpression) ExpressionUtils.replace(output, 
compensated));
+        }
+        LogicalAggregate<Plan> newAggregate = new 
LogicalAggregate<>(newGroupBy, newOutputs, domainJoin);
+
+        Set<Expression> newHavingConjuncts = Sets.newLinkedHashSet();
+        for (Expression conjunct : havingPredicates) {
+            
newHavingConjuncts.add(ExpressionUtils.replace(ExpressionUtils.replace(conjunct,
 compensated), slotToKey));
+        }
+        Plan newRight = newHavingConjuncts.isEmpty() ? newAggregate
+                : new LogicalFilter<>(newHavingConjuncts, newAggregate);
+
+        // the aggregate of the subquery is now computed for the correlation 
keys of every outer row,
+        // so the outer rows which own one of those groups are the rows for 
which the subquery has rows
+        List<Expression> backConjuncts = new ArrayList<>(slotToKey.size());
+        for (Map.Entry<Expression, Expression> entry : slotToKey.entrySet()) {
+            backConjuncts.add(new NullSafeEqual(entry.getKey(), 
entry.getValue()));
+        }
+        return new LogicalJoin<>(apply.isNot() ? JoinType.LEFT_ANTI_JOIN : 
JoinType.LEFT_SEMI_JOIN,
+                ExpressionUtils.EMPTY_CONDITION, backConjuncts, new 
DistributeHint(DistributeType.NONE),
+                apply.getMarkJoinSlotReference(), outer, newRight, null);
+    }
+
+    /**
+     * Replace the arguments of the aggregates so that the row which the left 
outer join keeps for an
+     * empty correlated domain does not contribute to the aggregation: the 
marker of that row is
+     * null, so every argument is null for it and the aggregates which ignore 
null arguments (the
+     * caller only admits those) return the value of an empty input. 
`count(*)` has no argument to
+     * build that guard on, so it counts the marker instead; a distinct count 
keeps its argument and
+     * its distinct flag, because the null of the kept row must not be counted 
as a value.
+     */
+    private static Map<Expression, Expression> guardAggregateArguments(
+            Set<AggregateFunction> aggregates, Slot matchMarker) {
+        Map<Expression, Expression> replace = Maps.newHashMap();
+        for (AggregateFunction function : aggregates) {
+            if (function instanceof Count && ((Count) function).isCountStar() 
&& !function.isDistinct()) {
+                replace.put(function, new Count(matchMarker));
+                continue;
+            }
+            List<Expression> arguments = 
Lists.newArrayListWithCapacity(function.arity());
+            for (Expression argument : function.getArguments()) {
+                arguments.add(new If(matchMarker, argument, new 
NullLiteral(argument.getDataType())));
+            }
+            replace.put(function, function.withChildren(arguments));
+        }
+        return replace;
+    }
+
+    /**
+     * Whether the HAVING clause of a global aggregate can hold for the row 
which the aggregate
+     * returns for an empty input.
+     */
+    private static boolean havingMayHoldWithEmptyInput(LogicalAggregate<?> 
agg, Set<Expression> havingConjuncts) {
+        // the having clause usually references the output slots of the 
aggregate, but it may also
+        // contain the aggregate functions themselves, so both of them are 
replaced by the value
+        // which the aggregate returns for an empty input
+        Map<Expression, Expression> emptyValues = Maps.newHashMap();
+        for (NamedExpression output : agg.getOutputExpressions()) {
+            Expression expression = output instanceof Alias ? ((Alias) 
output).child() : output;
+            if (!(expression instanceof AggregateFunction)) {
+                continue;
+            }
+            Expression emptyValue = 
emptyValueForEmptyInput((AggregateFunction) expression);
+            if (emptyValue != null) {
+                emptyValues.put(output.toSlot(), emptyValue);
+                emptyValues.put(expression, emptyValue);
+            }
+        }
+        for (Expression conjunct : havingConjuncts) {
+            Expression substituted = emptyValues.isEmpty() ? conjunct
+                    : ExpressionUtils.replace(conjunct, emptyValues);
+            Expression folded = 
FoldConstantRuleOnFE.evaluateWithoutContext(substituted);
+            if (!(folded instanceof Literal)) {
+                // the aggregate returns an unknown value for an empty input, 
rewrite conservatively
+                return true;
+            }
+            if (!BooleanLiteral.TRUE.equals(folded)) {
+                // false or null: this conjunct filters the row of the empty 
input out
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * The value which an aggregate function returns for an empty input, or 
null if it cannot be
+     * decided.
+     */
+    private static Expression emptyValueForEmptyInput(AggregateFunction 
function) {
+        if (function instanceof Count) {
+            return new BigIntLiteral(0);
+        }
+        // only count is known to return a non null value for an empty input

Review Comment:
   [P1] Keep the legacy equality rewrite when HAVING rejects the known empty 
value. `SUM`, `AVG`, `MIN`, and `MAX` return SQL NULL on empty input, but this 
fallback treats them as unknown. Thus `Apply(EXISTS) -> outer Limit -> HAVING 
SUM(i.v) IS NOT NULL -> GlobalAggregate -> Filter(i.k = outer.k)` is classified 
as needing outer aggregation; that path rejects the Limit and turns a 
previously valid query into `Unsupported correlated subquery`, even though 
absence of the inner group exactly represents the rejected empty domain. This 
differs from the existing unsafe-fallback thread because outer aggregation is 
not required here. Model exact typed NULL empty results, and scan all HAVING 
conjuncts so a false/NULL conjunct dominates unknown ones, before selecting the 
outer-copy 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