2525import org .elasticsearch .xpack .esql .core .tree .NodeInfo ;
2626import org .elasticsearch .xpack .esql .core .tree .Source ;
2727import org .elasticsearch .xpack .esql .core .type .DataType ;
28+ import org .elasticsearch .xpack .esql .expression .LocalSurrogateExpression ;
2829import org .elasticsearch .xpack .esql .expression .function .Example ;
2930import org .elasticsearch .xpack .esql .expression .function .FunctionInfo ;
3031import org .elasticsearch .xpack .esql .expression .function .FunctionType ;
3536import org .elasticsearch .xpack .esql .expression .predicate .operator .arithmetic .Div ;
3637import org .elasticsearch .xpack .esql .expression .predicate .operator .arithmetic .Mul ;
3738import org .elasticsearch .xpack .esql .io .stream .PlanStreamInput ;
39+ import org .elasticsearch .xpack .esql .stats .SearchStats ;
3840
3941import java .io .IOException ;
40- import java .time .ZoneId ;
41- import java .time .ZoneOffset ;
4242import java .util .ArrayList ;
4343import java .util .List ;
4444
5050import static org .elasticsearch .xpack .esql .core .expression .TypeResolutions .isNumeric ;
5151import static org .elasticsearch .xpack .esql .core .expression .TypeResolutions .isType ;
5252import 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 ;
5355import static org .elasticsearch .xpack .esql .type .EsqlDataTypeConverter .dateTimeToLong ;
5456
5557/**
6163public class Bucket extends GroupingFunction .EvaluatableGroupingFunction
6264 implements
6365 PostOptimizationVerificationAware ,
64- TwoOptionalArguments {
66+ TwoOptionalArguments ,
67+ LocalSurrogateExpression {
6568 public static final NamedWriteableRegistry .Entry ENTRY = new NamedWriteableRegistry .Entry (Expression .class , "Bucket" , Bucket ::new );
6669
6770 // TODO maybe we should just cover the whole of representable dates here - like ten years, 100 years, 1000 years, all the way up.
@@ -87,8 +90,6 @@ public class Bucket extends GroupingFunction.EvaluatableGroupingFunction
8790 Rounding .builder (TimeValue .timeValueMillis (10 )).build (),
8891 Rounding .builder (TimeValue .timeValueMillis (1 )).build (), };
8992
90- private static final ZoneId DEFAULT_TZ = ZoneOffset .UTC ; // TODO: plug in the config
91-
9293 private final Expression field ;
9394 private final Expression buckets ;
9495 private final Expression from ;
@@ -301,15 +302,22 @@ public Rounding.Prepared getDateRoundingOrNull(FoldContext foldCtx) {
301302 }
302303
303304 private Rounding .Prepared getDateRounding (FoldContext foldContext ) {
305+ return getDateRounding (foldContext , null , null );
306+ }
307+
308+ private Rounding .Prepared getDateRounding (FoldContext foldContext , Long min , Long max ) {
304309 assert field .dataType () == DataType .DATETIME || field .dataType () == DataType .DATE_NANOS : "expected date type; got " + field ;
305310 if (buckets .dataType ().isWholeNumber ()) {
306311 int b = ((Number ) buckets .fold (foldContext )).intValue ();
307312 long f = foldToLong (foldContext , from );
308313 long t = foldToLong (foldContext , to );
314+ if (min != null && max != null ) {
315+ return new DateRoundingPicker (b , f , t ).pickRounding ().prepare (min , max );
316+ }
309317 return new DateRoundingPicker (b , f , t ).pickRounding ().prepareForUnknown ();
310318 } else {
311319 assert DataType .isTemporalAmount (buckets .dataType ()) : "Unexpected span data type [" + buckets .dataType () + "]" ;
312- return DateTrunc .createRounding (buckets .fold (foldContext ), DEFAULT_TZ );
320+ return DateTrunc .createRounding (buckets .fold (foldContext ), DEFAULT_TZ , min , max );
313321 }
314322 }
315323
@@ -488,4 +496,17 @@ public Expression to() {
488496 public String toString () {
489497 return "Bucket{" + "field=" + field + ", buckets=" + buckets + ", from=" + from + ", to=" + to + '}' ;
490498 }
499+
500+ @ Override
501+ public Expression surrogate (SearchStats searchStats ) {
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+ );
511+ }
491512}
0 commit comments