Skip to content

Commit 3f233bf

Browse files
committed
Add configuration to DATE_TRUNC and related functions
1 parent 4c5adc0 commit 3f233bf

File tree

18 files changed

+253
-141
lines changed

18 files changed

+253
-141
lines changed

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ private static FunctionDefinition[][] functions() {
344344
new FunctionDefinition[] {
345345
def(Bucket.class, Bucket::new, "bucket", "bin"),
346346
def(Categorize.class, Categorize::new, "categorize"),
347-
def(TBucket.class, uni(TBucket::new), "tbucket") },
347+
def(TBucket.class, (UnaryConfigurationAwareBuilder<TBucket>) TBucket::new, "tbucket") },
348348
// aggregate functions
349349
// since they declare two public constructors - one with filter (for nested where) and one without
350350
// use casting to disambiguate between the two
@@ -1276,6 +1276,43 @@ protected interface TernaryConfigurationAwareBuilder<T> {
12761276
T build(Source source, Expression one, Expression two, Expression three, Configuration configuration);
12771277
}
12781278

1279+
/**
1280+
* Build a {@linkplain FunctionDefinition} for a quaternary function that is configuration aware.
1281+
*/
1282+
@SuppressWarnings("overloads") // These are ambiguous if you aren't using ctor references but we always do
1283+
protected static <T extends Function> FunctionDefinition def(
1284+
Class<T> function,
1285+
QuaternaryConfigurationAwareBuilder<T> ctorRef,
1286+
String... names
1287+
) {
1288+
FunctionBuilder builder = (source, children, cfg) -> {
1289+
if (OptionalArgument.class.isAssignableFrom(function)) {
1290+
if (children.size() > 4 || children.size() < 3) {
1291+
throw new QlIllegalArgumentException("expects three or four arguments");
1292+
}
1293+
} else if (TwoOptionalArguments.class.isAssignableFrom(function)) {
1294+
if (children.size() > 4 || children.size() < 2) {
1295+
throw new QlIllegalArgumentException("expects minimum two, maximum four arguments");
1296+
}
1297+
} else if (children.size() != 4) {
1298+
throw new QlIllegalArgumentException("expects exactly four arguments");
1299+
}
1300+
return ctorRef.build(
1301+
source,
1302+
children.get(0),
1303+
children.get(1),
1304+
children.size() > 2 ? children.get(2) : null,
1305+
children.size() > 3 ? children.get(3) : null,
1306+
cfg
1307+
);
1308+
};
1309+
return def(function, builder, names);
1310+
}
1311+
1312+
protected interface QuaternaryConfigurationAwareBuilder<T> {
1313+
T build(Source source, Expression one, Expression two, Expression three, Expression four, Configuration configuration);
1314+
}
1315+
12791316
//
12801317
// Utility functions to help disambiguate the method handle passed in.
12811318
// They work by providing additional method information to help the compiler know which method to pick.

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

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,13 @@
3535
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Div;
3636
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Mul;
3737
import org.elasticsearch.xpack.esql.io.stream.PlanStreamInput;
38+
import org.elasticsearch.xpack.esql.session.Configuration;
3839

3940
import java.io.IOException;
41+
import java.time.ZoneId;
4042
import java.util.ArrayList;
4143
import java.util.List;
44+
import java.util.Objects;
4245

4346
import static org.elasticsearch.common.logging.LoggerMessageFormat.format;
4447
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST;
@@ -48,7 +51,6 @@
4851
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNumeric;
4952
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType;
5053
import static org.elasticsearch.xpack.esql.expression.Validations.isFoldable;
51-
import static org.elasticsearch.xpack.esql.session.Configuration.DEFAULT_TZ;
5254
import static org.elasticsearch.xpack.esql.type.EsqlDataTypeConverter.dateTimeToLong;
5355

5456
/**
@@ -65,27 +67,34 @@ public class Bucket extends GroupingFunction.EvaluatableGroupingFunction
6567

6668
// TODO maybe we should just cover the whole of representable dates here - like ten years, 100 years, 1000 years, all the way up.
6769
// That way you never end up with more than the target number of buckets.
68-
private static final Rounding LARGEST_HUMAN_DATE_ROUNDING = Rounding.builder(Rounding.DateTimeUnit.YEAR_OF_CENTURY).build();
69-
private static final Rounding[] HUMAN_DATE_ROUNDINGS = new Rounding[] {
70-
Rounding.builder(Rounding.DateTimeUnit.MONTH_OF_YEAR).build(),
71-
Rounding.builder(Rounding.DateTimeUnit.WEEK_OF_WEEKYEAR).build(),
72-
Rounding.builder(Rounding.DateTimeUnit.DAY_OF_MONTH).build(),
73-
Rounding.builder(TimeValue.timeValueHours(12)).build(),
74-
Rounding.builder(TimeValue.timeValueHours(3)).build(),
75-
Rounding.builder(TimeValue.timeValueHours(1)).build(),
76-
Rounding.builder(TimeValue.timeValueMinutes(30)).build(),
77-
Rounding.builder(TimeValue.timeValueMinutes(10)).build(),
78-
Rounding.builder(TimeValue.timeValueMinutes(5)).build(),
79-
Rounding.builder(TimeValue.timeValueMinutes(1)).build(),
80-
Rounding.builder(TimeValue.timeValueSeconds(30)).build(),
81-
Rounding.builder(TimeValue.timeValueSeconds(10)).build(),
82-
Rounding.builder(TimeValue.timeValueSeconds(5)).build(),
83-
Rounding.builder(TimeValue.timeValueSeconds(1)).build(),
84-
Rounding.builder(TimeValue.timeValueMillis(100)).build(),
85-
Rounding.builder(TimeValue.timeValueMillis(50)).build(),
86-
Rounding.builder(TimeValue.timeValueMillis(10)).build(),
87-
Rounding.builder(TimeValue.timeValueMillis(1)).build(), };
88-
70+
private static final Rounding.Builder LARGEST_HUMAN_DATE_ROUNDING = Rounding.builder(Rounding.DateTimeUnit.YEAR_OF_CENTURY);
71+
private static final Rounding.Builder[] HUMAN_DATE_ROUNDINGS = new Rounding.Builder[] {
72+
Rounding.builder(Rounding.DateTimeUnit.MONTH_OF_YEAR),
73+
Rounding.builder(Rounding.DateTimeUnit.WEEK_OF_WEEKYEAR),
74+
Rounding.builder(Rounding.DateTimeUnit.DAY_OF_MONTH),
75+
Rounding.builder(TimeValue.timeValueHours(12)),
76+
Rounding.builder(TimeValue.timeValueHours(3)),
77+
Rounding.builder(TimeValue.timeValueHours(1)),
78+
Rounding.builder(TimeValue.timeValueMinutes(30)),
79+
Rounding.builder(TimeValue.timeValueMinutes(10)),
80+
Rounding.builder(TimeValue.timeValueMinutes(5)),
81+
Rounding.builder(TimeValue.timeValueMinutes(1)),
82+
Rounding.builder(TimeValue.timeValueSeconds(30)),
83+
Rounding.builder(TimeValue.timeValueSeconds(10)),
84+
Rounding.builder(TimeValue.timeValueSeconds(5)),
85+
Rounding.builder(TimeValue.timeValueSeconds(1)),
86+
Rounding.builder(TimeValue.timeValueMillis(100)),
87+
Rounding.builder(TimeValue.timeValueMillis(50)),
88+
Rounding.builder(TimeValue.timeValueMillis(10)),
89+
Rounding.builder(TimeValue.timeValueMillis(1)), };
90+
91+
/*
92+
* As Bucket already extends GroupingFunction, it can't extend EsqlConfigurationFunction, so we had to replicate here:
93+
* - The Configuration field
94+
* - HashCode
95+
* - Equals
96+
*/
97+
private final Configuration configuration;
8998
private final Expression field;
9099
private final Expression buckets;
91100
private final Expression from;
@@ -208,13 +217,15 @@ public Bucket(
208217
type = { "integer", "long", "double", "date", "keyword", "text" },
209218
optional = true,
210219
description = "End of the range. Can be a number, a date or a date expressed as a string."
211-
) Expression to
220+
) Expression to,
221+
Configuration configuration
212222
) {
213223
super(source, fields(field, buckets, from, to));
214224
this.field = field;
215225
this.buckets = buckets;
216226
this.from = from;
217227
this.to = to;
228+
this.configuration = configuration;
218229
}
219230

220231
private Bucket(StreamInput in) throws IOException {
@@ -223,10 +234,15 @@ private Bucket(StreamInput in) throws IOException {
223234
in.readNamedWriteable(Expression.class),
224235
in.readNamedWriteable(Expression.class),
225236
in.readOptionalNamedWriteable(Expression.class),
226-
in.readOptionalNamedWriteable(Expression.class)
237+
in.readOptionalNamedWriteable(Expression.class),
238+
((PlanStreamInput) in).configuration()
227239
);
228240
}
229241

242+
public Configuration configuration() {
243+
return configuration;
244+
}
245+
230246
private static List<Expression> fields(Expression field, Expression buckets, Expression from, Expression to) {
231247
List<Expression> list = new ArrayList<>(4);
232248
list.add(field);
@@ -308,19 +324,20 @@ public Rounding.Prepared getDateRounding(FoldContext foldContext, Long min, Long
308324
long f = foldToLong(foldContext, from);
309325
long t = foldToLong(foldContext, to);
310326
if (min != null && max != null) {
311-
return new DateRoundingPicker(b, f, t).pickRounding().prepare(min, max);
327+
return new DateRoundingPicker(b, f, t, configuration.zoneId()).pickRounding().prepare(min, max);
312328
}
313-
return new DateRoundingPicker(b, f, t).pickRounding().prepareForUnknown();
329+
return new DateRoundingPicker(b, f, t, configuration.zoneId()).pickRounding().prepareForUnknown();
314330
} else {
315331
assert DataType.isTemporalAmount(buckets.dataType()) : "Unexpected span data type [" + buckets.dataType() + "]";
316-
return DateTrunc.createRounding(buckets.fold(foldContext), DEFAULT_TZ, min, max);
332+
return DateTrunc.createRounding(buckets.fold(foldContext), configuration.zoneId(), min, max);
317333
}
318334
}
319335

320-
private record DateRoundingPicker(int buckets, long from, long to) {
336+
private record DateRoundingPicker(int buckets, long from, long to, ZoneId zoneId) {
321337
Rounding pickRounding() {
322-
Rounding prev = LARGEST_HUMAN_DATE_ROUNDING;
323-
for (Rounding r : HUMAN_DATE_ROUNDINGS) {
338+
Rounding prev = LARGEST_HUMAN_DATE_ROUNDING.timeZone(zoneId).build();
339+
for (Rounding.Builder builder : HUMAN_DATE_ROUNDINGS) {
340+
Rounding r = builder.timeZone(zoneId).build();
324341
if (roundingIsOk(r)) {
325342
prev = r;
326343
} else {
@@ -464,12 +481,12 @@ public DataType dataType() {
464481
public Expression replaceChildren(List<Expression> newChildren) {
465482
Expression from = newChildren.size() > 2 ? newChildren.get(2) : null;
466483
Expression to = newChildren.size() > 3 ? newChildren.get(3) : null;
467-
return new Bucket(source(), newChildren.get(0), newChildren.get(1), from, to);
484+
return new Bucket(source(), newChildren.get(0), newChildren.get(1), from, to, configuration);
468485
}
469486

470487
@Override
471488
protected NodeInfo<? extends Expression> info() {
472-
return NodeInfo.create(this, Bucket::new, field, buckets, from, to);
489+
return NodeInfo.create(this, Bucket::new, field, buckets, from, to, configuration);
473490
}
474491

475492
public Expression field() {
@@ -492,4 +509,19 @@ public Expression to() {
492509
public String toString() {
493510
return "Bucket{" + "field=" + field + ", buckets=" + buckets + ", from=" + from + ", to=" + to + '}';
494511
}
512+
513+
@Override
514+
public int hashCode() {
515+
return Objects.hash(getClass(), children(), configuration);
516+
}
517+
518+
@Override
519+
public boolean equals(Object obj) {
520+
if (super.equals(obj) == false) {
521+
return false;
522+
}
523+
Bucket other = (Bucket) obj;
524+
525+
return configuration.equals(other.configuration);
526+
}
495527
}

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

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@
1919
import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
2020
import org.elasticsearch.xpack.esql.expression.function.FunctionType;
2121
import org.elasticsearch.xpack.esql.expression.function.Param;
22+
import org.elasticsearch.xpack.esql.session.Configuration;
2223

2324
import java.io.IOException;
2425
import java.util.List;
26+
import java.util.Objects;
2527

2628
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.DEFAULT;
2729
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND;
@@ -33,6 +35,13 @@
3335
public class TBucket extends GroupingFunction.EvaluatableGroupingFunction implements SurrogateExpression {
3436
public static final String NAME = "TBucket";
3537

38+
/*
39+
* As Bucket already extends GroupingFunction, it can't extend EsqlConfigurationFunction, so we had to replicate here:
40+
* - The Configuration field
41+
* - HashCode
42+
* - Equals
43+
*/
44+
private final Configuration configuration;
3645
private final Expression buckets;
3746
private final Expression timestamp;
3847

@@ -62,19 +71,22 @@ public class TBucket extends GroupingFunction.EvaluatableGroupingFunction implem
6271
)
6372
public TBucket(
6473
Source source,
65-
@Param(name = "buckets", type = { "date_period", "time_duration" }, description = "Desired bucket size.") Expression buckets
74+
@Param(name = "buckets", type = { "date_period", "time_duration" }, description = "Desired bucket size.") Expression buckets,
75+
Configuration configuration
6676
) {
6777
this(
6878
source,
6979
buckets,
70-
new UnresolvedTimestamp(source, "TBucket function requires @timestamp field, but @timestamp was renamed or dropped")
80+
new UnresolvedTimestamp(source, "TBucket function requires @timestamp field, but @timestamp was renamed or dropped"),
81+
configuration
7182
);
7283
}
7384

74-
public TBucket(Source source, Expression buckets, Expression timestamp) {
85+
public TBucket(Source source, Expression buckets, Expression timestamp, Configuration configuration) {
7586
super(source, List.of(buckets, timestamp));
7687
this.buckets = buckets;
7788
this.timestamp = timestamp;
89+
this.configuration = configuration;
7890
}
7991

8092
@Override
@@ -94,7 +106,7 @@ public EvalOperator.ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvalua
94106

95107
@Override
96108
public Expression surrogate() {
97-
return new Bucket(source(), timestamp, buckets, null, null);
109+
return new Bucket(source(), timestamp, buckets, null, null, configuration);
98110
}
99111

100112
@Override
@@ -114,12 +126,12 @@ public DataType dataType() {
114126

115127
@Override
116128
public Expression replaceChildren(List<Expression> newChildren) {
117-
return new TBucket(source(), newChildren.get(0), newChildren.get(1));
129+
return new TBucket(source(), newChildren.get(0), newChildren.get(1), configuration);
118130
}
119131

120132
@Override
121133
protected NodeInfo<? extends Expression> info() {
122-
return NodeInfo.create(this, TBucket::new, buckets, timestamp);
134+
return NodeInfo.create(this, TBucket::new, buckets, timestamp, configuration);
123135
}
124136

125137
public Expression field() {
@@ -134,4 +146,19 @@ public Expression buckets() {
134146
public String toString() {
135147
return "TBucket{buckets=" + buckets + "}";
136148
}
149+
150+
@Override
151+
public int hashCode() {
152+
return Objects.hash(getClass(), children(), configuration);
153+
}
154+
155+
@Override
156+
public boolean equals(Object obj) {
157+
if (super.equals(obj) == false) {
158+
return false;
159+
}
160+
TBucket other = (TBucket) obj;
161+
162+
return configuration.equals(other.configuration);
163+
}
137164
}

0 commit comments

Comments
 (0)