1616import org .elasticsearch .xpack .esql .core .expression .FoldContext ;
1717import org .elasticsearch .xpack .esql .core .expression .Literal ;
1818import org .elasticsearch .xpack .esql .core .expression .ReferenceAttribute ;
19- import org .elasticsearch .xpack .esql .core .type .DataType ;
2019import org .elasticsearch .xpack .esql .core .util .CollectionUtils ;
2120import org .elasticsearch .xpack .esql .expression .predicate .Predicates ;
21+ import org .elasticsearch .xpack .esql .optimizer .LogicalOptimizerContext ;
2222import org .elasticsearch .xpack .esql .plan .logical .Enrich ;
2323import org .elasticsearch .xpack .esql .plan .logical .Eval ;
2424import org .elasticsearch .xpack .esql .plan .logical .Filter ;
4848 *
4949 * Also combines adjacent filters using a logical {@code AND}.
5050 */
51- public final class PushDownAndCombineFilters extends OptimizerRules .OptimizerRule <Filter > {
51+ public final class PushDownAndCombineFilters extends OptimizerRules .ParameterizedOptimizerRule <Filter , LogicalOptimizerContext > {
52+
53+ public PushDownAndCombineFilters () {
54+ super (OptimizerRules .TransformDirection .DOWN );
55+ }
56+
5257 @ Override
53- protected LogicalPlan rule (Filter filter ) {
58+ protected LogicalPlan rule (Filter filter , LogicalOptimizerContext ctx ) {
5459 LogicalPlan plan = filter ;
5560 LogicalPlan child = filter .child ();
5661 Expression condition = filter .condition ();
@@ -103,7 +108,7 @@ protected LogicalPlan rule(Filter filter) {
103108 // TODO: could we do better here about pushing down filters for inlinestats?
104109 // See also https://github.com/elastic/elasticsearch/issues/127497
105110 // Push down past INLINESTATS if the condition is on the groupings
106- return pushDownPastJoin (filter , join );
111+ return pushDownPastJoin (filter , join , ctx . foldCtx () );
107112 }
108113 // cannot push past a Limit, this could change the tailing result set returned
109114 return plan ;
@@ -130,7 +135,7 @@ private static ScopedFilter scopeFilter(List<Expression> filters, LogicalPlan le
130135 return new ScopedFilter (rest , leftFilters , rightFilters );
131136 }
132137
133- private static LogicalPlan pushDownPastJoin (Filter filter , Join join ) {
138+ private static LogicalPlan pushDownPastJoin (Filter filter , Join join , FoldContext foldCtx ) {
134139 LogicalPlan plan = filter ;
135140 // pushdown only through LEFT joins
136141 // TODO: generalize this for other join types
@@ -155,8 +160,10 @@ private static LogicalPlan pushDownPastJoin(Filter filter, Join join) {
155160 optimizationApplied = true ;
156161 }
157162 // push the right scoped filter down to the right child
158- if (scoped .rightFilters ().isEmpty () == false && (join .right () instanceof Filter == false )) {
159- List <Expression > rightPushableFilters = buildRightPushableFilters (scoped .rightFilters ());
163+ // We don't want to execute this rule more than once per join, so we check if we
164+ // already pushed some filters to the right, and we only apply the rule if join.candidateRightHandFilters() is not empty
165+ if (scoped .rightFilters ().isEmpty () == false && join .candidateRightHandFilters ().isEmpty ()) {
166+ List <Expression > rightPushableFilters = buildRightPushableFilters (scoped .rightFilters (), foldCtx );
160167 if (rightPushableFilters .isEmpty () == false ) {
161168 join = join .withCandidateRightHandFilters (rightPushableFilters );
162169 optimizationApplied = true ;
@@ -195,24 +202,24 @@ private static LogicalPlan pushDownPastJoin(Filter filter, Join join) {
195202 /**
196203 * Builds the right pushable filters for the given expressions.
197204 */
198- private static List <Expression > buildRightPushableFilters (List <Expression > expressions ) {
199- return expressions .stream ().filter (x -> isRightPushableFilter (x )).toList ();
205+ private static List <Expression > buildRightPushableFilters (List <Expression > expressions , FoldContext foldCtx ) {
206+ return expressions .stream ().filter (x -> isRightPushableFilter (x , foldCtx )).toList ();
200207 }
201208
202209 /**
203210 * Determines if the given expression can be pushed down to the right side of a join.
204211 * A filter is right pushable if the filter's predicate evaluates to false or null when all fields are set to null
205212 */
206- private static boolean isRightPushableFilter (Expression filter ) {
213+ private static boolean isRightPushableFilter (Expression filter , FoldContext foldCtx ) {
207214 // traverse the filter tree
208215 // replace any reference to an attribute with a null literal
209- Expression nullifiedFilter = filter .transformUp (Attribute .class , r -> new Literal (r .source (), null , DataType . NULL ));
216+ Expression nullifiedFilter = filter .transformUp (Attribute .class , r -> new Literal (r .source (), null , r . dataType () ));
210217 // try to fold the filter
211218 // check if the folded filter evaluates to false or null, if yes return true
212219 // pushable WHERE field > 1 (evaluates to null), WHERE field is NOT NULL (evaluates to false)
213- // not pushable WHERE field is NULL (evaluates to true), WHERE coalesce(field, 10) = 10 (evaluates to true)
220+ // not pushable WHERE field is NULL (evaluates to true), WHERE coalesce(field, 10) == 10 (evaluates to true)
214221 if (nullifiedFilter .foldable ()) {
215- Object folded = nullifiedFilter .fold (FoldContext . small () );
222+ Object folded = nullifiedFilter .fold (foldCtx );
216223 return folded == null || Boolean .FALSE .equals (folded );
217224 }
218225 return false ;
0 commit comments