Skip to content

Commit 4e82da7

Browse files
refactor according to review comments
1 parent 1d453f3 commit 4e82da7

File tree

11 files changed

+85
-221
lines changed

11 files changed

+85
-221
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.esql.expression;
9+
10+
import org.elasticsearch.xpack.esql.core.expression.Expression;
11+
import org.elasticsearch.xpack.esql.stats.SearchStats;
12+
13+
/**
14+
* Interface signaling to the local logical plan optimizer that the declaring expression
15+
* has to be replaced by a different form.
16+
* Implement this on {@code Function}s when:
17+
* <ul>
18+
* <li>The expression can be rewritten to another expression on data node, with the statistics available in SearchStats.
19+
* Like {@code DateTrunc} and {@code Bucket} could be rewritten to {@code RoundTo} with the min/max values on the date field.
20+
* </li>
21+
* </ul>
22+
*/
23+
public interface LocalSurrogateExpression {
24+
/**
25+
* Returns the expression to be replaced by or {@code null} if this cannot be replaced.
26+
*/
27+
Expression surrogate(SearchStats searchStats);
28+
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/SurrogateExpression.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
import org.elasticsearch.xpack.esql.core.expression.Expression;
1111
import org.elasticsearch.xpack.esql.expression.function.aggregate.AggregateFunction;
12-
import org.elasticsearch.xpack.esql.stats.SearchStats;
1312

1413
/**
1514
* Interface signaling to the planner that the declaring expression
@@ -29,8 +28,4 @@ public interface SurrogateExpression {
2928
* be replaced.
3029
*/
3130
Expression surrogate();
32-
33-
default Expression surrogate(SearchStats searchStats) {
34-
return null;
35-
}
3631
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/grouping/Bucket.java

Lines changed: 13 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,42 +14,33 @@
1414
import org.elasticsearch.common.io.stream.StreamOutput;
1515
import org.elasticsearch.compute.operator.EvalOperator.ExpressionEvaluator;
1616
import org.elasticsearch.core.TimeValue;
17-
import org.elasticsearch.logging.LogManager;
18-
import org.elasticsearch.logging.Logger;
1917
import org.elasticsearch.xpack.esql.EsqlIllegalArgumentException;
2018
import org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware;
2119
import org.elasticsearch.xpack.esql.common.Failures;
2220
import org.elasticsearch.xpack.esql.core.expression.Expression;
23-
import org.elasticsearch.xpack.esql.core.expression.FieldAttribute;
2421
import org.elasticsearch.xpack.esql.core.expression.FoldContext;
2522
import org.elasticsearch.xpack.esql.core.expression.Foldables;
2623
import org.elasticsearch.xpack.esql.core.expression.Literal;
2724
import org.elasticsearch.xpack.esql.core.expression.TypeResolutions;
2825
import org.elasticsearch.xpack.esql.core.tree.NodeInfo;
2926
import org.elasticsearch.xpack.esql.core.tree.Source;
3027
import org.elasticsearch.xpack.esql.core.type.DataType;
31-
import org.elasticsearch.xpack.esql.core.type.MultiTypeEsField;
32-
import org.elasticsearch.xpack.esql.expression.SurrogateExpression;
28+
import org.elasticsearch.xpack.esql.expression.LocalSurrogateExpression;
3329
import org.elasticsearch.xpack.esql.expression.function.Example;
3430
import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
3531
import org.elasticsearch.xpack.esql.expression.function.FunctionType;
3632
import org.elasticsearch.xpack.esql.expression.function.Param;
3733
import org.elasticsearch.xpack.esql.expression.function.TwoOptionalArguments;
3834
import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc;
3935
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Floor;
40-
import org.elasticsearch.xpack.esql.expression.function.scalar.math.RoundTo;
4136
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Div;
4237
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Mul;
4338
import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
4439
import org.elasticsearch.xpack.esql.stats.SearchStats;
4540

4641
import java.io.IOException;
47-
import java.time.ZoneId;
48-
import java.time.ZoneOffset;
4942
import java.util.ArrayList;
50-
import java.util.Arrays;
5143
import java.util.List;
52-
import java.util.stream.Collectors;
5344

5445
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
5546
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST;
@@ -58,10 +49,10 @@
5849
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.THIRD;
5950
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNumeric;
6051
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType;
61-
import static org.elasticsearch.xpack.esql.core.type.DataType.isDateTime;
6252
import static org.elasticsearch.xpack.esql.expression.Validations.isFoldable;
53+
import static org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc.maybeSubstituteWithRoundTo;
54+
import static org.elasticsearch.xpack.esql.session.Configuration.DEFAULT_TZ;
6355
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateTimeToLong;
64-
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateWithTypeToString;
6556

6657
/**
6758
* Splits dates and numbers into a given number of buckets. There are two ways to invoke
@@ -73,11 +64,9 @@ public class Bucket extends GroupingFunction.EvaluatableGroupingFunction
7364
implements
7465
PostOptimizationVerificationAware,
7566
TwoOptionalArguments,
76-
SurrogateExpression {
67+
LocalSurrogateExpression {
7768
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Bucket", Bucket::new);
7869

79-
private static final Logger logger = LogManager.getLogger(Bucket.class);
80-
8170
// TODO maybe we should just cover the whole of representable dates here - like ten years, 100 years, 1000 years, all the way up.
8271
// That way you never end up with more than the target number of buckets.
8372
private static final Rounding LARGEST_HUMAN_DATE_ROUNDING = Rounding.builder(Rounding.DateTimeUnit.YEAR_OF_CENTURY).build();
@@ -101,8 +90,6 @@ public class Bucket extends GroupingFunction.EvaluatableGroupingFunction
10190
Rounding.builder(TimeValue.timeValueMillis(10)).build(),
10291
Rounding.builder(TimeValue.timeValueMillis(1)).build(), };
10392

104-
private static final ZoneId DEFAULT_TZ = ZoneOffset.UTC; // TODO: plug in the config
105-
10693
private final Expression field;
10794
private final Expression buckets;
10895
private final Expression from;
@@ -510,41 +497,16 @@ public String toString() {
510497
return "Bucket{" + "field=" + field + ", buckets=" + buckets + ", from=" + from + ", to=" + to + '}';
511498
}
512499

513-
@Override
514-
public Expression surrogate() {
515-
return null;
516-
}
517-
518500
@Override
519501
public Expression surrogate(SearchStats searchStats) {
520-
if (field() instanceof FieldAttribute fa && fa.field() instanceof MultiTypeEsField == false && isDateTime(fa.dataType())) {
521-
// Extract min/max from SearchStats
522-
DataType fieldType = fa.dataType();
523-
FieldAttribute.FieldName fieldName = fa.fieldName();
524-
var min = searchStats.min(fieldName);
525-
var max = searchStats.max(fieldName);
526-
// If min/max is available create rounding with them
527-
if (min instanceof Long minValue && max instanceof Long maxValue && buckets().foldable()) {
528-
Rounding.Prepared rounding = getDateRounding(FoldContext.small(), minValue, maxValue);
529-
long[] roundingPoints = rounding.fixedRoundingPoints();
530-
if (roundingPoints == null) {
531-
logger.trace(
532-
"Fixed rounding point is null for field {}, minValue {} in string format {} and maxValue {} in string format {}",
533-
fieldName,
534-
minValue,
535-
dateWithTypeToString(minValue, fieldType),
536-
maxValue,
537-
dateWithTypeToString(maxValue, fieldType)
538-
);
539-
return null;
540-
}
541-
// Convert to round_to function with the roundings
542-
List<Expression> points = Arrays.stream(roundingPoints)
543-
.mapToObj(l -> new Literal(Source.EMPTY, l, fieldType))
544-
.collect(Collectors.toList());
545-
return new RoundTo(source(), field(), points);
546-
}
547-
}
548-
return null;
502+
// LocalSubstituteSurrogateExpressions should make sure this doesn't happen
503+
assert searchStats != null : "SearchStats cannot be null";
504+
return maybeSubstituteWithRoundTo(
505+
source(),
506+
field(),
507+
buckets(),
508+
searchStats,
509+
(interval, minValue, maxValue) -> getDateRounding(FoldContext.small(), minValue, maxValue)
510+
);
549511
}
550512
}

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/BinaryDateTimeFunction.java

Lines changed: 0 additions & 62 deletions
This file was deleted.

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateDiff.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
public class DateDiff extends EsqlScalarFunction {
5858
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "DateDiff", DateDiff::new);
5959

60-
public static final ZoneId UTC = ZoneId.of("Z");
60+
public static final ZoneId UTC = org.elasticsearch.xpack.esql.core.util.DateUtils.UTC;
6161

6262
private final Expression unit;
6363
private final Expression startTimestamp;

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/expression/function/scalar/date/DateTrunc.java

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package org.elasticsearch.xpack.esql.expression.function.scalar.date;
99

1010
import org.elasticsearch.common.Rounding;
11+
import org.elasticsearch.common.TriFunction;
1112
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1213
import org.elasticsearch.common.io.stream.StreamInput;
1314
import org.elasticsearch.common.io.stream.StreamOutput;
@@ -26,7 +27,7 @@
2627
import org.elasticsearch.xpack.esql.core.tree.Source;
2728
import org.elasticsearch.xpack.esql.core.type.DataType;
2829
import org.elasticsearch.xpack.esql.core.type.MultiTypeEsField;
29-
import org.elasticsearch.xpack.esql.expression.SurrogateExpression;
30+
import org.elasticsearch.xpack.esql.expression.LocalSurrogateExpression;
3031
import org.elasticsearch.xpack.esql.expression.function.Example;
3132
import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
3233
import org.elasticsearch.xpack.esql.expression.function.Param;
@@ -39,7 +40,6 @@
3940
import java.time.Duration;
4041
import java.time.Period;
4142
import java.time.ZoneId;
42-
import java.time.ZoneOffset;
4343
import java.util.Arrays;
4444
import java.util.List;
4545
import java.util.Map;
@@ -52,9 +52,10 @@
5252
import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME;
5353
import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS;
5454
import static org.elasticsearch.xpack.esql.core.type.DataType.isDateTime;
55+
import static org.elasticsearch.xpack.esql.session.Configuration.DEFAULT_TZ;
5556
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateWithTypeToString;
5657

57-
public class DateTrunc extends EsqlScalarFunction implements SurrogateExpression {
58+
public class DateTrunc extends EsqlScalarFunction implements LocalSurrogateExpression {
5859
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
5960
Expression.class,
6061
"DateTrunc",
@@ -74,7 +75,6 @@ public interface DateTruncFactoryProvider {
7475
);
7576
private final Expression interval;
7677
private final Expression timestampField;
77-
protected static final ZoneId DEFAULT_TZ = ZoneOffset.UTC;
7878

7979
@FunctionInfo(
8080
returnType = { "date", "date_nanos" },
@@ -287,22 +287,35 @@ public static ExpressionEvaluator.Factory evaluator(
287287
}
288288

289289
@Override
290-
public Expression surrogate() { // there is no substitute without SearchStats
291-
return null;
290+
public Expression surrogate(SearchStats searchStats) {
291+
// LocalSubstituteSurrogateExpressions should make sure this doesn't happen
292+
assert searchStats != null : "SearchStats cannot be null";
293+
return maybeSubstituteWithRoundTo(
294+
source(),
295+
field(),
296+
interval(),
297+
searchStats,
298+
(interval, minValue, maxValue) -> createRounding(interval, DEFAULT_TZ, minValue, maxValue)
299+
);
292300
}
293301

294-
@Override
295-
public Expression surrogate(SearchStats searchStats) {
296-
if (field() instanceof FieldAttribute fa && fa.field() instanceof MultiTypeEsField == false && isDateTime(fa.dataType())) {
302+
public static RoundTo maybeSubstituteWithRoundTo(
303+
Source source,
304+
Expression field,
305+
Expression foldableTimeExpression,
306+
SearchStats searchStats,
307+
TriFunction<Object, Long, Long, Rounding.Prepared> roundingFunction
308+
) {
309+
if (field instanceof FieldAttribute fa && fa.field() instanceof MultiTypeEsField == false && isDateTime(fa.dataType())) {
297310
// Extract min/max from SearchStats
298311
DataType fieldType = fa.dataType();
299312
FieldAttribute.FieldName fieldName = fa.fieldName();
300313
var min = searchStats.min(fieldName);
301314
var max = searchStats.max(fieldName);
302315
// If min/max is available create rounding with them
303-
if (min instanceof Long minValue && max instanceof Long maxValue && interval().foldable()) {
304-
Object foldedInterval = interval().fold(FoldContext.small() /* TODO remove me */);
305-
Rounding.Prepared rounding = createRounding(foldedInterval, DEFAULT_TZ, minValue, maxValue);
316+
if (min instanceof Long minValue && max instanceof Long maxValue && foldableTimeExpression.foldable()) {
317+
Object foldedInterval = foldableTimeExpression.fold(FoldContext.small() /* TODO remove me */);
318+
Rounding.Prepared rounding = roundingFunction.apply(foldedInterval, minValue, maxValue);
306319
long[] roundingPoints = rounding.fixedRoundingPoints();
307320
if (roundingPoints == null) {
308321
logger.trace(
@@ -319,7 +332,7 @@ public Expression surrogate(SearchStats searchStats) {
319332
List<Expression> points = Arrays.stream(roundingPoints)
320333
.mapToObj(l -> new Literal(Source.EMPTY, l, fieldType))
321334
.collect(Collectors.toList());
322-
return new RoundTo(source(), field(), points);
335+
return new RoundTo(source, field, points);
323336
}
324337
}
325338
return null;

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/optimizer/LocalLogicalPlanOptimizer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.InferIsNotNull;
1616
import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.InferNonNullAggConstraint;
1717
import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.LocalPropagateEmptyRelation;
18+
import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.LocalSubstituteSurrogateExpressions;
1819
import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.ReplaceFieldWithConstantOrNull;
1920
import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.ReplaceTopNWithLimitAndSort;
20-
import org.elasticsearch.xpack.esql.optimizer.rules.logical.local.SubstituteSurrogateExpressionsWithSearchStats;
2121
import org.elasticsearch.xpack.esql.plan.logical.LogicalPlan;
2222
import org.elasticsearch.xpack.esql.rule.ParameterizedRuleExecutor;
2323
import org.elasticsearch.xpack.esql.rule.Rule;
@@ -48,7 +48,7 @@ public class LocalLogicalPlanOptimizer extends ParameterizedRuleExecutor<Logical
4848
new ReplaceFieldWithConstantOrNull(),
4949
new InferIsNotNull(),
5050
new InferNonNullAggConstraint(),
51-
new SubstituteSurrogateExpressionsWithSearchStats()
51+
new LocalSubstituteSurrogateExpressions()
5252
),
5353
localOperators(),
5454
cleanup()

0 commit comments

Comments
 (0)