1212import org .elasticsearch .xpack .esql .core .expression .Alias ;
1313import org .elasticsearch .xpack .esql .core .expression .Attribute ;
1414import org .elasticsearch .xpack .esql .core .expression .AttributeSet ;
15+ import org .elasticsearch .xpack .esql .core .expression .Expression ;
1516import org .elasticsearch .xpack .esql .core .expression .FieldAttribute ;
1617import org .elasticsearch .xpack .esql .core .expression .Literal ;
1718import org .elasticsearch .xpack .esql .core .expression .NamedExpression ;
2930import org .elasticsearch .xpack .esql .rule .ParameterizedRule ;
3031
3132import java .util .ArrayList ;
33+ import java .util .HashMap ;
3234import java .util .List ;
3335import java .util .Map ;
3436import java .util .function .Predicate ;
3537
3638/**
37- * Look for any fields used in the plan that are missing locally and replace them with null.
39+ * Look for any fields used in the plan that are missing or that are constant locally and replace them with null or with the value .
3840 * This should minimize the plan execution, in the best scenario skipping its execution all together.
3941 */
40- public class ReplaceMissingFieldWithNull extends ParameterizedRule <LogicalPlan , LogicalPlan , LocalLogicalOptimizerContext > {
42+ public class ReplaceFieldWithConstant extends ParameterizedRule <LogicalPlan , LogicalPlan , LocalLogicalOptimizerContext > {
4143
4244 @ Override
4345 public LogicalPlan apply (LogicalPlan plan , LocalLogicalOptimizerContext localLogicalOptimizerContext ) {
4446 var lookupFieldsBuilder = AttributeSet .builder ();
47+ Map <Attribute , Expression > attrToValue = new HashMap <>();
4548 plan .forEachUp (EsRelation .class , esRelation -> {
4649 // Looking only for indices in LOOKUP mode is correct: during parsing, we assign the expected mode and even if a lookup index
4750 // is used in the FROM command, it will not be marked with LOOKUP mode there - but STANDARD.
@@ -52,6 +55,18 @@ public LogicalPlan apply(LogicalPlan plan, LocalLogicalOptimizerContext localLog
5255 if (esRelation .indexMode () == IndexMode .LOOKUP ) {
5356 lookupFieldsBuilder .addAll (esRelation .output ());
5457 }
58+ // find constant values only in the main indices
59+ else if (esRelation .indexMode () == IndexMode .STANDARD ) {
60+ for (Attribute attribute : esRelation .output ()) {
61+ if (attribute instanceof FieldAttribute fa ) {
62+ // Do not use the attribute name, this can deviate from the field name for union types; use fieldName() instead.
63+ var val = localLogicalOptimizerContext .searchStats ().constantValue (fa .fieldName ());
64+ if (val != null ) {
65+ attrToValue .put (attribute , Literal .of (attribute , val ));
66+ }
67+ }
68+ }
69+ }
5570 });
5671 AttributeSet lookupFields = lookupFieldsBuilder .build ();
5772
@@ -61,10 +76,10 @@ public LogicalPlan apply(LogicalPlan plan, LocalLogicalOptimizerContext localLog
6176 || localLogicalOptimizerContext .searchStats ().exists (f .fieldName ())
6277 || lookupFields .contains (f );
6378
64- return plan .transformUp (p -> missingToNull (p , shouldBeRetained ));
79+ return plan .transformUp (p -> missingToNull (p , shouldBeRetained , attrToValue ));
6580 }
6681
67- private LogicalPlan missingToNull (LogicalPlan plan , Predicate <FieldAttribute > shouldBeRetained ) {
82+ private LogicalPlan missingToNull (LogicalPlan plan , Predicate <FieldAttribute > shouldBeRetained , Map < Attribute , Expression > constants ) {
6883 if (plan instanceof EsRelation relation ) {
6984 // For any missing field, place an Eval right after the EsRelation to assign null values to that attribute (using the same name
7085 // id!), thus avoiding that InsertFieldExtrations inserts a field extraction later.
@@ -118,7 +133,10 @@ private LogicalPlan missingToNull(LogicalPlan plan, Predicate<FieldAttribute> sh
118133 || plan instanceof OrderBy
119134 || plan instanceof RegexExtract
120135 || plan instanceof TopN ) {
121- return plan .transformExpressionsOnlyUp (FieldAttribute .class , f -> shouldBeRetained .test (f ) ? f : Literal .of (f , null ));
136+ return plan .transformExpressionsOnlyUp (
137+ FieldAttribute .class ,
138+ f -> constants .getOrDefault (f , shouldBeRetained .test (f ) ? f : Literal .of (f , null ))
139+ );
122140 }
123141
124142 return plan ;
0 commit comments