getDataTypeForStringLiteralConversion(Class extends Function> clazz) {
- return dataTypesForStringLiteralConversion.get(clazz);
+ return DATA_TYPES_FOR_STRING_LITERAL_CONVERSIONS.get(clazz);
}
private static class SnapshotFunctionRegistry extends EsqlFunctionRegistry {
diff --git a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverter.java b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverter.java
index 7fb998e82001e..93fba06aab988 100644
--- a/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverter.java
+++ b/x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/type/EsqlDataTypeConverter.java
@@ -276,27 +276,11 @@ public static TemporalAmount parseTemporalAmount(Object val, DataType expectedTy
return null;
}
StringBuilder value = new StringBuilder();
- StringBuilder qualifier = new StringBuilder();
- StringBuilder nextBuffer = value;
- boolean lastWasSpace = false;
- for (char c : str.trim().toCharArray()) {
- if (c == ' ') {
- if (lastWasSpace == false) {
- nextBuffer = nextBuffer == value ? qualifier : null;
- }
- lastWasSpace = true;
- continue;
- }
- if (nextBuffer == null) {
- throw new ParsingException(Source.EMPTY, errorMessage, val, expectedType);
- }
- nextBuffer.append(c);
- lastWasSpace = false;
- }
-
- if ((value.isEmpty() || qualifier.isEmpty()) == false) {
+ StringBuilder temporalUnit = new StringBuilder();
+ separateValueAndTemporalUnitForTemporalAmount(str.strip(), value, temporalUnit, errorMessage, expectedType.toString());
+ if ((value.isEmpty() || temporalUnit.isEmpty()) == false) {
try {
- TemporalAmount result = parseTemporalAmount(Integer.parseInt(value.toString()), qualifier.toString(), Source.EMPTY);
+ TemporalAmount result = parseTemporalAmount(Integer.parseInt(value.toString()), temporalUnit.toString(), Source.EMPTY);
if (DataType.DATE_PERIOD == expectedType && result instanceof Period
|| DataType.TIME_DURATION == expectedType && result instanceof Duration) {
return result;
@@ -314,6 +298,48 @@ public static TemporalAmount parseTemporalAmount(Object val, DataType expectedTy
throw new ParsingException(Source.EMPTY, errorMessage, val, expectedType);
}
+ public static TemporalAmount maybeParseTemporalAmount(String str) {
+ // The string literal can be either Date_Period or Time_Duration, derive the data type from its temporal unit
+ String errorMessage = "Cannot parse [{}] to {}";
+ String expectedTypes = DATE_PERIOD + " or " + TIME_DURATION;
+ StringBuilder value = new StringBuilder();
+ StringBuilder temporalUnit = new StringBuilder();
+ separateValueAndTemporalUnitForTemporalAmount(str, value, temporalUnit, errorMessage, expectedTypes);
+ if ((value.isEmpty() || temporalUnit.isEmpty()) == false) {
+ try {
+ return parseTemporalAmount(Integer.parseInt(value.toString()), temporalUnit.toString(), Source.EMPTY);
+ } catch (NumberFormatException ex) {
+ throw new ParsingException(Source.EMPTY, errorMessage, str, expectedTypes);
+ }
+ }
+ return null;
+ }
+
+ private static void separateValueAndTemporalUnitForTemporalAmount(
+ String temporalAmount,
+ StringBuilder value,
+ StringBuilder temporalUnit,
+ String errorMessage,
+ String expectedType
+ ) {
+ StringBuilder nextBuffer = value;
+ boolean lastWasSpace = false;
+ for (char c : temporalAmount.toCharArray()) {
+ if (c == ' ') {
+ if (lastWasSpace == false) {
+ nextBuffer = nextBuffer == value ? temporalUnit : null;
+ }
+ lastWasSpace = true;
+ continue;
+ }
+ if (nextBuffer == null) {
+ throw new ParsingException(Source.EMPTY, errorMessage, temporalAmount, expectedType);
+ }
+ nextBuffer.append(c);
+ lastWasSpace = false;
+ }
+ }
+
/**
* Converts arbitrary object to the desired data type.
*
@@ -401,10 +427,10 @@ public static DataType commonType(DataType left, DataType right) {
}
// generally supporting abbreviations from https://en.wikipedia.org/wiki/Unit_of_time
- public static TemporalAmount parseTemporalAmount(Number value, String qualifier, Source source) throws InvalidArgumentException,
+ public static TemporalAmount parseTemporalAmount(Number value, String temporalUnit, Source source) throws InvalidArgumentException,
ArithmeticException, ParsingException {
try {
- return switch (INTERVALS.valueOf(qualifier.toUpperCase(Locale.ROOT))) {
+ return switch (INTERVALS.valueOf(temporalUnit.toUpperCase(Locale.ROOT))) {
case MILLISECOND, MILLISECONDS, MS -> Duration.ofMillis(safeToLong(value));
case SECOND, SECONDS, SEC, S -> Duration.ofSeconds(safeToLong(value));
case MINUTE, MINUTES, MIN -> Duration.ofMinutes(safeToLong(value));
@@ -417,7 +443,7 @@ public static TemporalAmount parseTemporalAmount(Number value, String qualifier,
case YEAR, YEARS, YR, Y -> Period.ofYears(safeToInt(safeToLong(value)));
};
} catch (IllegalArgumentException e) {
- throw new ParsingException(source, "Unexpected time interval qualifier: '{}'", qualifier);
+ throw new ParsingException(source, "Unexpected temporal unit: '{}'", temporalUnit);
}
}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java
index 0a34d6cd848bb..d9225d266c213 100644
--- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/VerifierTests.java
@@ -1667,6 +1667,72 @@ public void testToDatePeriodToTimeDurationWithInvalidType() {
);
}
+ public void testIntervalAsString() {
+ // DateTrunc
+ for (String interval : List.of("1 minu", "1 dy", "1.5 minutes", "0.5 days", "minutes 1", "day 5")) {
+ assertThat(
+ error("from types | EVAL x = date_trunc(\"" + interval + "\", \"1991-06-26T00:00:00.000Z\")"),
+ containsString("1:35: Cannot convert string [" + interval + "] to [DATE_PERIOD or TIME_DURATION]")
+ );
+ assertThat(
+ error("from types | EVAL x = \"1991-06-26T00:00:00.000Z\", y = date_trunc(\"" + interval + "\", x::datetime)"),
+ containsString("1:67: Cannot convert string [" + interval + "] to [DATE_PERIOD or TIME_DURATION]")
+ );
+ }
+ for (String interval : List.of("1", "0.5", "invalid")) {
+ assertThat(
+ error("from types | EVAL x = date_trunc(\"" + interval + "\", \"1991-06-26T00:00:00.000Z\")"),
+ containsString(
+ "1:24: first argument of [date_trunc(\""
+ + interval
+ + "\", \"1991-06-26T00:00:00.000Z\")] must be [dateperiod or timeduration], found value [\""
+ + interval
+ + "\"] type [keyword]"
+ )
+ );
+ assertThat(
+ error("from types | EVAL x = \"1991-06-26T00:00:00.000Z\", y = date_trunc(\"" + interval + "\", x::datetime)"),
+ containsString(
+ "1:56: first argument of [date_trunc(\""
+ + interval
+ + "\", x::datetime)] "
+ + "must be [dateperiod or timeduration], found value [\""
+ + interval
+ + "\"] type [keyword]"
+ )
+ );
+ }
+
+ // Bucket
+ assertEquals(
+ "1:52: Cannot convert string [1 yar] to [DATE_PERIOD or TIME_DURATION], error [Unexpected temporal unit: 'yar']",
+ error("from test | stats max(emp_no) by bucket(hire_date, \"1 yar\")")
+ );
+ assertEquals(
+ "1:52: Cannot convert string [1 hur] to [DATE_PERIOD or TIME_DURATION], error [Unexpected temporal unit: 'hur']",
+ error("from test | stats max(emp_no) by bucket(hire_date, \"1 hur\")")
+ );
+ assertEquals(
+ "1:58: Cannot convert string [1 mu] to [DATE_PERIOD or TIME_DURATION], error [Unexpected temporal unit: 'mu']",
+ error("from test | stats max = max(emp_no) by bucket(hire_date, \"1 mu\") | sort max ")
+ );
+ assertEquals(
+ "1:34: second argument of [bucket(hire_date, \"1\")] must be [integral, date_period or time_duration], "
+ + "found value [\"1\"] type [keyword]",
+ error("from test | stats max(emp_no) by bucket(hire_date, \"1\")")
+ );
+ assertEquals(
+ "1:40: second argument of [bucket(hire_date, \"1\")] must be [integral, date_period or time_duration], "
+ + "found value [\"1\"] type [keyword]",
+ error("from test | stats max = max(emp_no) by bucket(hire_date, \"1\") | sort max ")
+ );
+ assertEquals(
+ "1:68: second argument of [bucket(y, \"1\")] must be [integral, date_period or time_duration], "
+ + "found value [\"1\"] type [keyword]",
+ error("from test | eval x = emp_no, y = hire_date | stats max = max(x) by bucket(y, \"1\") | sort max ")
+ );
+ }
+
private void query(String query) {
defaultAnalyzer.analyze(parser.createStatement(query));
}
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java
index 1e189c6eab038..1ecb471e882fe 100644
--- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/expression/function/AbstractFunctionTestCase.java
@@ -879,8 +879,7 @@ public static void renderDocs() throws IOException {
"elseValue",
trueValue.type(),
"The value that's returned when no condition evaluates to `true`.",
- true,
- EsqlFunctionRegistry.getTargetType(trueValue.type())
+ true
);
description = new EsqlFunctionRegistry.FunctionDescription(
description.name(),
@@ -1085,8 +1084,7 @@ private static void renderDocsForOperators(String name) throws IOException {
String[] type = paramInfo == null ? new String[] { "?" } : paramInfo.type();
String desc = paramInfo == null ? "" : paramInfo.description().replace('\n', ' ');
boolean optional = paramInfo == null ? false : paramInfo.optional();
- DataType targetDataType = EsqlFunctionRegistry.getTargetType(type);
- args.add(new EsqlFunctionRegistry.ArgSignature(paramName, type, desc, optional, targetDataType));
+ args.add(new EsqlFunctionRegistry.ArgSignature(paramName, type, desc, optional));
}
}
renderKibanaFunctionDefinition(name, functionInfo, args, likeOrInOperator(name));
diff --git a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/ExpressionTests.java b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/ExpressionTests.java
index 67b4dd71260aa..0177747d27243 100644
--- a/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/ExpressionTests.java
+++ b/x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/parser/ExpressionTests.java
@@ -431,7 +431,7 @@ public void testDatePeriodLiterals() {
}
public void testUnknownNumericQualifier() {
- assertParsingException(() -> whereExpression("1 decade"), "Unexpected time interval qualifier: 'decade'");
+ assertParsingException(() -> whereExpression("1 decade"), "Unexpected temporal unit: 'decade'");
}
public void testQualifiedDecimalLiteral() {
diff --git a/x-pack/plugin/inference/build.gradle b/x-pack/plugin/inference/build.gradle
index 15a2d0eb41368..29d5add35ff49 100644
--- a/x-pack/plugin/inference/build.gradle
+++ b/x-pack/plugin/inference/build.gradle
@@ -205,8 +205,14 @@ tasks.named("thirdPartyAudit").configure {
'io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueConsumerIndexField',
'io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerIndexField',
'io.netty.util.internal.shaded.org.jctools.queues.MpscArrayQueueProducerLimitField',
+ 'io.netty.util.internal.shaded.org.jctools.queues.MpmcArrayQueueConsumerIndexField',
+ 'io.netty.util.internal.shaded.org.jctools.queues.MpmcArrayQueueProducerIndexField',
+ 'io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueConsumerIndexField',
+ 'io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueProducerIndexField',
+ 'io.netty.util.internal.shaded.org.jctools.queues.unpadded.MpscUnpaddedArrayQueueProducerLimitField',
'io.netty.util.internal.shaded.org.jctools.util.UnsafeAccess',
'io.netty.util.internal.shaded.org.jctools.util.UnsafeRefArrayAccess',
+ 'io.netty.util.internal.shaded.org.jctools.util.UnsafeLongArrayAccess'
)
ignoreMissingClasses(
@@ -320,10 +326,9 @@ tasks.named("thirdPartyAudit").configure {
'com.aayushatharva.brotli4j.encoder.BrotliEncoderChannel',
'com.aayushatharva.brotli4j.encoder.Encoder$Mode',
'com.aayushatharva.brotli4j.encoder.Encoder$Parameters',
- 'com.github.luben.zstd.BaseZstdBufferDecompressingStreamNoFinalizer',
'com.github.luben.zstd.Zstd',
- 'com.github.luben.zstd.ZstdBufferDecompressingStreamNoFinalizer',
- 'com.github.luben.zstd.ZstdDirectBufferDecompressingStreamNoFinalizer',
+ 'com.github.luben.zstd.ZstdInputStreamNoFinalizer',
+ 'com.github.luben.zstd.util.Native',
'com.google.appengine.api.urlfetch.URLFetchServiceFactory',
'com.google.protobuf.nano.CodedOutputByteBufferNano',
'com.google.protobuf.nano.MessageNano',
diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/26_aggs_bucket.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/26_aggs_bucket.yml
index ea7684fb69a09..9fbe69ac05f0a 100644
--- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/26_aggs_bucket.yml
+++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/esql/26_aggs_bucket.yml
@@ -234,3 +234,58 @@
- match: { values.2.1: "2024-08-01T00:00:00.000Z" }
- match: { values.3.0: 1 }
- match: { values.3.1: "2024-09-01T00:00:00.000Z" }
+
+---
+"Datetime interval as string":
+ - requires:
+ test_runner_features: [allowed_warnings_regex, capabilities]
+ capabilities:
+ - method: POST
+ path: /_query
+ parameters: [ ]
+ capabilities: [ implicit_casting_string_literal_to_temporal_amount ]
+ reason: "interval in parameters as string"
+
+ - do:
+ indices.create:
+ index: test_bucket
+ body:
+ mappings:
+ properties:
+ ts :
+ type : date
+
+ - do:
+ bulk:
+ refresh: true
+ body:
+ - { "index": { "_index": "test_bucket" } }
+ - { "ts": "2024-06-16" }
+ - { "index": { "_index": "test_bucket" } }
+ - { "ts": "2024-07-16" }
+ - { "index": { "_index": "test_bucket" } }
+ - { "ts": "2024-08-16" }
+ - { "index": { "_index": "test_bucket" } }
+ - { "ts": "2024-09-16" }
+
+ - do:
+ allowed_warnings_regex:
+ - "No limit defined, adding default limit of \\[.*\\]"
+ esql.query:
+ body:
+ query: 'FROM test_bucket | STATS c = COUNT(*) BY b = BUCKET(ts, ?bucket) | SORT b'
+ params: [{"bucket" : "1 month"}]
+
+ - match: { columns.0.name: c }
+ - match: { columns.0.type: long }
+ - match: { columns.1.name: b }
+ - match: { columns.1.type: date }
+ - length: { values: 4 }
+ - match: { values.0.0: 1 }
+ - match: { values.0.1: "2024-06-01T00:00:00.000Z" }
+ - match: { values.1.0: 1 }
+ - match: { values.1.1: "2024-07-01T00:00:00.000Z" }
+ - match: { values.2.0: 1 }
+ - match: { values.2.1: "2024-08-01T00:00:00.000Z" }
+ - match: { values.3.0: 1 }
+ - match: { values.3.1: "2024-09-01T00:00:00.000Z" }