|
18 | 18 | import org.elasticsearch.xpack.esql.capabilities.PostOptimizationVerificationAware; |
19 | 19 | import org.elasticsearch.xpack.esql.common.Failures; |
20 | 20 | import org.elasticsearch.xpack.esql.core.expression.Expression; |
| 21 | +import org.elasticsearch.xpack.esql.core.expression.FieldAttribute; |
21 | 22 | import org.elasticsearch.xpack.esql.core.expression.FoldContext; |
22 | 23 | import org.elasticsearch.xpack.esql.core.expression.Foldables; |
23 | 24 | import org.elasticsearch.xpack.esql.core.expression.Literal; |
24 | 25 | import org.elasticsearch.xpack.esql.core.expression.TypeResolutions; |
25 | 26 | import org.elasticsearch.xpack.esql.core.tree.NodeInfo; |
26 | 27 | import org.elasticsearch.xpack.esql.core.tree.Source; |
27 | 28 | import org.elasticsearch.xpack.esql.core.type.DataType; |
| 29 | +import org.elasticsearch.xpack.esql.core.type.MultiTypeEsField; |
| 30 | +import org.elasticsearch.xpack.esql.expression.SurrogateExpression; |
28 | 31 | import org.elasticsearch.xpack.esql.expression.function.Example; |
29 | 32 | import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; |
30 | 33 | import org.elasticsearch.xpack.esql.expression.function.FunctionType; |
31 | 34 | import org.elasticsearch.xpack.esql.expression.function.Param; |
32 | 35 | import org.elasticsearch.xpack.esql.expression.function.TwoOptionalArguments; |
33 | 36 | import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc; |
34 | 37 | import org.elasticsearch.xpack.esql.expression.function.scalar.math.Floor; |
| 38 | +import org.elasticsearch.xpack.esql.expression.function.scalar.math.RoundTo; |
35 | 39 | import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Div; |
36 | 40 | import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Mul; |
37 | 41 | import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput; |
| 42 | +import org.elasticsearch.xpack.esql.stats.SearchStats; |
38 | 43 |
|
39 | 44 | import java.io.IOException; |
40 | 45 | import java.time.ZoneId; |
41 | 46 | import java.time.ZoneOffset; |
42 | 47 | import java.util.ArrayList; |
| 48 | +import java.util.Arrays; |
43 | 49 | import java.util.List; |
| 50 | +import java.util.stream.Collectors; |
44 | 51 |
|
45 | 52 | import static org.elasticsearch.common.logging.LoggerMessageFormat.format; |
46 | 53 | import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; |
|
49 | 56 | import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.THIRD; |
50 | 57 | import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNumeric; |
51 | 58 | import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; |
| 59 | +import static org.elasticsearch.xpack.esql.core.type.DataType.isDateTime; |
52 | 60 | import static org.elasticsearch.xpack.esql.expression.Validations.isFoldable; |
53 | 61 | import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateTimeToLong; |
54 | 62 |
|
|
61 | 69 | public class Bucket extends GroupingFunction.EvaluatableGroupingFunction |
62 | 70 | implements |
63 | 71 | PostOptimizationVerificationAware, |
64 | | - TwoOptionalArguments { |
| 72 | + TwoOptionalArguments, |
| 73 | + SurrogateExpression { |
65 | 74 | public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Bucket", Bucket::new); |
66 | 75 |
|
67 | 76 | // TODO maybe we should just cover the whole of representable dates here - like ten years, 100 years, 1000 years, all the way up. |
@@ -301,15 +310,22 @@ public Rounding.Prepared getDateRoundingOrNull(FoldContext foldCtx) { |
301 | 310 | } |
302 | 311 |
|
303 | 312 | private Rounding.Prepared getDateRounding(FoldContext foldContext) { |
| 313 | + return getDateRounding(foldContext, null, null); |
| 314 | + } |
| 315 | + |
| 316 | + private Rounding.Prepared getDateRounding(FoldContext foldContext, Long min, Long max) { |
304 | 317 | assert field.dataType() == DataType.DATETIME || field.dataType() == DataType.DATE_NANOS : "expected date type; got " + field; |
305 | 318 | if (buckets.dataType().isWholeNumber()) { |
306 | 319 | int b = ((Number) buckets.fold(foldContext)).intValue(); |
307 | 320 | long f = foldToLong(foldContext, from); |
308 | 321 | long t = foldToLong(foldContext, to); |
| 322 | + if (min != null && max != null) { |
| 323 | + return new DateRoundingPicker(b, f, t).pickRounding().prepare(min, max); |
| 324 | + } |
309 | 325 | return new DateRoundingPicker(b, f, t).pickRounding().prepareForUnknown(); |
310 | 326 | } else { |
311 | 327 | assert DataType.isTemporalAmount(buckets.dataType()) : "Unexpected span data type [" + buckets.dataType() + "]"; |
312 | | - return DateTrunc.createRounding(buckets.fold(foldContext), DEFAULT_TZ); |
| 328 | + return DateTrunc.createRounding(buckets.fold(foldContext), DEFAULT_TZ, min, max); |
313 | 329 | } |
314 | 330 | } |
315 | 331 |
|
@@ -488,4 +504,40 @@ public Expression to() { |
488 | 504 | public String toString() { |
489 | 505 | return "Bucket{" + "field=" + field + ", buckets=" + buckets + ", from=" + from + ", to=" + to + '}'; |
490 | 506 | } |
| 507 | + |
| 508 | + @Override |
| 509 | + public Expression surrogate() { |
| 510 | + return null; |
| 511 | + } |
| 512 | + |
| 513 | + @Override |
| 514 | + public Expression surrogate(SearchStats searchStats) { |
| 515 | + if (field() instanceof FieldAttribute fa && fa.field() instanceof MultiTypeEsField == false && isDateTime(fa.dataType())) { |
| 516 | + // Extract min/max from SearchStats |
| 517 | + DataType fieldType = fa.dataType(); |
| 518 | + String fieldName = fa.fieldName(); |
| 519 | + var min = searchStats.min(fieldName); |
| 520 | + var max = searchStats.max(fieldName); |
| 521 | + // If min/max is available create rounding with them |
| 522 | + if (min != null && max != null && buckets().foldable()) { |
| 523 | + // System.out.println("field: " + fieldName + ", min string: " + dateWithTypeToString((Long) min, fieldType)); |
| 524 | + // System.out.println("field: " + fieldName + ", max string: " + dateWithTypeToString((Long) max, fieldType)); |
| 525 | + Rounding.Prepared rounding = getDateRounding(FoldContext.small(), (Long) min, (Long) max); |
| 526 | + // createRounding(foldedInterval, DEFAULT_TZ, (Long) min, (Long) max); |
| 527 | + long[] roundingPoints = rounding.fixedRoundingPoints(); |
| 528 | + // TODO do we support date_nanos? It seems like prepare(long minUtcMillis, long maxUtcMillis) takes millis only |
| 529 | + // the min/max long values for date and date_nanos are correct, however the roundingPoints for date_nanos is null |
| 530 | + // System.out.println("roundingPoints = " + Arrays.toString(roundingPoints)); |
| 531 | + if (roundingPoints == null) { |
| 532 | + return null; // TODO log this case |
| 533 | + } |
| 534 | + // Convert to round_to function with the roundings |
| 535 | + List<Expression> points = Arrays.stream(roundingPoints) |
| 536 | + .mapToObj(l -> new Literal(Source.EMPTY, l, fieldType)) |
| 537 | + .collect(Collectors.toList()); |
| 538 | + return new RoundTo(source(), field(), points); |
| 539 | + } |
| 540 | + } |
| 541 | + return null; |
| 542 | + } |
491 | 543 | } |
0 commit comments