Skip to content

Commit 138bfec

Browse files
[ESQL] Add support for date trunc on date nanos type (#116354)
Resolves #110008 As discussed elsewhere, this does NOT allow for truncating to a value smaller than a millisecond. Our timespan literal syntax doesn't allow specifying less than a millisecond, and the rounding infrastructure also does not support it. We also had a discussion regarding the return type, and decided that it made sense to keep the type as date_nanos, even though the truncation will always produce a millisecond-rounded (or higher) value. --------- Co-authored-by: Elastic Machine <[email protected]>
1 parent b205c02 commit 138bfec

File tree

10 files changed

+286
-69
lines changed

10 files changed

+286
-69
lines changed

x-pack/plugin/esql/qa/testFixtures/src/main/resources/date_nanos.csv-spec

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,3 +439,23 @@ FROM date_nanos | WHERE millis > "2020-01-01" | STATS v = MV_SORT(VALUES(nanos),
439439
v:date_nanos
440440
[2023-10-23T13:55:01.543123456Z, 2023-10-23T13:53:55.832987654Z, 2023-10-23T13:52:55.015787878Z, 2023-10-23T13:51:54.732102837Z, 2023-10-23T13:33:34.937193000Z, 2023-10-23T12:27:28.948000000Z, 2023-10-23T12:15:03.360103847Z]
441441
;
442+
443+
Date trunc on date nanos
444+
required_capability: date_trunc_date_nanos
445+
446+
FROM date_nanos
447+
| WHERE millis > "2020-01-01"
448+
| EVAL yr = DATE_TRUNC(1 year, nanos), mo = DATE_TRUNC(1 month, nanos), mn = DATE_TRUNC(10 minutes, nanos), ms = DATE_TRUNC(1 millisecond, nanos)
449+
| SORT nanos DESC
450+
| KEEP yr, mo, mn, ms;
451+
452+
yr:date_nanos | mo:date_nanos | mn:date_nanos | ms:date_nanos
453+
2023-01-01T00:00:00.000000000Z | 2023-10-01T00:00:00.000000000Z | 2023-10-23T13:50:00.000000000Z | 2023-10-23T13:55:01.543000000Z
454+
2023-01-01T00:00:00.000000000Z | 2023-10-01T00:00:00.000000000Z | 2023-10-23T13:50:00.000000000Z | 2023-10-23T13:53:55.832000000Z
455+
2023-01-01T00:00:00.000000000Z | 2023-10-01T00:00:00.000000000Z | 2023-10-23T13:50:00.000000000Z | 2023-10-23T13:52:55.015000000Z
456+
2023-01-01T00:00:00.000000000Z | 2023-10-01T00:00:00.000000000Z | 2023-10-23T13:50:00.000000000Z | 2023-10-23T13:51:54.732000000Z
457+
2023-01-01T00:00:00.000000000Z | 2023-10-01T00:00:00.000000000Z | 2023-10-23T13:30:00.000000000Z | 2023-10-23T13:33:34.937000000Z
458+
2023-01-01T00:00:00.000000000Z | 2023-10-01T00:00:00.000000000Z | 2023-10-23T12:20:00.000000000Z | 2023-10-23T12:27:28.948000000Z
459+
2023-01-01T00:00:00.000000000Z | 2023-10-01T00:00:00.000000000Z | 2023-10-23T12:10:00.000000000Z | 2023-10-23T12:15:03.360000000Z
460+
2023-01-01T00:00:00.000000000Z | 2023-10-01T00:00:00.000000000Z | 2023-10-23T12:10:00.000000000Z | 2023-10-23T12:15:03.360000000Z
461+
;

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

Lines changed: 130 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,11 @@ public enum Cap {
329329
*/
330330
LEAST_GREATEST_FOR_DATENANOS(EsqlCorePlugin.DATE_NANOS_FEATURE_FLAG),
331331

332+
/**
333+
* Support for date_trunc function on date nanos type
334+
*/
335+
DATE_TRUNC_DATE_NANOS(EsqlCorePlugin.DATE_NANOS_FEATURE_FLAG),
336+
332337
/**
333338
* support aggregations on date nanos
334339
*/

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
252252
assert DataType.isTemporalAmount(buckets.dataType()) : "Unexpected span data type [" + buckets.dataType() + "]";
253253
preparedRounding = DateTrunc.createRounding(buckets.fold(), DEFAULT_TZ);
254254
}
255-
return DateTrunc.evaluator(source(), toEvaluator.apply(field), preparedRounding);
255+
return DateTrunc.evaluator(field.dataType(), source(), toEvaluator.apply(field), preparedRounding);
256256
}
257257
if (field.dataType().isNumeric()) {
258258
double roundTo;

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

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
1212
import org.elasticsearch.common.io.stream.StreamInput;
1313
import org.elasticsearch.common.io.stream.StreamOutput;
14+
import org.elasticsearch.common.time.DateUtils;
1415
import org.elasticsearch.compute.ann.Evaluator;
1516
import org.elasticsearch.compute.ann.Fixed;
1617
import org.elasticsearch.compute.operator.EvalOperator.ExpressionEvaluator;
@@ -31,12 +32,14 @@
3132
import java.time.ZoneId;
3233
import java.time.ZoneOffset;
3334
import java.util.List;
35+
import java.util.Map;
3436
import java.util.concurrent.TimeUnit;
3537

3638
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST;
3739
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND;
38-
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isDate;
3940
import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType;
41+
import static org.elasticsearch.xpack.esql.core.type.DataType.DATETIME;
42+
import static org.elasticsearch.xpack.esql.core.type.DataType.DATE_NANOS;
4043

4144
public class DateTrunc extends EsqlScalarFunction {
4245
public static final NamedWriteableRegistry.Entry ENTRY = new NamedWriteableRegistry.Entry(
@@ -45,6 +48,15 @@ public class DateTrunc extends EsqlScalarFunction {
4548
DateTrunc::new
4649
);
4750

51+
@FunctionalInterface
52+
public interface DateTruncFactoryProvider {
53+
ExpressionEvaluator.Factory apply(Source source, ExpressionEvaluator.Factory lhs, Rounding.Prepared rounding);
54+
}
55+
56+
private static final Map<DataType, DateTruncFactoryProvider> evaluatorMap = Map.ofEntries(
57+
Map.entry(DATETIME, DateTruncDatetimeEvaluator.Factory::new),
58+
Map.entry(DATE_NANOS, DateTruncDateNanosEvaluator.Factory::new)
59+
);
4860
private final Expression interval;
4961
private final Expression timestampField;
5062
protected static final ZoneId DEFAULT_TZ = ZoneOffset.UTC;
@@ -108,20 +120,28 @@ protected TypeResolution resolveType() {
108120
return new TypeResolution("Unresolved children");
109121
}
110122

123+
String operationName = sourceText();
111124
return isType(interval, DataType::isTemporalAmount, sourceText(), FIRST, "dateperiod", "timeduration").and(
112-
isDate(timestampField, sourceText(), SECOND)
125+
isType(timestampField, evaluatorMap::containsKey, operationName, SECOND, "date_nanos or datetime")
113126
);
114127
}
115128

116129
public DataType dataType() {
117-
return DataType.DATETIME;
130+
// Default to DATETIME in the case of nulls. This mimics the behavior before DATE_NANOS support
131+
return timestampField.dataType() == DataType.NULL ? DATETIME : timestampField.dataType();
118132
}
119133

120-
@Evaluator
121-
static long process(long fieldVal, @Fixed Rounding.Prepared rounding) {
134+
@Evaluator(extraName = "Datetime")
135+
static long processDatetime(long fieldVal, @Fixed Rounding.Prepared rounding) {
122136
return rounding.round(fieldVal);
123137
}
124138

139+
@Evaluator(extraName = "DateNanos")
140+
static long processDateNanos(long fieldVal, @Fixed Rounding.Prepared rounding) {
141+
// Currently, ES|QL doesn't support rounding to sub-millisecond values, so it's safe to cast before rounding.
142+
return DateUtils.toNanoSeconds(rounding.round(DateUtils.toMilliSeconds(fieldVal)));
143+
}
144+
125145
@Override
126146
public Expression replaceChildren(List<Expression> newChildren) {
127147
return new DateTrunc(source(), newChildren.get(0), newChildren.get(1));
@@ -214,14 +234,15 @@ public ExpressionEvaluator.Factory toEvaluator(ToEvaluator toEvaluator) {
214234
"Function [" + sourceText() + "] has invalid interval [" + interval.sourceText() + "]. " + e.getMessage()
215235
);
216236
}
217-
return evaluator(source(), fieldEvaluator, DateTrunc.createRounding(foldedInterval, DEFAULT_TZ));
237+
return evaluator(dataType(), source(), fieldEvaluator, DateTrunc.createRounding(foldedInterval, DEFAULT_TZ));
218238
}
219239

220240
public static ExpressionEvaluator.Factory evaluator(
241+
DataType forType,
221242
Source source,
222243
ExpressionEvaluator.Factory fieldEvaluator,
223244
Rounding.Prepared rounding
224245
) {
225-
return new DateTruncEvaluator.Factory(source, fieldEvaluator, rounding);
246+
return evaluatorMap.get(forType).apply(source, fieldEvaluator, rounding);
226247
}
227248
}

x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerTests.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1118,21 +1118,24 @@ public void testDateTruncOnInt() {
11181118
verifyUnsupported("""
11191119
from test
11201120
| eval date_trunc(1 month, int)
1121-
""", "second argument of [date_trunc(1 month, int)] must be [datetime], found value [int] type [integer]");
1121+
""", "second argument of [date_trunc(1 month, int)] must be [date_nanos or datetime], found value [int] type [integer]");
11221122
}
11231123

11241124
public void testDateTruncOnFloat() {
11251125
verifyUnsupported("""
11261126
from test
11271127
| eval date_trunc(1 month, float)
1128-
""", "second argument of [date_trunc(1 month, float)] must be [datetime], found value [float] type [double]");
1128+
""", "second argument of [date_trunc(1 month, float)] must be [date_nanos or datetime], found value [float] type [double]");
11291129
}
11301130

11311131
public void testDateTruncOnText() {
1132-
verifyUnsupported("""
1133-
from test
1134-
| eval date_trunc(1 month, keyword)
1135-
""", "second argument of [date_trunc(1 month, keyword)] must be [datetime], found value [keyword] type [keyword]");
1132+
verifyUnsupported(
1133+
"""
1134+
from test
1135+
| eval date_trunc(1 month, keyword)
1136+
""",
1137+
"second argument of [date_trunc(1 month, keyword)] must be [date_nanos or datetime], found value [keyword] type [keyword]"
1138+
);
11361139
}
11371140

11381141
public void testDateTruncWithNumericInterval() {

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ private static void dateCases(List<TestCaseSupplier> suppliers, String name, Lon
8787
args.add(dateBound("to", toType, "2023-03-01T09:00:00.00Z"));
8888
return new TestCaseSupplier.TestCase(
8989
args,
90-
"DateTruncEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding[DAY_OF_MONTH in Z][fixed to midnight]]",
90+
"DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], "
91+
+ "rounding=Rounding[DAY_OF_MONTH in Z][fixed to midnight]]",
9192
DataType.DATETIME,
9293
resultsMatcher(args)
9394
);
@@ -101,7 +102,7 @@ private static void dateCases(List<TestCaseSupplier> suppliers, String name, Lon
101102
args.add(dateBound("to", toType, "2023-02-17T12:00:00Z"));
102103
return new TestCaseSupplier.TestCase(
103104
args,
104-
"DateTruncEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding[3600000 in Z][fixed]]",
105+
"DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding[3600000 in Z][fixed]]",
105106
DataType.DATETIME,
106107
equalTo(Rounding.builder(Rounding.DateTimeUnit.HOUR_OF_DAY).build().prepareForUnknown().round(date.getAsLong()))
107108
);
@@ -134,7 +135,7 @@ private static void dateCasesWithSpan(
134135
args.add(new TestCaseSupplier.TypedData(span, spanType, "buckets").forceLiteral());
135136
return new TestCaseSupplier.TestCase(
136137
args,
137-
"DateTruncEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding" + spanStr + "]",
138+
"DateTruncDatetimeEvaluator[fieldVal=Attribute[channel=0], rounding=Rounding" + spanStr + "]",
138139
DataType.DATETIME,
139140
resultsMatcher(args)
140141
);

0 commit comments

Comments
 (0)