|
15 | 15 | import org.elasticsearch.xpack.esql.core.expression.Expressions; |
16 | 16 | import org.elasticsearch.xpack.esql.core.expression.ReferenceAttribute; |
17 | 17 | import org.elasticsearch.xpack.esql.core.expression.predicate.Predicates; |
| 18 | +import org.elasticsearch.xpack.esql.core.util.CollectionUtils; |
18 | 19 | import org.elasticsearch.xpack.esql.plan.logical.Enrich; |
19 | 20 | import org.elasticsearch.xpack.esql.plan.logical.Eval; |
20 | 21 | import org.elasticsearch.xpack.esql.plan.logical.Filter; |
|
23 | 24 | import org.elasticsearch.xpack.esql.plan.logical.Project; |
24 | 25 | import org.elasticsearch.xpack.esql.plan.logical.RegexExtract; |
25 | 26 | import org.elasticsearch.xpack.esql.plan.logical.UnaryPlan; |
| 27 | +import org.elasticsearch.xpack.esql.plan.logical.join.Join; |
| 28 | +import org.elasticsearch.xpack.esql.plan.logical.join.JoinTypes; |
26 | 29 |
|
27 | 30 | import java.util.ArrayList; |
28 | 31 | import java.util.List; |
@@ -76,11 +79,63 @@ protected LogicalPlan rule(Filter filter) { |
76 | 79 | } else if (child instanceof OrderBy orderBy) { |
77 | 80 | // swap the filter with its child |
78 | 81 | plan = orderBy.replaceChild(filter.with(orderBy.child(), condition)); |
| 82 | + } else if (child instanceof Join join) { |
| 83 | + return pushDownPastJoin(filter, join); |
79 | 84 | } |
80 | 85 | // cannot push past a Limit, this could change the tailing result set returned |
81 | 86 | return plan; |
82 | 87 | } |
83 | 88 |
|
| 89 | + private record ScopedFilter(List<Expression> commonFilters, List<Expression> leftFilters, List<Expression> rightFilters) {} |
| 90 | + |
| 91 | + // split the filter condition in 3 parts: |
| 92 | + // 1. filter scoped to the left |
| 93 | + // 2. filter scoped to the right |
| 94 | + // 3. filter that requires both sides to be evaluated |
| 95 | + private static ScopedFilter scopeFilter(List<Expression> filters, LogicalPlan left, LogicalPlan right) { |
| 96 | + List<Expression> rest = new ArrayList<>(filters); |
| 97 | + List<Expression> leftFilters = new ArrayList<>(); |
| 98 | + List<Expression> rightFilters = new ArrayList<>(); |
| 99 | + |
| 100 | + AttributeSet leftOutput = left.outputSet(); |
| 101 | + AttributeSet rightOutput = right.outputSet(); |
| 102 | + |
| 103 | + // first remove things that are left scoped only |
| 104 | + rest.removeIf(f -> f.references().subsetOf(leftOutput) && leftFilters.add(f)); |
| 105 | + // followed by right scoped only |
| 106 | + rest.removeIf(f -> f.references().subsetOf(rightOutput) && rightFilters.add(f)); |
| 107 | + return new ScopedFilter(rest, leftFilters, rightFilters); |
| 108 | + } |
| 109 | + |
| 110 | + private static LogicalPlan pushDownPastJoin(Filter filter, Join join) { |
| 111 | + LogicalPlan plan = filter; |
| 112 | + // pushdown only through LEFT joins |
| 113 | + // TODO: generalize this for other join types |
| 114 | + if (join.config().type() == JoinTypes.LEFT) { |
| 115 | + LogicalPlan left = join.left(); |
| 116 | + LogicalPlan right = join.right(); |
| 117 | + |
| 118 | + // split the filter condition in 3 parts: |
| 119 | + // 1. filter scoped to the left |
| 120 | + // 2. filter scoped to the right |
| 121 | + // 3. filter that requires both sides to be evaluated |
| 122 | + ScopedFilter scoped = scopeFilter(Predicates.splitAnd(filter.condition()), left, right); |
| 123 | + // push the left scoped filter down to the left child, keep the rest intact |
| 124 | + if (scoped.leftFilters.size() > 0) { |
| 125 | + // push the filter down to the left child |
| 126 | + left = new Filter(left.source(), left, Predicates.combineAnd(scoped.leftFilters)); |
| 127 | + // update the join with the new left child |
| 128 | + join = (Join) join.replaceLeft(left); |
| 129 | + |
| 130 | + // keep the remaining filters in place, otherwise return the new join; |
| 131 | + Expression remainingFilter = Predicates.combineAnd(CollectionUtils.combine(scoped.commonFilters, scoped.rightFilters)); |
| 132 | + plan = remainingFilter != null ? filter.with(join, remainingFilter) : join; |
| 133 | + } |
| 134 | + } |
| 135 | + // ignore the rest of the join |
| 136 | + return plan; |
| 137 | + } |
| 138 | + |
84 | 139 | private static Function<Expression, Expression> NO_OP = expression -> expression; |
85 | 140 |
|
86 | 141 | private static LogicalPlan maybePushDownPastUnary( |
|
0 commit comments