Skip to content

Commit 76e3726

Browse files
committed
Add "emitEmptyBuckets" parameter to the "Bucket" function.
1 parent 788e18f commit 76e3726

File tree

7 files changed

+96
-16
lines changed

7 files changed

+96
-16
lines changed

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ private static FunctionDefinition[][] functions() {
301301
return new FunctionDefinition[][] {
302302
// grouping functions
303303
new FunctionDefinition[] {
304-
def(Bucket.class, Bucket::new, "bucket", "bin"),
304+
def(Bucket.class, quin(Bucket::new), "bucket", "bin"),
305305
def(Categorize.class, Categorize::new, "categorize") },
306306
// aggregate functions
307307
// since they declare two public constructors - one with filter (for nested where) and one without
@@ -1010,6 +1010,39 @@ protected static <T extends Function> FunctionDefinition def(Class<T> function,
10101010
return def(function, builder, names);
10111011
}
10121012

1013+
/**
1014+
* Build a {@linkplain FunctionDefinition} for a quinary function.
1015+
*/
1016+
@SuppressWarnings("overloads") // These are ambiguous if you aren't using ctor references but we always do
1017+
protected static <T extends Function> FunctionDefinition def(Class<T> function, QuinaryBuilder<T> ctorRef, String... names) {
1018+
FunctionBuilder builder = (source, children, cfg) -> {
1019+
if (OptionalArgument.class.isAssignableFrom(function)) {
1020+
if (children.size() > 5 || children.size() < 4) {
1021+
throw new QlIllegalArgumentException("expects four or five arguments");
1022+
}
1023+
} else if (TwoOptionalArguments.class.isAssignableFrom(function)) {
1024+
if (children.size() > 5 || children.size() < 3) {
1025+
throw new QlIllegalArgumentException("expects minimum three, maximum five arguments");
1026+
}
1027+
} else if (ThreeOptionalArguments.class.isAssignableFrom(function)) {
1028+
if (children.size() > 5 || children.size() < 2) {
1029+
throw new QlIllegalArgumentException("expects minimum two, maximum five arguments");
1030+
}
1031+
} else if (children.size() != 5) {
1032+
throw new QlIllegalArgumentException("expects exactly five arguments");
1033+
}
1034+
return ctorRef.build(
1035+
source,
1036+
children.get(0),
1037+
children.get(1),
1038+
children.size() > 2 ? children.get(2) : null,
1039+
children.size() > 3 ? children.get(3) : null,
1040+
children.size() > 4 ? children.get(4) : null
1041+
);
1042+
};
1043+
return def(function, builder, names);
1044+
}
1045+
10131046
protected interface QuaternaryBuilder<T> {
10141047
T build(Source source, Expression one, Expression two, Expression three, Expression four);
10151048
}
@@ -1204,4 +1237,7 @@ private static <T extends Function> TernaryBuilder<T> tri(TernaryBuilder<T> func
12041237
return function;
12051238
}
12061239

1240+
private static <T extends Function> QuinaryBuilder<T> quin(QuinaryBuilder<T> function) {
1241+
return function;
1242+
}
12071243
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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.function;
9+
10+
/**
11+
* Marker interface indicating that a function accepts three optional arguments (the last three).
12+
* This is used by the {@link EsqlFunctionRegistry} to perform validation of function declaration.
13+
*/
14+
public interface ThreeOptionalArguments {
15+
16+
}

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

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import org.elasticsearch.xpack.esql.expression.function.FunctionInfo;
3030
import org.elasticsearch.xpack.esql.expression.function.FunctionType;
3131
import org.elasticsearch.xpack.esql.expression.function.Param;
32-
import org.elasticsearch.xpack.esql.expression.function.TwoOptionalArguments;
32+
import org.elasticsearch.xpack.esql.expression.function.ThreeOptionalArguments;
3333
import org.elasticsearch.xpack.esql.expression.function.scalar.date.DateTrunc;
3434
import org.elasticsearch.xpack.esql.expression.function.scalar.math.Floor;
3535
import org.elasticsearch.xpack.esql.expression.predicate.operator.arithmetic.Div;
@@ -61,7 +61,7 @@
6161
public class Bucket extends GroupingFunction.EvaluatableGroupingFunction
6262
implements
6363
PostOptimizationVerificationAware,
64-
TwoOptionalArguments {
64+
ThreeOptionalArguments {
6565
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(Expression.class, "Bucket", Bucket::new);
6666

6767
// TODO maybe we should just cover the whole of representable dates here - like ten years, 100 years, 1000 years, all the way up.
@@ -93,6 +93,7 @@ public class Bucket extends GroupingFunction.EvaluatableGroupingFunction
9393
private final Expression buckets;
9494
private final Expression from;
9595
private final Expression to;
96+
private final Expression emitEmptyBuckets;
9697

9798
@FunctionInfo(
9899
returnType = { "double", "date", "date_nanos" },
@@ -211,13 +212,20 @@ public Bucket(
211212
type = { "integer", "long", "double", "date", "keyword", "text" },
212213
optional = true,
213214
description = "End of the range. Can be a number, a date or a date expressed as a string."
214-
) Expression to
215+
) Expression to,
216+
@Param(
217+
name = "emitEmptyBuckets",
218+
type = { "boolean" },
219+
optional = true,
220+
description = "Whether or not empty buckets should be emitted."
221+
) Expression emitEmptyBuckets
215222
) {
216-
super(source, fields(field, buckets, from, to));
223+
super(source, fields(field, buckets, from, to, emitEmptyBuckets));
217224
this.field = field;
218225
this.buckets = buckets;
219226
this.from = from;
220227
this.to = to;
228+
this.emitEmptyBuckets = emitEmptyBuckets;
221229
}
222230

223231
private Bucket(StreamInput in) throws IOException {
@@ -226,11 +234,12 @@ private Bucket(StreamInput in) throws IOException {
226234
in.readNamedWriteable(Expression.class),
227235
in.readNamedWriteable(Expression.class),
228236
in.readOptionalNamedWriteable(Expression.class),
237+
in.readOptionalNamedWriteable(Expression.class),
229238
in.readOptionalNamedWriteable(Expression.class)
230239
);
231240
}
232241

233-
private static List<Expression> fields(Expression field, Expression buckets, Expression from, Expression to) {
242+
private static List<Expression> fields(Expression field, Expression buckets, Expression from, Expression to, Expression emitEmptyBuckets) {
234243
List<Expression> list = new ArrayList<>(4);
235244
list.add(field);
236245
list.add(buckets);
@@ -240,6 +249,9 @@ private static List<Expression> fields(Expression field, Expression buckets, Exp
240249
list.add(to);
241250
}
242251
}
252+
if (emitEmptyBuckets != null) {
253+
list.add(emitEmptyBuckets);
254+
}
243255
return list;
244256
}
245257

@@ -250,6 +262,7 @@ public void writeTo(StreamOutput out) throws IOException {
250262
out.writeNamedWriteable(buckets);
251263
out.writeOptionalNamedWriteable(from);
252264
out.writeOptionalNamedWriteable(to);
265+
out.writeOptionalNamedWriteable(emitEmptyBuckets);
253266
}
254267

255268
@Override
@@ -259,7 +272,7 @@ public String getWriteableName() {
259272

260273
@Override
261274
public boolean foldable() {
262-
return field.foldable() && buckets.foldable() && (from == null || from.foldable()) && (to == null || to.foldable());
275+
return field.foldable() && buckets.foldable() && (from == null || from.foldable()) && (to == null || to.foldable()) && (emitEmptyBuckets == null || emitEmptyBuckets.foldable());
263276
}
264277

265278
@Override
@@ -460,12 +473,13 @@ public DataType dataType() {
460473
public Expression replaceChildren(List<Expression> newChildren) {
461474
Expression from = newChildren.size() > 2 ? newChildren.get(2) : null;
462475
Expression to = newChildren.size() > 3 ? newChildren.get(3) : null;
463-
return new Bucket(source(), newChildren.get(0), newChildren.get(1), from, to);
476+
Expression emitEmptyBuckets = newChildren.size() > 4 ? newChildren.get(4) : null;
477+
return new Bucket(source(), newChildren.get(0), newChildren.get(1), from, to, emitEmptyBuckets);
464478
}
465479

466480
@Override
467481
protected NodeInfo<? extends Expression> info() {
468-
return NodeInfo.create(this, Bucket::new, field, buckets, from, to);
482+
return NodeInfo.create(this, Bucket::new, field, buckets, from, to, emitEmptyBuckets);
469483
}
470484

471485
public Expression field() {
@@ -484,8 +498,12 @@ public Expression to() {
484498
return to;
485499
}
486500

501+
public Expression emitEmptyBuckets() {
502+
return emitEmptyBuckets;
503+
}
504+
487505
@Override
488506
public String toString() {
489-
return "Bucket{" + "field=" + field + ", buckets=" + buckets + ", from=" + from + ", to=" + to + '}';
507+
return "Bucket{" + "field=" + field + ", buckets=" + buckets + ", from=" + from + ", to=" + to + ", emitEmptyBuckets=" + emitEmptyBuckets + '}';
490508
}
491509
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/RailRoadDiagram.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ static String functionSignature(FunctionDefinition definition) throws IOExceptio
7272
// BUCKET requires optional args to be optional together, so we need custom code to do that
7373
var nextArg = args.get(++i);
7474
assert nextArg.optional();
75-
Sequence seq = new Sequence(new Literal(argName), new Syntax(","), new Literal(nextArg.name));
75+
var nexterArg = args.get(++i);
76+
assert nexterArg.optional();
77+
// TODO: Should it be possible to be able to specify "emitEmptyBuckets" but not "from" and "to"?
78+
Sequence seq = new Sequence(new Literal(argName), new Syntax(","), new Literal(nextArg.name), new Syntax(","), new Literal(nexterArg.name));
7679
argExpressions.add(new Repetition(seq, 0, 1));
7780
} else if (i < args.size() - 1 && args.get(i + 1).optional() == false) {
7881
// Special case with leading optional args

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketSerializationTests.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ protected Bucket createTestInstance() {
2121
Expression buckets = randomChild();
2222
Expression from = randomChild();
2323
Expression to = randomChild();
24-
return new Bucket(source, field, buckets, from, to);
24+
Expression emitEmptyBuckets = randomChild();
25+
return new Bucket(source, field, buckets, from, to, emitEmptyBuckets);
2526
}
2627

2728
@Override
@@ -31,12 +32,14 @@ protected Bucket mutateInstance(Bucket instance) throws IOException {
3132
Expression buckets = instance.buckets();
3233
Expression from = instance.from();
3334
Expression to = instance.to();
34-
switch (between(0, 3)) {
35+
Expression emitEmptyBuckets = instance.emitEmptyBuckets();
36+
switch (between(0, 4)) {
3537
case 0 -> field = randomValueOtherThan(field, AbstractExpressionSerializationTests::randomChild);
3638
case 1 -> buckets = randomValueOtherThan(buckets, AbstractExpressionSerializationTests::randomChild);
3739
case 2 -> from = randomValueOtherThan(from, AbstractExpressionSerializationTests::randomChild);
3840
case 3 -> to = randomValueOtherThan(to, AbstractExpressionSerializationTests::randomChild);
41+
case 4 -> emitEmptyBuckets = randomValueOtherThan(emitEmptyBuckets, AbstractExpressionSerializationTests::randomChild);
3942
}
40-
return new Bucket(source, field, buckets, from, to);
43+
return new Bucket(source, field, buckets, from, to, emitEmptyBuckets);
4144
}
4245
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/grouping/BucketTests.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,10 +319,14 @@ private static Matcher<Object> resultsMatcher(List<TestCaseSupplier.TypedData> t
319319
protected Expression build(Source source, List<Expression> args) {
320320
Expression from = null;
321321
Expression to = null;
322+
Expression emitEmptyBuckets = null;
322323
if (args.size() > 2) {
323324
from = args.get(2);
324325
to = args.get(3);
325326
}
326-
return new Bucket(source, args.get(0), args.get(1), from, to);
327+
if (args.size() > 4) {
328+
emitEmptyBuckets = args.get(4);
329+
}
330+
return new Bucket(source, args.get(0), args.get(1), from, to, emitEmptyBuckets);
327331
}
328332
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/optimizer/rules/logical/FoldNullTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ public void testNullFoldableDoesNotApplyToIsNullAndNotNull() {
265265
}
266266

267267
public void testNullBucketGetsFolded() {
268-
assertEquals(NULL, foldNull(new Bucket(EMPTY, NULL, NULL, NULL, NULL)));
268+
assertEquals(NULL, foldNull(new Bucket(EMPTY, NULL, NULL, NULL, NULL, NULL)));
269269
}
270270

271271
public void testNullCategorizeGroupingNotFolded() {

0 commit comments

Comments
 (0)